Fix interactive authentication for PowerShell Core 7+ by using system browser
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
IRuntimeUtildependency injection toInteractiveAuthenticationImplfor consistent PowerShell edition detection - Implemented
IsPowerShellCore()method usingRuntimeUtil.Platform.Equals("Core") - Modified MSAL token builder to conditionally apply
.WithUseEmbeddedWebView(false)for PowerShell Core
Documentation Update
- Updated help text in
CommonHelpText.psd1to 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
NewCredentialclass
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.
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
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
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
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
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
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.