internal/functions/Get-BapAccessToken.ps1
|
<# .SYNOPSIS Centralized Azure access token minter for d365bap.tools. .DESCRIPTION Mints an Azure access token for the requested resource via the configured auth provider and returns the .Token equivalent (a SecureString), so existing call sites can keep their ConvertFrom-SecureString -AsPlainText pattern. A failed mint stops execution entirely with a terminating error: running any code without a token makes no sense, so there is no soft-fail path. Tokens are cached per resource + tenant + account in $script:BapTokenCache and reused while more than 15 minutes of lifetime remain. The current Azure context is re-read on every call: a manual context switch outside Switch-BapTenant changes the cache key / stored stamp, so the next call treats the entry as a miss and mints a fresh token. #> if ($null -eq $script:BapTokenCache) { $script:BapTokenCache = @{} } <# .SYNOPSIS Mints (or returns a cached) Azure access token as a SecureString. .DESCRIPTION Returns the .Token equivalent (a SecureString) for the requested resource. Uses Get-AzAccessToken when the auth provider is 'AzAccount' and the Azure CLI ('az account get-access-token') when the provider is 'AzCli'. .PARAMETER ResourceUrl The resource (audience) to mint the token for, e.g. "https://graph.microsoft.com/". .PARAMETER TenantId Optional tenant to mint the token for. Passed through to the active auth provider. .EXAMPLE PS C:\> $secureToken = Get-BapAccessToken -ResourceUrl 'https://graph.microsoft.com/' PS C:\> $tokenValue = ConvertFrom-SecureString -AsPlainText -SecureString $secureToken Mints (or reuses a cached) token for Microsoft Graph and converts it to plain text at the call site. .NOTES Author: Motz Jensen (@Splaxi) #> function Get-BapAccessToken { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "", Justification = "The Azure CLI returns the access token as plain text; it is converted to a SecureString immediately and never stored.")] [CmdletBinding()] [OutputType([System.Security.SecureString])] param ( [Parameter(Mandatory = $true)] [string] $ResourceUrl, [string] $TenantId ) if ($null -eq $script:BapTokenCache) { $script:BapTokenCache = @{} } $provider = Get-PSFConfigValue -FullName 'd365bap.tools.auth.provider' -Fallback 'AzAccount' if ([string]::IsNullOrWhiteSpace("$provider")) { $provider = 'AzAccount' } #region Resolve current context stamp (account + tenant) $currentAccountId = '' $currentTenantId = '' if ("$provider" -eq 'AzAccount') { try { $azContext = Get-AzContext -ErrorAction Stop if ($null -ne $azContext) { $currentAccountId = "$($azContext.Account.Id)" $currentTenantId = "$($azContext.Tenant.Id)" } } catch { Write-PSFMessage -Level Verbose -Message "Unable to read the current Az context. Continuing without a context stamp." } } elseif ("$provider" -eq 'AzCli') { try { $accountJson = az account show --query '{user:user.name, tenant:tenantId}' -o json 2>$null if ($LASTEXITCODE -eq 0 -and -not [string]::IsNullOrWhiteSpace("$accountJson")) { $accountObj = "$accountJson" | ConvertFrom-Json -ErrorAction Stop $currentAccountId = "$($accountObj.user)" $currentTenantId = "$($accountObj.tenant)" } } catch { Write-PSFMessage -Level Verbose -Message "Unable to read the current Azure CLI account. Continuing without a context stamp." } } else { $messageString = "Unknown auth provider '<c='em'>$provider</c>'. Expected 'AzAccount' or 'AzCli' in configuration 'd365bap.tools.auth.provider'." Write-PSFMessage -Level Critical -Message $messageString throw $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) } #endregion Resolve current context stamp if (-not [string]::IsNullOrWhiteSpace($TenantId)) { $effectiveTenantId = $TenantId } else { $effectiveTenantId = $currentTenantId } $cacheKey = "$($ResourceUrl.ToLowerInvariant())|$("$effectiveTenantId".ToLowerInvariant())|$("$currentAccountId".ToLowerInvariant())" #region Serve from cache $cached = $script:BapTokenCache[$cacheKey] if ($null -ne $cached -and $null -ne $cached.Token -and $null -ne $cached.ExpiresOn) { if ("$($cached.AccountId)" -eq "$currentAccountId" -and "$($cached.TenantId)" -eq "$effectiveTenantId") { $remainingMinutes = -1 try { $remainingMinutes = (([datetime]$cached.ExpiresOn).ToUniversalTime() - [datetime]::UtcNow).TotalMinutes } catch { $remainingMinutes = -1 } if ($remainingMinutes -gt 15) { Write-PSFMessage -Level Verbose -Message "Returning cached access token for resource <c='em'>$ResourceUrl</c>." return $cached.Token } Write-PSFMessage -Level Verbose -Message "Cached access token for resource <c='em'>$ResourceUrl</c> expires soon. Minting a new token." } else { Write-PSFMessage -Level Verbose -Message "Azure context changed since the token was cached. Minting a new token for resource <c='em'>$ResourceUrl</c>." } } #endregion Serve from cache #region Mint new token $secureToken = $null $expiresOn = [datetime]::MinValue if ("$provider" -eq 'AzAccount') { try { $tokenParams = @{ ResourceUrl = $ResourceUrl AsSecureString = $true ErrorAction = 'Stop' } if (-not [string]::IsNullOrWhiteSpace($TenantId) -and "$TenantId" -ne "$currentTenantId") { $tokenParams['TenantId'] = $TenantId } $azToken = Get-AzAccessToken @tokenParams if ($null -eq $azToken -or $null -eq $azToken.Token) { throw "Get-AzAccessToken returned an empty result for resource '$ResourceUrl'." } $secureToken = $azToken.Token $rawExpiry = $azToken.ExpiresOn if ($rawExpiry -is [datetimeoffset]) { $expiresOn = $rawExpiry.UtcDateTime } elseif ($rawExpiry -is [datetime]) { $expiresOn = $rawExpiry } else { $expiresOn = ([datetime]"$rawExpiry") } } catch { $messageString = "Failed to mint an access token for resource <c='em'>$ResourceUrl</c> via Get-AzAccessToken. Stopping execution, a missing token must never be ignored." Write-PSFMessage -Level Critical -Message $messageString -Exception $_.Exception throw } } else { try { $cliArgs = @('account', 'get-access-token', '--resource', $ResourceUrl) if (-not [string]::IsNullOrWhiteSpace($TenantId)) { $cliArgs += @('--tenant', $TenantId) } # Request both expiry shapes: expires_on (POSIX epoch, UTC) and expiresOn (datetime string). $cliArgs += @('--query', '{accessToken:accessToken, expires_on:expires_on, expiresOn:expiresOn}', '-o', 'json') $rawJson = & az @cliArgs 2>$null if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace("$rawJson")) { throw "The Azure CLI command 'az account get-access-token' failed with exit code $LASTEXITCODE." } $tokenJson = "$rawJson" | ConvertFrom-Json -ErrorAction Stop $accessTokenPlain = "$($tokenJson.accessToken)" if ([string]::IsNullOrWhiteSpace($accessTokenPlain)) { throw "The Azure CLI returned an empty access token for resource '$ResourceUrl'." } $expiresRaw = "$($tokenJson.expires_on)" if ([string]::IsNullOrWhiteSpace($expiresRaw)) { $expiresRaw = "$($tokenJson.expiresOn)" } $epoch = 0 if ([long]::TryParse($expiresRaw, [ref]$epoch)) { $expiresOn = [DateTimeOffset]::FromUnixTimeSeconds($epoch).UtcDateTime } else { $parsedExpiry = [datetime]::MinValue if ([datetime]::TryParse($expiresRaw, [ref]$parsedExpiry)) { $expiresOn = $parsedExpiry } else { throw "The Azure CLI returned an unparsable token expiry '$expiresRaw'." } } $secureToken = ConvertTo-SecureString -String $accessTokenPlain -AsPlainText -Force } catch { $messageString = "Failed to mint an access token for resource <c='em'>$ResourceUrl</c> via the Azure CLI. Stopping execution, a missing token must never be ignored." Write-PSFMessage -Level Critical -Message $messageString -Exception $_.Exception throw } } #endregion Mint new token $script:BapTokenCache[$cacheKey] = @{ Token = $secureToken ExpiresOn = $expiresOn AccountId = "$currentAccountId" TenantId = "$effectiveTenantId" } return $secureToken } <# .SYNOPSIS Clears the cached Azure access tokens. .DESCRIPTION Empties $script:BapTokenCache. Called by Set-BapToolsAuthProvider and Switch-BapTenant so a provider swap or tenant switch never serves a stale token. .EXAMPLE PS C:\> Clear-BapTokenCache Clears all cached access tokens. .NOTES Author: Motz Jensen (@Splaxi) #> function Clear-BapTokenCache { [CmdletBinding()] param () if ($null -eq $script:BapTokenCache) { $script:BapTokenCache = @{} } else { $script:BapTokenCache.Clear() } Write-PSFMessage -Level Verbose -Message "Cleared the BAP access token cache." } |