diff --git a/CHANGELOG.md b/CHANGELOG.md index 6674f75..d1aa410 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/RequiredModules.psd1 b/RequiredModules.psd1 index 8a17246..e610f6c 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' diff --git a/src/Public/Connect-FabricAccount.ps1 b/src/Public/Connect-FabricAccount.ps1 index 21ee28b..2e3491b 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 a97dfa0..14bb36c 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 + } + } }