-
Notifications
You must be signed in to change notification settings - Fork 7
Fix Graph authentication for non-interactive terminals #158
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -108,7 +108,7 @@ public MicrosoftGraphTokenProvider( | |
| useDeviceCode); | ||
|
|
||
| var script = BuildPowerShellScript(tenantId, validatedScopes, useDeviceCode, clientAppId); | ||
| var result = await ExecuteWithFallbackAsync(script, ct); | ||
| var result = await ExecuteWithFallbackAsync(script, useDeviceCode, ct); | ||
| var token = ProcessResult(result); | ||
|
|
||
| if (string.IsNullOrWhiteSpace(token)) | ||
|
|
@@ -216,16 +216,17 @@ private static string BuildScopesArray(string[] scopes) | |
|
|
||
| private async Task<CommandResult> ExecuteWithFallbackAsync( | ||
| string script, | ||
| bool useDeviceCode, | ||
| CancellationToken ct) | ||
| { | ||
| // Try PowerShell Core first (cross-platform) | ||
| var result = await ExecutePowerShellAsync("pwsh", script, ct); | ||
| var result = await ExecutePowerShellAsync("pwsh", script, useDeviceCode, ct); | ||
|
|
||
| // Fallback to Windows PowerShell if pwsh is not available | ||
| if (!result.Success && IsPowerShellNotFoundError(result)) | ||
| { | ||
| _logger.LogDebug("PowerShell Core not found, falling back to Windows PowerShell"); | ||
| result = await ExecutePowerShellAsync("powershell", script, ct); | ||
| result = await ExecutePowerShellAsync("powershell", script, useDeviceCode, ct); | ||
| } | ||
|
|
||
| return result; | ||
|
|
@@ -234,9 +235,10 @@ private async Task<CommandResult> ExecuteWithFallbackAsync( | |
| private async Task<CommandResult> ExecutePowerShellAsync( | ||
| string shell, | ||
| string script, | ||
| bool useDeviceCode, | ||
| CancellationToken ct) | ||
| { | ||
| var arguments = BuildPowerShellArguments(shell, script); | ||
| var arguments = BuildPowerShellArguments(shell, script, useDeviceCode); | ||
|
|
||
| return await _executor.ExecuteWithStreamingAsync( | ||
| command: shell, | ||
|
|
@@ -247,11 +249,12 @@ private async Task<CommandResult> ExecutePowerShellAsync( | |
| cancellationToken: ct); | ||
| } | ||
|
|
||
| private static string BuildPowerShellArguments(string shell, string script) | ||
| private static string BuildPowerShellArguments(string shell, string script, bool useDeviceCode) | ||
| { | ||
| var baseArgs = shell == "pwsh" | ||
| ? "-NoProfile -NonInteractive" | ||
| : "-NoLogo -NoProfile -NonInteractive"; | ||
| // Never use -NonInteractive for Graph authentication as it prevents both: | ||
| // - Device code prompts from being displayed | ||
| // - Interactive browser windows from opening (WAM) | ||
| var baseArgs = shell == "pwsh" ? "-NoProfile" : "-NoLogo -NoProfile"; | ||
|
|
||
| var wrappedScript = $"try {{ {script} }} catch {{ Write-Error $_.Exception.Message; exit 1 }}"; | ||
|
|
||
|
|
@@ -268,14 +271,24 @@ private static string BuildPowerShellArguments(string shell, string script) | |
| return null; | ||
| } | ||
|
|
||
| var token = result.StandardOutput?.Trim(); | ||
| var output = result.StandardOutput?.Trim(); | ||
|
|
||
| if (string.IsNullOrWhiteSpace(token)) | ||
| if (string.IsNullOrWhiteSpace(output)) | ||
| { | ||
| _logger.LogWarning("PowerShell succeeded but returned empty output"); | ||
| return null; | ||
| } | ||
|
|
||
| // Extract the JWT token from output - PowerShell may include WARNING lines | ||
| // JWT tokens start with "eyJ" (base64 encoded '{"') | ||
| var token = ExtractJwtFromOutput(output); | ||
|
|
||
| if (string.IsNullOrWhiteSpace(token)) | ||
| { | ||
| _logger.LogWarning("Could not extract JWT token from PowerShell output"); | ||
| return null; | ||
| } | ||
|
|
||
| if (!IsValidJwtFormat(token)) | ||
| { | ||
| _logger.LogWarning("Returned token does not appear to be a valid JWT"); | ||
|
|
@@ -285,6 +298,42 @@ private static string BuildPowerShellArguments(string shell, string script) | |
| return token; | ||
| } | ||
|
|
||
| private static string? ExtractJwtFromOutput(string output) | ||
|
||
| { | ||
| // Split output into lines and find the JWT token | ||
| // JWT tokens start with "eyJ" and contain exactly two dots | ||
| var lines = output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); | ||
|
|
||
| foreach (var line in lines) | ||
| { | ||
| var trimmed = line.Trim(); | ||
| // Skip WARNING, ERROR, and other PowerShell output lines | ||
| if (trimmed.StartsWith("WARNING:", StringComparison.OrdinalIgnoreCase) || | ||
| trimmed.StartsWith("ERROR:", StringComparison.OrdinalIgnoreCase) || | ||
| trimmed.StartsWith("VERBOSE:", StringComparison.OrdinalIgnoreCase) || | ||
| trimmed.StartsWith("DEBUG:", StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| // Check if this line looks like a JWT token | ||
| if (IsValidJwtFormat(trimmed)) | ||
| { | ||
| return trimmed; | ||
| } | ||
| } | ||
|
Comment on lines
+307
to
+324
|
||
|
|
||
| // Fallback: if no line matches, return the trimmed output | ||
| // (in case the token is on a single line without prefixes) | ||
| var trimmedOutput = output.Trim(); | ||
| if (IsValidJwtFormat(trimmedOutput)) | ||
| { | ||
| return trimmedOutput; | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| private static bool IsPowerShellNotFoundError(CommandResult result) | ||
| { | ||
| if (string.IsNullOrWhiteSpace(result.StandardError)) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
useDeviceCodeparameter is added to the method signature but is never used within the method body. This creates confusion about the parameter's purpose and suggests incomplete implementation. Either use the parameter or remove it from the signature if it's not needed.