TfsCmdlets icon indicating copy to clipboard operation
TfsCmdlets copied to clipboard

Fix interactive authentication for PowerShell Core 7+ by using system browser

Open Copilot opened this issue 9 months ago • 6 comments

Problem

When using interactive authentication (Connect-TfsTeamProjectCollection -Interactive) in PowerShell Core 7+, users encounter the following error:

Connect-TfsTeamProjectCollection: A window handle must be configured. See https://aka.ms/msal-net-wam#parent-window-handles

This prevents users from authenticating interactively in PowerShell Core environments, forcing them to use alternative authentication methods like Personal Access Tokens.

Root Cause

MSAL (Microsoft Authentication Library) by default attempts to create an embedded web view for interactive authentication, which requires a proper window handle. In PowerShell Core, especially in console environments or on non-Windows platforms, no suitable window handle is available, causing the authentication to fail.

Solution

This PR implements PowerShell edition detection and configures MSAL appropriately for each environment:

  • PowerShell Core: Uses system browser authentication (.WithUseEmbeddedWebView(false))
  • Windows PowerShell: Maintains existing embedded web view behavior

Technical Changes

Core Implementation

  • Added IRuntimeUtil dependency injection to InteractiveAuthenticationImpl for consistent PowerShell edition detection
  • Implemented IsPowerShellCore() method using RuntimeUtil.Platform.Equals("Core")
  • Modified MSAL token builder to conditionally apply .WithUseEmbeddedWebView(false) for PowerShell Core

Documentation Update

  • Updated help text in CommonHelpText.psd1 to reflect that interactive authentication now supports both Windows PowerShell and PowerShell Core

Code Example

// For PowerShell Core, use system browser instead of embedded web view
// to avoid window handle issues
if (IsPowerShellCore())
{
    tokenBuilder = tokenBuilder.WithUseEmbeddedWebView(false);
}

Impact

  • Cross-platform support: Interactive authentication now works on Windows, Linux, and macOS with PowerShell Core
  • Backward compatibility: Zero impact on existing Windows PowerShell workflows
  • User experience: PowerShell Core users can now use interactive authentication instead of being forced to use PATs
  • Consistency: Uses the same PowerShell detection logic as existing NewCredential class

Testing

  • Validated PowerShell edition detection logic in PowerShell Core 7.4.10
  • Confirmed the approach aligns with Microsoft's MSAL recommendations for console applications
  • Verified minimal scope of changes (29 additions, 4 deletions in main implementation)

The fix enables MSAL to open the system browser for authentication instead of attempting to create an embedded web view that requires window handles unavailable in PowerShell Core environments.

Fixes #249.


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot avatar Jun 17 '25 21:06 Copilot

P:\Repos\Gh\igoravl\TfsCmdlets> ctfs cloudmotion-projetos -Interactive Connect-TfsTeamProjectCollection: Object reference not set to an instance of an object.

[Not connected] P:\Repos\Gh\igoravl\TfsCmdlets> ctfs cloudmotion-projetos -Verbose -Interactive VERBOSE: [19:17:42.3990] [Connect-TfsTeamProjectCollection] BeginProcessing VERBOSE: [19:17:42.4010] [Connect-TfsTeamProjectCollection] ProcessRecord VERBOSE: [19:17:42.4057] [Connect-TfsTeamProjectCollection] Running [Connect-TfsTeamProjectCollection] with parameter set 'Prompt for credential' and the following implicit and explicit arguments:

"Collection" = "cloudmotion-projetos", "Passthru" = false, "Cached" = false, "Interactive" = true

VERBOSE: [19:17:42.4061] [Connect-TfsTeamProjectCollection] Running [Connect-TfsTeamProjectCollection] with parameter set 'Prompt for credential' and the following implicit and explicit arguments:

"Collection" = "cloudmotion-projetos", "Passthru" = false, "Cached" = false, "Interactive" = true

VERBOSE: [19:17:42.4067] [Connect-TfsTeamProjectCollection] Get currently connected Server VERBOSE: [19:17:42.4074] [Connect-TfsTeamProjectCollection] Get Collection referenced by URL 'https://dev.azure.com/cloudmotion-projetos' VERBOSE: [19:17:42.4080] [Connect-TfsTeamProjectCollection] Running [Get-TfsCredential] with parameter set 'Prompt for credential' and the following implicit and explicit arguments:

"Collection" = "cloudmotion-projetos", "Passthru" = false, "Cached" = false, "Interactive" = true, "Url" = "https://dev.azure.com/cloudmotion-projetos"

VERBOSE: [19:17:42.4083] [Connect-TfsTeamProjectCollection] Using interactive credential VERBOSE: [19:17:42.4607] [Connect-TfsTeamProjectCollection] Detailed Error: { "ClassName": "System.NullReferenceException", "Message": "Object reference not set to an instance of an object.", "Data": null, "InnerException": null, "HelpURL": null, "StackTraceString": " at TfsCmdlets.Services.Impl.InteractiveAuthenticationImpl.IsPowerShellCore() in P:\Repos\Gh\igoravl\TfsCmdlets\CSharp\TfsCmdlets\Services\Impl\InteractiveAuthenticationImpl.cs:line 29\r\n at TfsCmdlets.Services.Impl.InteractiveAuthenticationImpl.SignInUserAndGetTokenUsingMSAL(String[] scopes) in P:\Repos\Gh\igoravl\TfsCmdlets\CSharp\TfsCmdlets\Services\Impl\InteractiveAuthenticationImpl.cs:line 79\r\n at TfsCmdlets.Services.Impl.InteractiveAuthenticationImpl.GetToken(Uri uri) in P:\Repos\Gh\igoravl\TfsCmdlets\CSharp\TfsCmdlets\Services\Impl\InteractiveAuthenticationImpl.cs:line 34\r\n at TfsCmdlets.Cmdlets.Credential.GetCredentialController.Run()+MoveNext() in P:\Repos\Gh\igoravl\TfsCmdlets\CSharp\TfsCmdlets\Cmdlets\Credential\NewCredential.cs:line 119\r\n at TfsCmdlets.Services.Impl.DataManagerImpl.EnumerableWrapper.MoveNext() in P:\Repos\Gh\igoravl\TfsCmdlets\CSharp\TfsCmdlets\Services\Impl\DataManagerImpl.cs:line 301\r\n at TfsCmdlets.Services.Impl.DataManagerImpl.Invoke[T](String verb, Object overridingParameters)+MoveNext() in P:\Repos\Gh\igoravl\TfsCmdlets\CSharp\TfsCmdlets\Services\Impl\DataManagerImpl.cs:line 31\r\n at System.Linq.Enumerable.TryGetFirstNonIterator[TSource](IEnumerable1 source, Boolean& found)\r\n at System.Linq.Enumerable.FirstOrDefault[TSource](IEnumerable1 source)\r\n at TfsCmdlets.Services.Impl.DataManagerImpl.GetItem[T](Object overridingParameters) in P:\Repos\Gh\igoravl\TfsCmdlets\CSharp\TfsCmdlets\Services\Impl\DataManagerImpl.cs:line 54\r\n at TfsCmdlets.Services.Impl.DataManagerImpl.CreateConnection(ClientScope scope, Object overridingParameters) in P:\Repos\Gh\igoravl\TfsCmdlets\CSharp\TfsCmdlets\Services\Impl\DataManagerImpl.cs:line 207\r\n at TfsCmdlets.Services.Impl.DataManagerImpl.GetCollection(Object overridingParameters) in P:\Repos\Gh\igoravl\TfsCmdlets\CSharp\TfsCmdlets\Services\Impl\DataManagerImpl.cs:line 106\r\n at TfsCmdlets.Cmdlets.TeamProjectCollection.ConnectTeamProjectCollectionController.Run()+MoveNext() in P:\Repos\Gh\igoravl\TfsCmdlets\CSharp\TfsCmdlets\Cmdlets\TeamProjectCollection\ConnectTeamProjectCollection.cs:line 62\r\n at TfsCmdlets.Cmdlets.CmdletBase.DoProcessRecord() in P:\Repos\Gh\igoravl\TfsCmdlets\CSharp\TfsCmdlets\Cmdlets\CmdletBase.cs:line 108", "RemoteStackTraceString": null, "RemoteStackIndex": 0, "ExceptionMethod": null, "HResult": -2147467261, "Source": "TfsCmdlets", "WatsonBuckets": null }) Connect-TfsTeamProjectCollection: Object reference not set to an instance of an object. VERBOSE: [19:17:42.4723] [Connect-TfsTeamProjectCollection] EndProcessing

igoravl avatar Jun 17 '25 22:06 igoravl

P:\Repos\Gh\igoravl\TfsCmdlets> ctfs cloudmotion-projetos -Interactive Connect-TfsTeamProjectCollection: Object reference not set to an instance of an object.

...

Fixed the null reference exception by switching from property-based to constructor-based dependency injection for IRuntimeUtil. The issue was that RuntimeUtil wasn't being properly initialized with the [Import] attribute pattern. Changed to use [ImportingConstructor] like other service implementations in the codebase.

Commit: 9516bd9

Copilot avatar Jun 17 '25 22:06 Copilot

P:\Repos\Gh\igoravl\TfsCmdlets> ctfs cloudmotion-projetos -Verbose -Interactive VERBOSE: [19:31:04.8545] [Connect-TfsTeamProjectCollection] BeginProcessing VERBOSE: [19:31:04.8570] [Connect-TfsTeamProjectCollection] ProcessRecord VERBOSE: [19:31:04.8759] [Connect-TfsTeamProjectCollection] Running [Connect-TfsTeamProjectCollection] with parameter set 'Prompt for credential' and the following implicit and explicit arguments:

"Collection" = "cloudmotion-projetos", "Passthru" = false, "Cached" = false, "Interactive" = true

VERBOSE: [19:31:04.8778] [Connect-TfsTeamProjectCollection] Running [Connect-TfsTeamProjectCollection] with parameter set 'Prompt for credential' and the following implicit and explicit arguments:

"Collection" = "cloudmotion-projetos", "Passthru" = false, "Cached" = false, "Interactive" = true

VERBOSE: [19:31:04.8845] [Connect-TfsTeamProjectCollection] Get currently connected Server VERBOSE: [19:31:04.8853] [Connect-TfsTeamProjectCollection] Get Collection referenced by URL 'https://dev.azure.com/cloudmotion-projetos' VERBOSE: [19:31:04.8949] [Connect-TfsTeamProjectCollection] Running [Get-TfsCredential] with parameter set 'Prompt for credential' and the following implicit and explicit arguments:

"Collection" = "cloudmotion-projetos", "Passthru" = false, "Cached" = false, "Interactive" = true, "Url" = "https://dev.azure.com/cloudmotion-projetos"

VERBOSE: [19:31:04.9042] [Connect-TfsTeamProjectCollection] Using interactive credential VERBOSE: [19:31:05.8926] [Connect-TfsTeamProjectCollection] Detailed Error: { "IsRetryable": false, "ErrorCode": "window_handle_required", "CorrelationId": "219e5e78-7b8f-435a-b454-b7d06d9325cc", "AdditionalExceptionData": {}, "Message": "A window handle must be configured. See https://aka.ms/msal-net-wam#parent-window-handles", "Data": {}, "InnerException": null, "HelpLink": null, "Source": "Microsoft.Identity.Client.Broker", "HResult": -2146233088, "StackTrace": " at Microsoft.Identity.Client.Platforms.Features.RuntimeBroker.RuntimeBroker.AcquireTokenInteractiveAsync(AuthenticationRequestParameters authenticationRequestParameters, AcquireTokenInteractiveParameters acquireTokenInteractiveParameters)\r\n at Microsoft.Identity.Client.Internal.Broker.BrokerInteractiveRequestComponent.FetchTokensAsync(CancellationToken cancellationToken)\r\n at Microsoft.Identity.Client.Internal.Requests.InteractiveRequest.FetchTokensFromBrokerAsync(String brokerInstallUrl, CancellationToken cancellationToken)\r\n at Microsoft.Identity.Client.Internal.Requests.InteractiveRequest.GetTokenResponseAsync(CancellationToken cancellationToken)\r\n at Microsoft.Identity.Client.Internal.Requests.InteractiveRequest.ExecuteAsync(CancellationToken cancellationToken)\r\n at Microsoft.Identity.Client.Internal.Requests.RequestBase.<>c__DisplayClass11_1.<<RunAsync>b__1>d.MoveNext()\r\n--- End of stack trace from previous location ---\r\n at Microsoft.Identity.Client.Utils.StopwatchService.MeasureCodeBlockAsync(Func1 codeBlock)\r\n at Microsoft.Identity.Client.Internal.Requests.RequestBase.RunAsync(CancellationToken cancellationToken)\r\n at Microsoft.Identity.Client.ApiConfig.Executors.PublicClientExecutor.ExecuteAsync(AcquireTokenCommonParameters commonParameters, AcquireTokenInteractiveParameters interactiveParameters, CancellationToken cancellationToken)\r\n at TfsCmdlets.Services.Impl.InteractiveAuthenticationImpl.SignInUserAndGetTokenUsingMSAL(String[] scopes) in P:\\Repos\\Gh\\igoravl\\TfsCmdlets\\CSharp\\TfsCmdlets\\Services\\Impl\\InteractiveAuthenticationImpl.cs:line 89\r\n at TfsCmdlets.Services.Impl.InteractiveAuthenticationImpl.GetToken(Uri uri) in P:\\Repos\\Gh\\igoravl\\TfsCmdlets\\CSharp\\TfsCmdlets\\Services\\Impl\\InteractiveAuthenticationImpl.cs:line 39\r\n at TfsCmdlets.Cmdlets.Credential.GetCredentialController.Run()+MoveNext() in P:\\Repos\\Gh\\igoravl\\TfsCmdlets\\CSharp\\TfsCmdlets\\Cmdlets\\Credential\\NewCredential.cs:line 119\r\n at TfsCmdlets.Services.Impl.DataManagerImpl.EnumerableWrapper.MoveNext() in P:\\Repos\\Gh\\igoravl\\TfsCmdlets\\CSharp\\TfsCmdlets\\Services\\Impl\\DataManagerImpl.cs:line 301\r\n at TfsCmdlets.Services.Impl.DataManagerImpl.Invoke[T](String verb, Object overridingParameters)+MoveNext() in P:\\Repos\\Gh\\igoravl\\TfsCmdlets\\CSharp\\TfsCmdlets\\Services\\Impl\\DataManagerImpl.cs:line 31\r\n at System.Linq.Enumerable.TryGetFirstNonIterator[TSource](IEnumerable1 source, Boolean& found)\r\n at System.Linq.Enumerable.FirstOrDefault[TSource](IEnumerable`1 source)\r\n at TfsCmdlets.Services.Impl.DataManagerImpl.GetItem[T](Object overridingParameters) in P:\Repos\Gh\igoravl\TfsCmdlets\CSharp\TfsCmdlets\Services\Impl\DataManagerImpl.cs:line 54\r\n at TfsCmdlets.Services.Impl.DataManagerImpl.CreateConnection(ClientScope scope, Object overridingParameters) in P:\Repos\Gh\igoravl\TfsCmdlets\CSharp\TfsCmdlets\Services\Impl\DataManagerImpl.cs:line 207\r\n at TfsCmdlets.Services.Impl.DataManagerImpl.GetCollection(Object overridingParameters) in P:\Repos\Gh\igoravl\TfsCmdlets\CSharp\TfsCmdlets\Services\Impl\DataManagerImpl.cs:line 106\r\n at TfsCmdlets.Cmdlets.TeamProjectCollection.ConnectTeamProjectCollectionController.Run()+MoveNext() in P:\Repos\Gh\igoravl\TfsCmdlets\CSharp\TfsCmdlets\Cmdlets\TeamProjectCollection\ConnectTeamProjectCollection.cs:line 62\r\n at TfsCmdlets.Cmdlets.CmdletBase.DoProcessRecord() in P:\Repos\Gh\igoravl\TfsCmdlets\CSharp\TfsCmdlets\Cmdlets\CmdletBase.cs:line 108" }) Connect-TfsTeamProjectCollection: A window handle must be configured. See https://aka.ms/msal-net-wam#parent-window-handles VERBOSE: [19:31:06.0213] [Connect-TfsTeamProjectCollection] EndProcessing

igoravl avatar Jun 17 '25 22:06 igoravl

Resume investigation on the original error. While on it, check also the following warning in the IDE:

'DesktopExtensions.WithDesktopFeatures(PublicClientApplicationBuilder)' is obsolete: 'Use WithWindowsDesktopFeatures instead. For broker support only, use WithBroker(BrokerOptions) from Microsoft.Identity.Client.Broker package.'CS0618

igoravl avatar Jun 17 '25 22:06 igoravl

Resume investigation on the original error. While on it, check also the following warning in the IDE:

'DesktopExtensions.WithDesktopFeatures(PublicClientApplicationBuilder)' is obsolete: 'Use WithW...

Fixed the PowerShell Core detection issue by switching from RuntimeUtil.Platform to PowerShell.Edition - now uses the same detection logic as the existing NewCredential class. The deprecation warning for WithDesktopFeatures remains as WithWindowsDesktopFeatures requires additional BrokerOptions configuration that would need broader architectural changes.

Commit: 6c0f5cd

Copilot avatar Jun 17 '25 22:06 Copilot

Test Results

  2 files   80 suites   44s ⏱️ 139 tests 139 ✅ 0 💤 0 ❌ 278 runs  278 ✅ 0 💤 0 ❌

Results for commit 0e0ab92d.

:recycle: This comment has been updated with latest results.

github-actions[bot] avatar Jun 18 '25 23:06 github-actions[bot]