From 7c1960b7771d354078232ab23f114f29e6e359d2 Mon Sep 17 00:00:00 2001 From: "Patrick B." Date: Sun, 23 Aug 2026 11:13:16 +0200 Subject: [PATCH 1/2] Add device-code fallback to Connect-FabricAccount When interactive/broker (WAM) sign-in fails, e.g. because a terminal host (Warp on Windows was the reported case) can't provide WAM a window handle to parent its account picker to, Connect-AzAccount can throw instead of showing any UI, causing Connect-FabricAccount to fail silently. Wrap the interactive sign-in call in try/catch and retry with Connect-AzAccount -UseDeviceAuthentication on failure, so users get a usable device-code prompt instead of a hard failure. Also add a -UseDeviceAuthentication switch for explicit opt-in. Thanks for taking a look! Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 4 ++ src/Public/Connect-FabricAccount.ps1 | 41 ++++++++++++-- tests/Unit/Connect-FabricAccount.Tests.ps1 | 62 ++++++++++++++++++++++ 3 files changed, 103 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6674f750..d1aa4101 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added `-UseDeviceAuthentication` switch to `Connect-FabricAccount` to force device code authentication instead of the interactive broker/browser flow + ### Changed ### Fixed +- `Connect-FabricAccount` now automatically falls back to device code authentication when interactive/broker (WAM) sign-in fails, e.g. in terminal hosts such as Warp on Windows where the account picker cannot render + ### Deprecated ### Removed diff --git a/src/Public/Connect-FabricAccount.ps1 b/src/Public/Connect-FabricAccount.ps1 index 21ee28b1..2e3491b6 100644 --- a/src/Public/Connect-FabricAccount.ps1 +++ b/src/Public/Connect-FabricAccount.ps1 @@ -22,6 +22,12 @@ function Connect-FabricAccount { .PARAMETER Reset A switch parameter. If provided, the function resets the Fabric authentication token. +.PARAMETER UseDeviceAuthentication + A switch parameter. Forces device code authentication instead of the interactive broker/browser flow. + Use this when the interactive account picker cannot be displayed by the host terminal (for example Warp on Windows, + which does not expose a window handle the WAM broker can parent its UI to). When this switch is not specified, + Connect-FabricAccount still falls back to device code authentication automatically if interactive sign-in fails. + .EXAMPLE Connects to the stated Tenant with existing credentials @@ -49,6 +55,13 @@ function Connect-FabricAccount { Connect-FabricAccount -TenantId $TenantID -ServicePrincipalId $ServicePrincipalId -ServicePrincipalSecret $ServicePrincipalSecretSecure -Reset ``` +.EXAMPLE + Connects using device code authentication (useful in terminals such as Warp where the account picker cannot render) + + ```powershell + Connect-FabricAccount -UseDeviceAuthentication + ``` + .EXAMPLE Connects as Service Principal using credential object @@ -72,6 +85,7 @@ function Connect-FabricAccount { - 2024-12-22 - FGE: Added Verbose Output - 2025-05-26 - Jojobit: Added Service Principal support, with secure string handling and parameter descriptions, as supported by the original FabTools module - 2025-06-02 - KNO: Added Reset switch to force re-authentication and token refresh + - 2026-08-23 - PBO: Added UseDeviceAuthentication switch and automatic fallback to device code auth when interactive/broker sign-in fails (e.g. Warp terminal) Author: Frank Geisler, Kamil Nowinski @@ -96,7 +110,10 @@ function Connect-FabricAccount { [PSCredential] $Credential, [Parameter(Mandatory = $false, HelpMessage = "Refresh current session.")] - [switch] $Reset + [switch] $Reset, + + [Parameter(Mandatory = $false, HelpMessage = "Use device code authentication instead of the interactive broker/browser flow.")] + [switch] $UseDeviceAuthentication ) begin { @@ -135,13 +152,29 @@ function Connect-FabricAccount { } else { Write-Message "Connecting to Azure Account using current user..." -Level Verbose + $connectAzAccountParams = @{} if ($TenantId) { - $null = Connect-AzAccount -Tenant $TenantId + $connectAzAccountParams['Tenant'] = $TenantId } else { - # If no TenantId is provided, connect to the default tenant Write-Message "No TenantId provided, connecting to default tenant..." -Level Verbose - $null = Connect-AzAccount + } + + if ($UseDeviceAuthentication) { + Write-Message "Using device code authentication as requested..." -Level Verbose + $null = Connect-AzAccount @connectAzAccountParams -UseDeviceAuthentication + } + else { + try { + $null = Connect-AzAccount @connectAzAccountParams -ErrorAction Stop + } + catch { + # Interactive/broker (WAM) sign-in can silently fail to render its account picker in some + # terminal hosts (e.g. Warp on Windows does not expose a window handle for WAM to parent + # its UI to). Fall back to device code authentication instead of failing outright. + Write-Message "Interactive sign-in failed, possibly because this terminal does not support the account picker (e.g. Warp): $($_.Exception.Message). Falling back to device code authentication..." -Level Warning + $null = Connect-AzAccount @connectAzAccountParams -UseDeviceAuthentication + } } } $azContext = Get-AzContext diff --git a/tests/Unit/Connect-FabricAccount.Tests.ps1 b/tests/Unit/Connect-FabricAccount.Tests.ps1 index a97dfa0f..14bb36c2 100644 --- a/tests/Unit/Connect-FabricAccount.Tests.ps1 +++ b/tests/Unit/Connect-FabricAccount.Tests.ps1 @@ -22,6 +22,7 @@ Describe "Connect-FabricAccount" -Tag "UnitTests" { @{ ExpectedParameterName = 'ServicePrincipalSecret'; ExpectedParameterType = 'securestring'; Mandatory = 'False' } @{ ExpectedParameterName = 'Credential'; ExpectedParameterType = 'pscredential'; Mandatory = 'False' } @{ ExpectedParameterName = 'Reset'; ExpectedParameterType = 'switch'; Mandatory = 'False' } + @{ ExpectedParameterName = 'UseDeviceAuthentication'; ExpectedParameterType = 'switch'; Mandatory = 'False' } ) { $Command | Should -HaveParameter -ParameterName $ExpectedParameterName -Type $ExpectedParameterType -Mandatory:([bool]::Parse($Mandatory)) } @@ -31,4 +32,65 @@ Describe "Connect-FabricAccount" -Tag "UnitTests" { $Command.Parameters.ContainsKey('Confirm') | Should -BeTrue } } + + Context "Device code authentication fallback" { + + BeforeEach { + Mock -CommandName Get-AzContext -MockWith { $null } + Mock -CommandName Write-Message -MockWith { } + Mock -CommandName Get-PSFConfigValue -MockWith { 'https://api.fabric.microsoft.com' } + Mock -CommandName Set-PSFConfig -MockWith { } + Mock -CommandName Get-AzAccessToken -MockWith { + [pscustomobject]@{ + Token = (ConvertTo-SecureString -String 'fake-token' -AsPlainText -Force) + ExpiresOn = (Get-Date).AddHours(1) + TenantId = [guid]::NewGuid() + } + } + } + + It 'Should use interactive Connect-AzAccount when broker sign-in succeeds' { + $script:connectCallLog = [System.Collections.Generic.List[bool]]::new() + Mock -CommandName Connect-AzAccount -MockWith { + $script:connectCallLog.Add([bool]$UseDeviceAuthentication) + [pscustomobject]@{ Context = [pscustomobject]@{ Account = 'user@contoso.com'; Tenant = [pscustomobject]@{ Id = [guid]::NewGuid() } } } + } + + Connect-FabricAccount -Confirm:$false + + $script:connectCallLog.Count | Should -Be 1 + $script:connectCallLog[0] | Should -BeFalse + } + + It 'Should fall back to device code authentication when interactive Connect-AzAccount fails' { + $script:connectCallLog = [System.Collections.Generic.List[bool]]::new() + Mock -CommandName Connect-AzAccount -MockWith { + $script:connectCallLog.Add([bool]$UseDeviceAuthentication) + if (-not $UseDeviceAuthentication) { + throw 'No account picker UI could be shown.' + } + [pscustomobject]@{ Context = [pscustomobject]@{ Account = 'user@contoso.com'; Tenant = [pscustomobject]@{ Id = [guid]::NewGuid() } } } + } + + { Connect-FabricAccount -Confirm:$false } | Should -Not -Throw + + $script:connectCallLog.Count | Should -Be 2 + $script:connectCallLog[0] | Should -BeFalse # first attempt: interactive, fails + $script:connectCallLog[1] | Should -BeTrue # second attempt: device code fallback, succeeds + Should -Invoke -CommandName Write-Message -ParameterFilter { $Level -eq 'Warning' -and $Message -like '*Falling back to device code authentication*' } + } + + It 'Should go straight to device code authentication when -UseDeviceAuthentication is specified' { + $script:connectCallLog = [System.Collections.Generic.List[bool]]::new() + Mock -CommandName Connect-AzAccount -MockWith { + $script:connectCallLog.Add([bool]$UseDeviceAuthentication) + [pscustomobject]@{ Context = [pscustomobject]@{ Account = 'user@contoso.com'; Tenant = [pscustomobject]@{ Id = [guid]::NewGuid() } } } + } + + Connect-FabricAccount -UseDeviceAuthentication -Confirm:$false + + $script:connectCallLog.Count | Should -Be 1 + $script:connectCallLog[0] | Should -BeTrue + } + } } From 0f913f35ffbce271fc7a2675c4abd4d8662cd084 Mon Sep 17 00:00:00 2001 From: Patrick Bossier Date: Fri, 11 Sep 2026 00:26:13 +0200 Subject: [PATCH 2/2] Pin Pester to 5.9.1 'latest' now resolves to Pester 6, which fails the existing test suite: empty -ForEach is an error, -HaveParameter -Mandatory:$false asserts non-mandatory, and parameter-filtered mocks no longer fall back to the real command. Pin to the last 5.x release until the tests are migrated to Pester 6. Co-Authored-By: Claude Opus 5 --- RequiredModules.psd1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RequiredModules.psd1 b/RequiredModules.psd1 index 8a17246e..e610f6ce 100644 --- a/RequiredModules.psd1 +++ b/RequiredModules.psd1 @@ -14,7 +14,7 @@ Assert = "0.9.6" InvokeBuild = 'latest' PSScriptAnalyzer = '1.24.0' - Pester = 'latest' + Pester = '5.9.1' ModuleBuilder = 'latest' ChangelogManagement = 'latest' Sampler = 'latest'