Private/Get-IDBridgeAzureAuthToken.ps1
|
<# .SYNOPSIS Acquire an Entra ID (Azure AD) OAuth token using a certificate credential (internal). .DESCRIPTION Internal helper for the AzKeyVault secrets provider. Implements the OAuth 2.0 client credentials flow with a certificate: builds a signed JWT client assertion (RS256, base64url, x5t = certificate hash) and exchanges it at the tenant's v2.0 token endpoint — no external module. The app registration must have the certificate's public key uploaded as a credential. The signing certificate is looked up by thumbprint in the CurrentUser and LocalMachine My stores; the running account needs read access to its private key. .PARAMETER ClientId The app registration (client) ID. .PARAMETER TenantId The Entra tenant ID or domain (e.g. 'contoso.onmicrosoft.com'). .PARAMETER CertThumbprint Thumbprint of the certificate uploaded to the app registration. .PARAMETER Scope The requested scope (e.g. 'https://vault.azure.net/.default'). .EXAMPLE $token = Get-IDBridgeAzureAuthToken -ClientId $id -TenantId $tenant -CertThumbprint $thumb -Scope 'https://vault.azure.net/.default' .NOTES Created by: Sam Cattanach Modified: 2026-07-02 #> function Get-IDBridgeAzureAuthToken { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$ClientId, [Parameter(Mandatory = $true)] [string]$TenantId, [Parameter(Mandatory = $true)] [string]$CertThumbprint, [Parameter(Mandatory = $true)] [string]$Scope ) # Find the auth certificate in the user or machine store (the private key signs the assertion) $clientCertificate = Get-Item "Cert:\*\My\$CertThumbprint" -ErrorAction SilentlyContinue | Select-Object -First 1 if (-not $clientCertificate) { Throw "The Azure auth certificate '$CertThumbprint' (Secrets.AzKeyVault.CertThumbprint) was not found in the CurrentUser or LocalMachine My store." } $tokenUrl = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token" # JWT parts are base64url encoded (base64 with -/_ and no padding) $x5t = [System.Convert]::ToBase64String($clientCertificate.GetCertHash()) -replace '\+', '-' -replace '/', '_' -replace '=' $jwtHeader = @{ alg = "RS256" typ = "JWT" x5t = $x5t } $now = [DateTimeOffset]::UtcNow $jwtPayload = @{ aud = $tokenUrl iss = $ClientId sub = $ClientId jti = [guid]::NewGuid().ToString() nbf = $now.ToUnixTimeSeconds() exp = $now.AddMinutes(10).ToUnixTimeSeconds() } $encodedHeader = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes(($jwtHeader | ConvertTo-Json -Compress))) -replace '\+', '-' -replace '/', '_' -replace '=' $encodedPayload = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes(($jwtPayload | ConvertTo-Json -Compress))) -replace '\+', '-' -replace '/', '_' -replace '=' $jwt = "$encodedHeader.$encodedPayload" # Sign the assertion with the certificate's private key (SHA-256 / PKCS1) $privateKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($clientCertificate) if (-not $privateKey) { Throw "The Azure auth certificate '$CertThumbprint' has no readable private key for the current account." } $signature = [System.Convert]::ToBase64String( $privateKey.SignData([System.Text.Encoding]::UTF8.GetBytes($jwt), [System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1) ) -replace '\+', '-' -replace '/', '_' -replace '=' $jwt = "$jwt.$signature" $body = @{ client_id = $ClientId client_assertion = $jwt client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" scope = $Scope grant_type = "client_credentials" } try { $tokenResponse = Invoke-RestMethod -Uri $tokenUrl -Method POST -Body $body -ContentType 'application/x-www-form-urlencoded' -ErrorAction Stop } catch { Throw "Failed to acquire an Azure token for client '$ClientId' in tenant '$TenantId': $($_)" } Write-Log -Message "Secret: Acquired Azure token for client '$ClientId' (scope $Scope)." -Level Trace return $tokenResponse } |