fix: device code fallback for macOS browser auth and cross-platform URL opening#309
Conversation
…RL opening - MsalBrowserCredential: catch PlatformNotSupportedException and fall back to device code flow instead of re-throwing, so macOS/Linux headless environments always get a working auth path - InteractiveGraphAuthService: eagerly acquire token before constructing GraphServiceClient to surface auth failures at construction time rather than silently returning a broken client (Gap 2 from PR #290) - InteractiveGraphAuthService: inject optional credentialFactory for testability - AuthenticationService: replace Console.WriteLine with structured logger calls in device code callback - Add BrowserHelper with cross-platform TryOpenUrl (Windows/macOS/Linux) - Remove duplicated TryOpenBrowser methods from BlueprintSubcommand and A365CreateInstanceRunner; both now call BrowserHelper.TryOpenUrl directly - Add 5 unit tests covering the eager-auth gap and credential caching behavior
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
There was a problem hiding this comment.
Pull request overview
This PR improves interactive authentication reliability across macOS/Linux (including headless environments) by ensuring there is always a working fallback path, and by centralizing cross-platform URL opening logic used during admin consent flows.
Changes:
- Add eager token acquisition in
InteractiveGraphAuthService(plus injectable credential factory) to surface auth failures immediately and improve testability. - Implement device-code fallback when browser-based MSAL auth is unsupported, and convert device-code output to structured logging.
- Introduce
BrowserHelper.TryOpenUrland replace duplicated “open browser” implementations with the shared helper.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Services/InteractiveGraphAuthServiceTests.cs | Adds unit tests for eager-auth failure surfacing and tenant client caching behavior. |
| src/Microsoft.Agents.A365.DevTools.Cli/Services/MsalBrowserCredential.cs | Falls back to MSAL device code flow when browser auth is not supported. |
| src/Microsoft.Agents.A365.DevTools.Cli/Services/InteractiveGraphAuthService.cs | Eagerly acquires a token before returning a GraphServiceClient; adds optional credentialFactory. |
| src/Microsoft.Agents.A365.DevTools.Cli/Services/AuthenticationService.cs | Replaces Console.WriteLine device-code messaging with structured logger calls. |
| src/Microsoft.Agents.A365.DevTools.Cli/Services/A365CreateInstanceRunner.cs | Uses shared BrowserHelper.TryOpenUrl instead of local browser-opening code. |
| src/Microsoft.Agents.A365.DevTools.Cli/Helpers/BrowserHelper.cs | New cross-platform URL opening helper (Windows/macOS/Linux). |
| src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/BlueprintSubcommand.cs | Switches admin consent browser opening to BrowserHelper.TryOpenUrl. |
You can also share your feedback on Copilot code review. Take the survey.
src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/BlueprintSubcommand.cs
Outdated
Show resolved
Hide resolved
- MsalBrowserCredential: also catch MsalClientException(linux_xdg_open_failed) for device code fallback on Linux/WSL (confirmed via WSL Ubuntu 24.04 repro); extract shared AcquireTokenWithDeviceCodeFallbackAsync to avoid duplication - BrowserHelper: fall back to Console.Error when logger is null so the manual URL is never silently lost regardless of caller - BlueprintSubcommand: log consent URL before TryOpenUrl and pass logger so users on headless platforms always see the URL to complete admin consent Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t, clean AADSTS7000218 error - ClientAppValidator: add EnsurePublicClientFlowsEnabledAsync (Step 7) that auto-detects and enables 'Allow public client flows' on the app registration via az rest GET/PATCH — required for MSAL device code fallback on macOS/Linux; non-fatal if PATCH fails - BlueprintSubcommand: run config requirements checks (LocationCheck + ClientAppCheck) before blueprint logic, with --skip-requirements opt-out; matches AllSubcommand pattern - MsalBrowserCredential: catch AADSTS7000218/invalid_client before general MsalException to emit clean actionable error message pointing to 'a365 setup requirements' instead of stack trace - Tests: add EnsurePublicClientFlowsEnabledAsync tests (enabled/disabled/patch-fails) and CreateCommand_ShouldHaveSkipRequirementsOption test Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
You can also share your feedback on Copilot code review. Take the survey.
src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/BlueprintSubcommand.cs
Outdated
Show resolved
Hide resolved
- MsalBrowserCredential: add shared in-memory MSAL token cache for Linux so all MsalBrowserCredential instances within one CLI invocation share the same token cache, eliminating repeated device code prompts during multi-step operations (e.g. blueprint creation + client secret creation) - BrowserHelper: remove exception object from LogWarning so xdg-open failures no longer emit a full stack trace in the output - MicrosoftGraphTokenProvider: detect missing PowerShell module error and log actionable guidance to run 'a365 setup requirements' - SetupHelpers: replace misleading "please sign in when prompted" message with one that mentions missing PS module as a common cause - PowerShellModulesRequirementCheck: auto-install missing modules via Install-Module -Scope CurrentUser -Force -AllowClobber when detected; returns Success if auto-install succeeds, Failure with manual steps if not Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ph PS auth setup blueprint was running only config checks (Location + ClientApp), missing PowerShellModulesRequirementCheck entirely. setup permissions mcp/bot/custom and setup permissions copilotstudio had no requirement checks at all. All four now gate on GetSystemRequirementChecks() before executing Graph operations, so missing Microsoft.Graph.Authentication is caught and auto-installed upfront rather than failing mid-run. Closes partial work toward #106.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.
You can also share your feedback on Copilot code review. Take the survey.
src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/BlueprintSubcommand.cs
Show resolved
Hide resolved
src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/PermissionsSubcommand.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/CopilotStudioSubcommand.cs
Outdated
Show resolved
Hide resolved
...65.DevTools.Cli/Services/Requirements/RequirementChecks/PowerShellModulesRequirementCheck.cs
Outdated
Show resolved
Hide resolved
Requirements checks are now skipped during dry runs to prevent unintended mutations. Updated `TryOpenUrl` documentation to clarify fallback behavior and logging when browser launch fails.
… Graph token acquisition - Skip system requirement checks (including auto-install) when --dry-run is set in PermissionsSubcommand (mcp/bot/custom) and CopilotStudioSubcommand to preserve non-mutating dry-run semantics - MicrosoftGraphTokenProvider now detects a missing/broken Microsoft.Graph module at runtime, auto-installs both required Graph modules, and retries token acquisition once before failing; error message clarifies auto-install was attempted if retry also fails - PowerShellModulesRequirementCheck.ExecutePowerShellCommandAsync returns stderr instead of null on failure so auto-install debug logs are actionable Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.
You can also share your feedback on Copilot code review. Take the survey.
src/Microsoft.Agents.A365.DevTools.Cli/Services/MsalBrowserCredential.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Agents.A365.DevTools.Cli/Helpers/BrowserHelper.cs
Outdated
Show resolved
Hide resolved
…s to reduce login prompts Microsoft.Graph.Authentication v2+ returns an opaque (non-JWT) token from $ctx.AccessToken that is rejected when used as a Bearer token. Switch to always extracting the token from the Authorization header of a live Invoke-MgGraphRequest call, which always contains the real JWT. Add AgentIdentityBlueprint.UpdateAuthProperties.All to RequiredPermissionGrantScopes so all PowerShell-based Graph operations (OAuth2 grants, service principal lookups, and inheritable permissions) use the same scope set. This ensures Connect-MgGraph authenticates once and the access token is reused from the in-process cache for all downstream calls, reducing device code prompts from 4 to 3 on first run (1 PS + 2 MSAL for different resources). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ng config
- MicrosoftGraphTokenProvider: extract last non-empty stdout line as token.
Connect-MgGraph in headless Linux/WSL falls back to device code and writes
the 'To sign in...' prompt to stdout, contaminating the full StandardOutput
string. Trimming the whole output returns a non-JWT string; taking only the
last line (where $token is always written) fixes the JWT validation warning
and the resulting SETUP_VALIDATION_FAILED for inheritable permissions.
- ConfigFileNotFoundException: new Agent365Exception subclass for missing
a365.config.json. ConfigService.LoadAsync now throws this instead of a raw
FileNotFoundException, so the global handler displays a clean actionable
error ('run a365 config init') instead of a full stack trace.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated 7 comments.
You can also share your feedback on Copilot code review. Take the survey.
src/Microsoft.Agents.A365.DevTools.Cli/Services/InteractiveGraphAuthService.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/PermissionsSubcommand.cs
Show resolved
Hide resolved
src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/CopilotStudioSubcommand.cs
Show resolved
Hide resolved
src/Microsoft.Agents.A365.DevTools.Cli/Services/Internal/MicrosoftGraphTokenProvider.cs
Show resolved
Hide resolved
...65.DevTools.Cli/Services/Requirements/RequirementChecks/PowerShellModulesRequirementCheck.cs
Show resolved
Hide resolved
...65.DevTools.Cli/Services/Requirements/RequirementChecks/PowerShellModulesRequirementCheck.cs
Show resolved
Hide resolved
…e test Both RemoveStaleCustomPermissionsAsync (PermissionsSubcommand) and DeployCommand used a hardcoded 2-scope array missing DelegatedPermissionGrant.ReadWrite.All. This produced a different sorted cache key than RequiredPermissionGrantScopes, causing a second Connect-MgGraph device-code prompt during 'setup blueprint' even though a token was already cached from the inheritable permissions step. Switch both sites to AuthenticationConstants.RequiredPermissionGrantScopes so all Graph token acquisitions share one cache entry per CLI invocation. Update Agent365ConfigServiceTests to expect ConfigFileNotFoundException instead of FileNotFoundException, matching the new exception type thrown by ConfigService. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MicrosoftGraphTokenProvider: replace misleading "Device Code: False" log with platform-specific guidance so users know what to expect: - Windows: "A browser window will open for authentication..." - Linux/macOS: "A device code prompt will appear below..." This avoids confusion when Connect-MgGraph auto-switches to device code in headless environments even though useDeviceCode=false was requested. PermissionsSubcommand: change "No custom blueprint permissions configured" to "No custom blueprint permissions specified in config. Skipping." to make clear this is about config content, not a system state or error condition. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 20 out of 20 changed files in this pull request and generated 1 comment.
You can also share your feedback on Copilot code review. Take the survey.
src/Microsoft.Agents.A365.DevTools.Cli/Services/InteractiveGraphAuthService.cs
Outdated
Show resolved
Hide resolved
LookupServicePrincipalByAppIdAsync can return null for 10-30s after blueprint creation due to Azure AD eventual consistency. Use the existing RetryHelper (already used for inheritable permissions verification) to retry up to 5 times with 5s base delay / exponential backoff before throwing the 'service principal may not have propagated' error. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: SP lookup and consent grant queries used az CLI token (scopes=null) which lacks Application.Read.All and DelegatedPermissionGrant.ReadWrite.All. Graph silently returned HTTP 200 with empty array, leaving blueprintSpId=null and causing polling to time out after 180s even after consent was granted. Changes: - BlueprintSubcommand: pass RequiredPermissionGrantScopes to SP lookups and CheckConsentExistsAsync; resolve blueprintSpId once at start of consent flow; use new MSAL PollAdminConsentAsync when SP ID is available - AdminConsentHelper: add MSAL PollAdminConsentAsync overload using GraphApiService; add scopes param to CheckConsentExistsAsync; add progress/timeout log messages - BlueprintLookupService: add optional scopes param to GetServicePrincipalByAppIdAsync - GraphApiService: add debug logging for failed GET; fix JsonDocument disposal - CommandExecutor: add optional outputTransform to ExecuteWithStreamingAsync - MicrosoftGraphTokenProvider: reformat PS device code output to MSAL box style - AuthenticationService: reduce token cache log noise (LogInfo -> LogDebug) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When PowerShell Connect-MgGraph fails (NullRef in DeviceCodeCredential on Linux, no TTY, module issues, or any other reason), fall back to MsalBrowserCredential to acquire the Graph token. On Windows this uses WAM; on Linux/macOS it uses device code with silent-first logic. The token is stored in MicrosoftGraphTokenProvider's in-process cache so subsequent calls (inheritable permissions, custom permissions) within the same CLI invocation reuse it without re-prompting. Also adds silent token acquisition attempt before device code in MsalBrowserCredential.AcquireTokenWithDeviceCodeFallbackAsync to reduce unnecessary device code prompts when a cached token exists. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…oc fixes - ConfigFileNotFoundException: derive from FileNotFoundException so existing catch sites (CleanupCommand, DeployCommand, etc.) continue to work - InteractiveGraphAuthService: remove unused Azure.Identity using; move credential factory resolution inside try/catch for consistent error wrapping - BrowserHelper: include exception object in LogWarning for structured logging - PowerShellModulesRequirementCheck: fix GenerateInstallationInstructions to produce valid PS syntax for multi-module Install-Module calls - PermissionsSubcommand / CopilotStudioSubcommand: replace Environment.Exit(1) with ExceptionHandler.ExitWithCleanup(1) to flush output before exit - MicrosoftGraphTokenProvider: dispose HttpResponseMessage after token extraction - MsalBrowserCredential: use platform-neutral path notation in cache doc comment Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 27 out of 28 changed files in this pull request and generated 5 comments.
You can also share your feedback on Copilot code review. Take the survey.
src/Microsoft.Agents.A365.DevTools.Cli/Services/MsalBrowserCredential.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Agents.A365.DevTools.Cli/Exceptions/ConfigFileNotFoundException.cs
Show resolved
Hide resolved
src/Microsoft.Agents.A365.DevTools.Cli/Services/Internal/MicrosoftGraphTokenProvider.cs
Show resolved
Hide resolved
src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/SetupHelpers.cs
Outdated
Show resolved
Hide resolved
- ConfigFileNotFoundException: add explicit using System.IO to avoid implicit global usings dependency - SetupHelpers: remove unnecessary ! null-forgiving operator on awaited task (it only asserts the Task itself is non-null, not the result) - BrowserHelper: use ArgumentList instead of Arguments for open/xdg-open to avoid argument parsing issues with special characters in URLs - MsalBrowserCredential: filter cached MSAL account by tenant ID before silent auth to avoid authenticating as wrong identity when multiple accounts are cached; fall through to device code if no tenant match - MicrosoftGraphTokenProvider: reuse IsPowerShellModuleMissingError in ProcessResult to eliminate duplicated string-matching logic Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 27 out of 28 changed files in this pull request and generated 7 comments.
You can also share your feedback on Copilot code review. Take the survey.
...65.DevTools.Cli/Services/Requirements/RequirementChecks/PowerShellModulesRequirementCheck.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Agents.A365.DevTools.Cli/Services/Internal/MicrosoftGraphTokenProvider.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Agents.A365.DevTools.Cli/Services/GraphApiService.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Agents.A365.DevTools.Cli/Services/BlueprintLookupService.cs
Show resolved
Hide resolved
src/Microsoft.Agents.A365.DevTools.Cli/Services/MsalBrowserCredential.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Agents.A365.DevTools.Cli/Services/InteractiveGraphAuthService.cs
Outdated
Show resolved
Hide resolved
- CHANGELOG.md: Keep a Changelog format covering [Unreleased], 1.1.0, 1.0.0 - Directory.Build.props: PackageReleaseNotes points to CHANGELOG.md (fixes NuGet warning) - src/DEVELOPER.md: update Release Process to describe workflow; add CHANGELOG to PR checklist - .github/copilot-instructions.md: add CHANGELOG reminder to Code Review Mindset - .claude/agents/pr-code-reviewer.md: add CHANGELOG check in Step 2 - .claude/agents/code-reviewer.md: add CHANGELOG check to Self-Verification - .claude/agents/code-review-manager.md: add CHANGELOG to Project Context standards Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ng, docs - MsalBrowserCredential.cs: StringComparison.Ordinal on Contains calls; comment clarifying same-tenant account edge case - InteractiveGraphAuthService.cs: StringComparison.Ordinal on all Contains calls in exception filters - PowerShellModulesRequirementCheck.cs: pin Install-Module to -Repository PSGallery - MicrosoftGraphTokenProvider.cs: pin both Install-Module calls to -Repository PSGallery - GraphApiService.cs: fix U+FFFD encoding artifact in comment - CommandExecutor.cs: document outputPrefix applies only to first line (outputTransform must not return multi-line) - BlueprintLookupService.cs: comment that DisplayName is not populated in this lookup path Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
gwharris7
left a comment
There was a problem hiding this comment.
was able to successfully run a365 setup blueprint from ubuntu
Fixes
a365 setup blueprintfailures on macOS and Linux where interactive browser authentication was unavailable.Changes
MsalBrowserCredentialPlatformNotSupportedExceptionand fall back to device code flow so macOS/Linux headless environments always get a working auth pathMicrosoftGraphTokenProviderConnect-MgGraphfails (non-TTY on Linux, null ref inDeviceCodeCredential, etc.); token is cached in-process so subsequent calls within the same run don't re-promptInteractiveGraphAuthServiceGraphServiceClientto surface auth failures at construction timecredentialFactoryfor testabilityAdminConsentHelper/BlueprintSubcommandRequiredPermissionGrantScopesto SP lookups so the correct token is used — fixesblueprintSpId = nulland unblocks consent detection (previously timed out at 180s)BrowserHelperTryOpenUrl(Windows/macOS/Linux); replaces duplicatedTryOpenBrowsermethods inBlueprintSubcommandandA365CreateInstanceRunnerTests
PR review feedback addressed — disposal, logging, exit handling, and minor correctness fixes across several files.
Closes #308