Private/Get-EntraAccessToken.ps1
|
function Get-EntraAccessToken { <# .SYNOPSIS Acquires an Entra (Azure AD) access token for an Azure DevOps collection. .DESCRIPTION Entra is the default authentication for REST calls: a PAT or -UseDefaultCredentials is an explicit opt-out, not the other way round. Tokens are cached per tenant for the life of the session, so a run makes one interactive sign-in at most. The tenant is discovered from the collection itself. Azure DevOps returns the tenant GUID in the 'X-VSS-ResourceTenant' response header, so an unauthenticated probe is enough. Pinning sign-in to that tenant stops Az enumerating - and failing MFA on - every other tenant the signed-in user can see. An on-premises Azure DevOps Server collection is not Entra-backed and returns no such header. That is not an error here: the caller is told to pass -UseDefaultCredentials (the normal on-premises case) or a -Pat. Lifted from the NKDAClient-United-Machine Set-WorkItemStartId.ps1 script, generalised to work from a collection URL rather than a dev.azure.com organisation name. .PARAMETER Collection Collection or organisation URL, e.g. https://dev.azure.com/contoso. .PARAMETER Force Re-acquire even when a cached token exists. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Collection, [switch]$Force ) $tenantId = Get-AzureDevOpsTenantId -Collection $Collection if (-not $tenantId) { throw "'$Collection' is not Entra-backed (no X-VSS-ResourceTenant), so Entra sign-in cannot be used. Pass -UseDefaultCredentials for an on-premises collection, or supply a -Pat." } if (-not $script:EntraTokenCache) { $script:EntraTokenCache = @{} } if (-not $Force -and $script:EntraTokenCache.ContainsKey($tenantId)) { $cached = $script:EntraTokenCache[$tenantId] # Renew a little before expiry rather than on it, so a long run does not fail mid-call. if ($cached.ExpiresOn -gt (Get-Date).AddMinutes(5)) { return $cached.Token } } Initialize-AzAccounts # 499b84ac-1321-427f-aa17-267ca6975798 is the well-known Azure DevOps application ID. $adoResource = '499b84ac-1321-427f-aa17-267ca6975798' $context = Get-AzContext -ErrorAction SilentlyContinue if (-not $context -or $context.Tenant.Id -ne $tenantId) { Write-FixStep "Signing in to Entra tenant $tenantId ..." Connect-AzAccount -TenantId $tenantId -ErrorAction Stop -WarningAction SilentlyContinue | Out-Null } $token = Get-AzAccessToken -ResourceUrl $adoResource -TenantId $tenantId -ErrorAction Stop # AsSecureString became the default in newer Az.Accounts versions. $value = if ($token.Token -is [System.Security.SecureString]) { [System.Net.NetworkCredential]::new('', $token.Token).Password } else { $token.Token } $expiresOn = if ($token.ExpiresOn) { ([datetimeoffset]$token.ExpiresOn).LocalDateTime } else { (Get-Date).AddMinutes(45) } $script:EntraTokenCache[$tenantId] = @{ Token = $value; ExpiresOn = $expiresOn } return $value } |