Skip to content

Simplify bulk onboarding auth methods - #505

Open
Walter Luna (walterluna) wants to merge 11 commits into
microsoft:mainfrom
walterluna:simplify-bulk-onboarding-auth-methods
Open

Walter Luna (walterluna) wants to merge 11 commits into
microsoft:mainfrom
walterluna:simplify-bulk-onboarding-auth-methods

Conversation

@walterluna

@walterluna Walter Luna (walterluna) commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Simplifies bulk onboarding authentication so every advertised runner authentication method is supported consistently across create, update, remove, orchestration, and bulk execution flows.

The supported runner authentication methods are now:

  • Client secret
  • Certificate by thumbprint,  X509Certificate2  object, or PFX file
  • System-assigned managed identity
  • Interactive delegated sign-in

Partially implemented runner authentication paths—including user-assigned managed identity selection, raw access tokens, federated/client assertions, GitHub OIDC, Azure CLI/Az PowerShell tokens, existing Graph sessions, and removal-only device code—have been removed.

Changes

  • Normalizes authentication parameters and validation across the bulk wrapper, orchestrator, and entity scripts.
  • Adds certificate object and PFX support to removal operations.
  • Supports interactive delegated authentication for AgentUser operations when a caller-controlled client ID is supplied.
  • Updates automation-app setup to declare the selected application permissions and delegated scopes even when  -SkipGrant  is used, allowing an administrator to grant consent later through Microsoft Entra admin center.
  • Enables public-client flows when delegated scopes are selected.
  • Retries immediate post-creation permission declaration reads and writes to handle Microsoft Entra replication delays.
  • Fixes interactive automation-app setup when no client secret or authentication certificate is supplied.
  • Updates bulk onboarding documentation and test fixtures to match the supported authentication contract.

Declare application roles and delegated scopes with -SkipGrant so
administrators can grant consent in Entra without running the script.

- Preserve existing API permissions and avoid duplicate declarations
- Report declared permissions, consent failures, and portal links
- Add regression coverage for permission merging and idempotency
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 744dc687-79b1-4181-a626-57d46d06260f
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 744dc687-79b1-4181-a626-57d46d06260f
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 744dc687-79b1-4181-a626-57d46d06260f
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 744dc687-79b1-4181-a626-57d46d06260f
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 744dc687-79b1-4181-a626-57d46d06260f
Copilot AI balanced review requested due to automatic review settings October 2, 2026 14:06
@walterluna

Copy link
Copy Markdown
Contributor Author

Copilot resolve the merge conflicts in this pull request

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Copilot review overview

Review effort: Lite
Findings: 2 High severity · 2 Medium severity · 1 Low severity

Open (5)
What changed in this PR

This PR streamlines the authentication surface across bulk onboarding/orchestration and update/remove wrappers by removing deprecated token/tool-based auth paths and standardizing on a smaller set of supported modes, while extending interactive delegated support and adding automated coverage around permission declaration behavior.

Changes:

  • Removed -AccessToken and other deprecated auth options from wrappers and step scripts; standardized on client secret, certificate, system-assigned managed identity, and interactive delegated sign-in.
  • Added/updated tests and fixtures to validate secure auth forwarding and new permission declaration/report semantics.
  • Enhanced New-A365AutomationApp.ps1 to declare permissions (and reconcile duplicates) separately from granting consent, with richer reporting and documentation updates.
File Description
scripts/​bulk-agent-registration/​tests/​fixtures/​update-wrappers/​New-A365AgentUser.ps1 Updates test wrapper fixture to stop capturing/accepting access tokens and reflect new auth contract.
scripts/​bulk-agent-registration/​tests/​fixtures/​update-wrappers/​New-A365AgentRegistration.ps1 Updates fixture auth parameters and removes access-token capture.
scripts/​bulk-agent-registration/​tests/​fixtures/​update-wrappers/​New-A365AgentIdentity.ps1 Updates fixture auth parameters and removes access-token capture.
scripts/​bulk-agent-registration/​tests/​fixtures/​update-wrappers/​New-A365AgentBlueprint.ps1 Updates fixture auth parameters and removes access-token capture.
scripts/​bulk-agent-registration/​tests/​fixtures/​A365-AutomationOrchestrator.ps1 Adjusts orchestrator fixture call capture to include Interactive.
scripts/​bulk-agent-registration/​tests/​SecureAuthenticationInput.Tests.ps1 Updates secure-string forwarding tests and adds coverage for interactive auth without cert source.
scripts/​bulk-agent-registration/​tests/​PermissionDeclaration.Tests.ps1 Adds comprehensive tests for permission declaration merge/approval/WhatIf/report semantics.
scripts/​bulk-agent-registration/​tests/​Execution.Tests.ps1 Updates execution preflight expectations for the simplified auth rules, including interactive support.
scripts/​bulk-agent-registration/​readme.md Updates docs for simplified auth matrix and outlines permission declaration reporting semantics.
scripts/​bulk-agent-registration/​Update-A365Blueprint.ps1 Removes -AccessToken plumbing and aligns forwarded auth parameters.
scripts/​bulk-agent-registration/​Update-A365AgentUser.ps1 Updates wrapper to support delegated interactive auth and removes deprecated auth parameters.
scripts/​bulk-agent-registration/​Update-A365AgentRegistration.ps1 Removes -AccessToken plumbing and aligns forwarded auth parameters.
scripts/​bulk-agent-registration/​Update-A365AgentIdentity.ps1 Removes -AccessToken plumbing and aligns forwarded auth parameters.
scripts/​bulk-agent-registration/​Remove-A365Blueprint.ps1 Reworks auth modes: drops access-token/device-code/existing-connection and adds cert object/path + MSI.
scripts/​bulk-agent-registration/​Remove-A365AgentUser.ps1 Reworks auth modes and adds interactive requirements for AgentUser delegated preview scopes.
scripts/​bulk-agent-registration/​Remove-A365AgentRegistration.ps1 Reworks auth modes similarly and removes access-token-specific JWT parsing helpers.
scripts/​bulk-agent-registration/​Remove-A365AgentIdentity.ps1 Reworks auth modes: drops deprecated options and adds cert object/path + MSI support.
scripts/​bulk-agent-registration/​New-A365AutomationApp.ps1 Adds permission declaration merge/approval reporting and updates auth handling (removing access token + federated creds).
scripts/​bulk-agent-registration/​New-A365AgentUser.ps1 Removes multiple auth methods, adds interactive device-code delegated auth, and updates docs/messages.
scripts/​bulk-agent-registration/​New-A365AgentRegistration.ps1 Removes -AccessToken support and enforces system-assigned managed identity only.
scripts/​bulk-agent-registration/​New-A365AgentIdentity.ps1 Removes -AccessToken support and enforces system-assigned managed identity only.
scripts/​bulk-agent-registration/​New-A365AgentBlueprint.ps1 Removes -AccessToken support and enforces system-assigned managed identity only.
scripts/​bulk-agent-registration/​A365-BulkOnboarding.ps1 Updates preflight auth validation and removes access-token mode; adds certificate-source validation.
scripts/​bulk-agent-registration/​A365-AutomationOrchestrator.ps1 Updates auth preflight, forwarding splats, and AgentUser interactive eligibility rules.
CHANGELOG.md Notes the unified auth contract and removal of deprecated auth paths.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread scripts/bulk-agent-registration/New-A365AutomationApp.ps1
Comment thread scripts/bulk-agent-registration/New-A365AutomationApp.ps1
Comment thread scripts/bulk-agent-registration/Remove-A365AgentUser.ps1
Comment thread scripts/bulk-agent-registration/readme.md
Prevent certificate authentication from persisting imported private keys to user or machine profile stores. Add coverage for password-protected and passwordless PFX loaders and include the registration fixture formatting update.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 744dc687-79b1-4181-a626-57d46d06260f
Copilot AI balanced review requested due to automatic review settings October 6, 2026 14:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Interactive scenario configuration, cascade authentication, environment-secret forwarding, and the generated admin-consent link have unresolved functional defects.

Review effort: Balanced
Findings: 1 High severity

Open (1)
Resolved since last review (5)
Previously missed (5)

In code that hasn't changed since last review

Medium severity Removal scripts omit environment-based client secret

scripts/​bulk-agent-registration/​A365-AutomationOrchestrator.ps1:2136

Environment-based client-secret auth is detected above, but this splat forwards only explicitly bound parameters. When A365_CLIENT_SECRET is the selected credential, create scripts recover it themselves, whereas the removal scripts do not, so an orchestrated removal reaches them with no authentication method and fails. Materialize the selected environment secret into the shared splat.

Medium severity Interactive scenarios lack required delegated permission scopes

scripts/​bulk-agent-registration/​New-A365AutomationApp.ps1:1196

Delegated permission setup remains incomplete for two advertised interactive scenarios. Blueprint adds no delegated scopes at all, while AgentIdentity adds only custom-security-attribute scopes, but the corresponding runners request the core AgentIdentityBlueprint.*, AgentIdentity.*, User.Read, and User.ReadBasic.All scopes. Apps created for those scenarios therefore are not enabled/prepared for the interactive flow. Add scenario-specific delegated scope sets matching each runner before computing isFallbackPublicClient.

Medium severity Admin consent link omits required redirect URI

scripts/​bulk-agent-registration/​tests/​PermissionDeclaration.Tests.ps1:286

This expectation codifies an unusable Entra admin-consent link: /adminconsent needs a registered, URL-encoded redirect_uri, and omitting it can produce AADSTS500113. Either configure/include a matching redirect URI or stop emitting this link and direct users to portalPermissionsUrl; then make the test assert that protocol requirement explicitly.

Low severity Device-code authentication lacks behavioral test coverage

scripts/​bulk-agent-registration/​New-A365AgentUser.ps1:1176

The new device-code implementation has no behavioral tests. Current additions test bulk forwarding and a separate Connect-MgGraph helper, but do not verify the device-code request, authorization_pending/slow_down polling, terminal errors, or timeout behavior. Add extracted-function tests with mocked Invoke-RestMethod and Start-Sleep so regressions in this authentication path are caught without network calls.

Low severity Oversized test file combines unrelated test concerns

scripts/​bulk-agent-registration/​tests/​PermissionDeclaration.Tests.ps1:47

This new test file is 809 lines and combines merge-helper unit tests, full-script source rewriting, permission-report assertions, retry behavior, and authentication configuration. Split it into focused test files (for example merge logic versus end-to-end declaration scenarios) so fixtures and failures remain maintainable.

Comment thread scripts/bulk-agent-registration/Remove-A365Blueprint.ps1 Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Interactive blueprint cascades can fail for AgentUser dependents, and routine delegated authentication requests unused privileged scopes.

Review effort: Balanced
Findings: 1 High severity · 2 Medium severity · 1 Low severity

Open (4)

Comment thread scripts/bulk-agent-registration/New-A365AgentUser.ps1 Outdated
Comment thread scripts/bulk-agent-registration/New-A365AutomationApp.ps1 Outdated
Comment thread scripts/bulk-agent-registration/A365-BulkOnboarding.ps1
Forward the resolved tenant through blueprint removal cascades and require a caller-controlled client ID before deleting AgentUsers interactively.

Limit routine AgentUser interactive tokens to provisioning scopes, request app-role assignment permissions only for permission configuration, and remove the unused delegated-grant scope from automation app declarations.

Add regression coverage for cascade authentication and least-privilege interactive scope selection, and document the supported bulk authentication contract in the release notes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 744dc687-79b1-4181-a626-57d46d06260f
Copilot AI balanced review requested due to automatic review settings October 6, 2026 17:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Delegated permission declarations are inconsistent, one warning misrepresents valid interactive-only apps, and interactive update forwarding lacks regression coverage.

Review effort: Balanced
Findings: 1 High severity · 1 Low severity

Open (2)
Resolved since last review (3)
Previously missed (2)

In code that hasn't changed since last review

Medium severity Correctly distinguish interactive-only authentication from unusable clients

scripts/​bulk-agent-registration/​New-A365AutomationApp.ps1:1436

This warning is false when delegated scopes were selected: the code enables public-client flows, so the application can authenticate interactively without a secret or certificate. Distinguish “interactive-only” from “cannot authenticate” to avoid telling users that a valid public client is unusable.

Low severity Add wrapper test coverage for forwarding the Interactive switch

scripts/​bulk-agent-registration/​Update-A365AgentUser.ps1:155

The wrapper now exposes -Interactive, but its wrapper tests still exercise only client-secret/password forwarding, and the AgentUser fixture does not even declare -Interactive. Add an invocation test that passes this switch and asserts the fixture receives it; otherwise this newly supported update path can regress while the suite remains green.

Comment thread scripts/bulk-agent-registration/New-A365AutomationApp.ps1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The authentication normalization is directionally sound and thoughtfully documented, but cross-script invariants are not yet enforced consistently enough for this head to merge safely.

Four things to address before this merges; details inline.

Comment on lines 87 to +101

function New-A365DeclarationFixture {
$roleNames = @('CustomSecAttributeAssignment.ReadWrite.All', 'CustomSecAttributeDefinition.Read.All')
$agentUserScopeNames = @(
'User.Read',
'User.Read.All',
'User.ReadWrite.All',
'Directory.Read.All',
'Organization.Read.All',
'Application.Read.All',
'AgentIdentity.Read.All',
'AgentIdentity.ReadWrite.All',
'AgentIdUser.ReadWrite.All',
'LicenseAssignment.ReadWrite.All',
'AppRoleAssignment.ReadWrite.All'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: Do not codify an incomplete bootstrap permission set

The AgentUser fixture requires AppRoleAssignment.ReadWrite.All for every generated application but omits Application.ReadWrite.All. That is neither the least-privilege contract for routine AgentUser provisioning nor the complete contract for -ConfigurePermissions, so this test now protects an over-privileged but still unusable declaration.

Evidence

PermissionDeclaration.Tests.ps1:87-102 includes AppRoleAssignment.ReadWrite.All, and lines 592-601 require every listed scope to be declared. In contrast, AgentUserInteractiveScopes.Tests.ps1:46-57 proves routine provisioning must exclude both Application.ReadWrite.All and AppRoleAssignment.ReadWrite.All, while lines 60-69 prove -ConfigurePermissions requires both. Production follows those runtime expectations in New-A365AgentUser.ps1:1151-1170, but New-A365AutomationApp.ps1:1068-1082 declares only AppRoleAssignment.ReadWrite.All and lines 1275-1290 persist that declaration even for portal-consent workflows. An administrator who portal-consents the generated AgentUser app therefore grants a broad permission ordinary runs do not use, yet a later interactive -ConfigurePermissions run still requests undeclared Application.ReadWrite.All and requires additional consent or fails for an operator unable to grant it.

Suggested fix: Keep both permission-management scopes out of the default AgentUser declaration. If generated apps are intended to support permission bootstrapping, gate that capability explicitly and require both Application.ReadWrite.All and AppRoleAssignment.ReadWrite.All together in a separate bootstrap test case.

Comment on lines +1280 to +1283
if ($delegatedToRequest.Count -gt 0 -and
((-not (Test-HasProperty $current 'isFallbackPublicClient')) -or (-not $current.isFallbackPublicClient))) {
if ($PSCmdlet.ShouldProcess($DisplayName, 'PATCH isFallbackPublicClient (enable interactive public-client flows)')) {
Invoke-Graph -Method PATCH -Uri "/applications/$applicationObjectId" -Body @{ isFallbackPublicClient = $true } @replicationRetry | Out-Null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should fix: Generated public client has no redirect URI, so the newly required -Interactive -ClientId runs fail at Connect-MgGraph

This PR makes -ClientId mandatory for every interactive run that touches AgentUsers: the orchestrator, the bulk runner, Remove-A365AgentUser and the Remove-A365Blueprint cascade. It also sets up the generated automation app as that client: the AgentUser delegated scopes are declared on it, and the README says it 'can be used for interactive device-code authentication'.

However, the app only gets isFallbackPublicClient = true, and no publicClient.redirectUris are ever registered. That flag only enables flows without a redirect (device code, ROPC, IWA). Only New-A365AgentUser.ps1 uses device code. Every other step in the same run calls Connect-MgGraph -TenantId -ClientId -Scopes, which is the browser/WAM authorization-code flow: New-A365AgentBlueprint, New-A365AgentIdentity, New-A365AgentRegistration, Remove-A365AgentUser, Remove-A365AgentIdentity and Remove-A365Blueprint. That flow needs http://localhost registered on the client, plus the WAM broker URI on Windows. With the generated app as -ClientId, those steps fail at sign-in (no reply address registered) before any work is done.

Before this change there were two ways around this: interactive runs could omit -ClientId and use the Graph SDK's first-party client, and Remove-A365AgentUser had -UseDeviceCode. This PR removes both.

Evidence

What the app gets

  • New-A365AutomationApp.ps1:1253-1258 creates the app with only displayName, signInAudience, notes and isFallbackPublicClient.
  • :1280-1286 PATCHes only @{ isFallbackPublicClient = $true } and prints 'Enabled public-client flows for interactive delegated authentication.'
  • Searching scripts/bulk-agent-registration for localhost|redirect uri|reply address|brokerplugin finds nothing, so no redirect URI is ever registered.

Where users are pointed at this app

  • readme.md:255: 'AgentUser operations also require -ClientId for a caller-controlled public client'.
  • readme.md:259: '...so the generated application can be used for interactive device-code authentication'.
  • New-A365AutomationApp.ps1:1068-1069: the AgentUser scopes are declared on the automation app.
  • :1650-1653: the next-step hint is New-A365AgentRegistration.ps1 -Interactive -ClientId $applicationAppId.

Where ClientId is now required

  • A365-AutomationOrchestrator.ps1:2126-2128
  • A365-BulkOnboarding.ps1:472-475
  • Remove-A365AgentUser.ps1:719-721
  • Remove-A365Blueprint.ps1:984-986

Where it reaches Connect-MgGraph

  • A365-AutomationOrchestrator.ps1:2133-2137 forwards ClientId to every phase via $authSplat.
  • New-A365AgentBlueprint.ps1:1923-1930 sets $connect.ClientId = $ClientId plus Scopes, then calls Connect-MgGraph @connect (interactive auth-code flow).
  • Remove-A365AgentUser.ps1:734 and :778 do the same.

Repo evidence for the requirement

  • src/Microsoft.Agents.A365.DevTools.Cli/Constants/AuthenticationConstants.cs:42-43 says http://localhost is 'Required by the Microsoft Graph PowerShell SDK (Connect-MgGraph -ClientId)'.
  • Line 62 gives the WAM broker URI format.
  • Line 147 notes that AADSTS500113 is returned when an app has no redirect URI.

Failing scenarios

  • Run New-A365AutomationApp.ps1 -Interactive with the default -Scenario All, then A365-AutomationOrchestrator.ps1 -Interactive -ClientId with an AgentUser phase. The phase-1 Connect-MgGraph fails before any provisioning.
  • Standalone Remove-A365AgentUser.ps1 -Interactive -ClientId fails the same way, and its -UseDeviceCode alternative was removed in this PR.

Suggested fix: When delegated scopes are requested, also merge publicClient.redirectUris into the same PATCH that sets isFallbackPublicClient: add http://localhost and ms-appx-web://microsoft.aad.brokerplugin/<appId>, keeping any existing URIs. At minimum, document the manual redirect-URI step in readme section 5 and in the script's next-step output.

Comment on lines +905 to +906
$password = ConvertTo-SecureStringValue -Value $CertificatePassword -Name 'AuthCertificatePassword'
$loadedCertificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: Auth PFX loader passes an unresolved relative path and a $null password to X509Certificate2

The new -AuthCertificatePath support differs from every sibling PFX loader in two ways:

  1. Relative paths: the path goes straight into X509Certificate2::new after Test-Path. Test-Path resolves relative paths against PowerShell's location, but .NET resolves them against the process working directory, which Set-Location doesn't change. So -AuthCertificatePath ./auth.pfx passes the existence check, then fails with file-not-found once the user has changed directory. The siblings, and this file's own credential upload at line 1390, call Resolve-Path ... .ProviderPath first.
  2. No password: for a PFX without a password (-AuthCertificatePassword is documented as optional), ConvertTo-SecureStringValue returns $null. $null matches the (string, string, flags) and (string, SecureString, flags) overloads equally, so PowerShell can't choose between them. This PR changed the siblings to pass [string]::Empty explicitly in that branch for exactly this reason.
Evidence

This loader (New-A365AutomationApp.ps1)

  • :902 checks Test-Path -LiteralPath $CertificatePath -PathType Leaf.
  • :906-910 calls [X509Certificate2]::new($CertificatePath, $password, EphemeralKeySet) with the raw parameter string.
  • :905 sets $password = ConvertTo-SecureStringValue -Value $CertificatePassword -Name 'AuthCertificatePassword', which returns $null when the value is absent (:466 if ($null -eq $Value) { return $null }).

Siblings

  • They resolve the path first: New-A365AgentBlueprint.ps1:1904 $pfx = (Resolve-Path -LiteralPath $CertificatePath).ProviderPath, and New-A365AutomationApp.ps1:1390 does the same.
  • They branch on the password and use [string]::Empty when there is none: New-A365AgentBlueprint.ps1:1906-1911, New-A365AgentIdentity.ps1:2050-2055, New-A365AgentRegistration.ps1:1190-1195, Remove-A365AgentUser.ps1:752-757.
  • The diff changed the siblings' passwordless call from ::new($pfx) to this explicit form.

Documentation

  • readme.md:259 documents '-AuthCertificatePath plus optional -AuthCertificatePassword'.

Failing scenario

  • In a pwsh session started in the home directory, run cd C:/a365 and then ./New-A365AutomationApp.ps1 -TenantId t -ClientId c -AuthCertificatePath ./auth.pfx -AuthCertificatePassword p.
  • Test-Path succeeds, then the constructor looks for ~/auth.pfx and fails.
  • Without -AuthCertificatePassword, the constructor call is ambiguous between the two overloads.

Suggested fix: Make this loader match its siblings: resolve the path with Resolve-Path, and branch on the password, passing [string]::Empty when there is none.

Suggested change
$password = ConvertTo-SecureStringValue -Value $CertificatePassword -Name 'AuthCertificatePassword'
$loadedCertificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new(
$password = ConvertTo-SecureStringValue -Value $CertificatePassword -Name 'AuthCertificatePassword'
$pfx = (Resolve-Path -LiteralPath $CertificatePath).ProviderPath
$keyStorageFlags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::EphemeralKeySet
$loadedCertificate = if ($password) {
[System.Security.Cryptography.X509Certificates.X509Certificate2]::new($pfx, $password, $keyStorageFlags)
}
else {
[System.Security.Cryptography.X509Certificates.X509Certificate2]::new($pfx, [string]::Empty, $keyStorageFlags)
}

Comment on lines +1175 to +1180
function Get-InteractiveToken {
$client = if ([string]::IsNullOrWhiteSpace($ClientId)) { '04b07795-8ddb-461a-bbee-02f9e1bf7b46' } else { $ClientId }
$scopes = Get-InteractiveDelegatedScopes
$deviceCodeResponse = Invoke-RestMethod -Method Post -Uri "$($script:Authority)/$($script:TenantId)/oauth2/v2.0/devicecode" -Body @{
client_id = $client
scope = ($scopes -join ' ')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: Interactive bulk runs start a new device-code sign-in for every AgentUser row

Get-InteractiveToken runs a fresh device-code flow every time New-A365AgentUser.ps1 is invoked. It keeps only the access_token in $script:CachedToken (no refresh token, no offline_access), and the script clears that cache at startup.

Bulk onboarding calls the orchestrator once per CSV row, and the orchestrator calls New-A365AgentUser.ps1 once per AgentUser. An interactive CSV with N AgentUser rows therefore needs N separate device-code completions, each with code entry and MFA. The Connect-MgGraph-based rows reuse the SDK token cache and stay silent after the first sign-in.

If the operator steps away, each AgentUser row blocks for the device-code lifetime (about 15 minutes) and then fails. This PR is what newly allows interactive AgentUser rows in bulk mode: the previous up-front refusal was replaced.

Evidence

Token acquisition

  • New-A365AgentUser.ps1:1178-1181 POSTs to /oauth2/v2.0/devicecode on every call.
  • :1190-1216 polls until expires_in.
  • :1198 returns only $tokenResponse.access_token.
  • :1218 then throws 'Interactive delegated sign-in timed out before authorization completed.'

Cache

  • :1264-1265 is the only cache.
  • :2467-2468 resets it with $script:CachedToken = $null at the start of every run.

Call chain

  • A365-AutomationOrchestrator.ps1:2623 and :2634 invoke & $agentUserScript @auArgs once per orchestrator run.
  • A365-BulkOnboarding.ps1:495-525 calls & $orchestratorPath @rowArgs once per plan node, with the same -Interactive -ClientId on every row.

What changed

  • The base version of A365-BulkOnboarding.ps1 refused interactive runs that had AgentUser rows ('...which require app-only authentication').
  • This PR replaced that with only a ClientId check at :472-475.

Failing scenario

  • A CSV with 20 AgentUser rows run with -Interactive -ClientId X gives 20 separate device-code prompts.
  • Any prompt that isn't completed stalls its row for about 15 minutes, then the row fails with the timeout error.

Suggested fix: Reuse the delegated token across invocations within the process. For example, keep a session- or module-scoped cache keyed by tenant + client + scope set that honours expiry, or request offline_access and redeem the refresh token on later invocations. Alternatively, acquire the token once in the orchestrator or bulk runner and pass it down.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants