Private/New-ECMA2GraphClientAssertion.ps1
|
function New-ECMA2GraphClientAssertion { <# .SYNOPSIS Builds and signs a JWT client assertion for Microsoft Entra app-only certificate authentication. .DESCRIPTION Internal function. Creates a short-lived RS256-signed JWT bearer assertion from the supplied certificate, per the OAuth2 client credentials + client assertion flow, without requiring MSAL or the Microsoft.Graph SDK. .PARAMETER ClientId The application (client) ID of the Entra app registration. .PARAMETER TenantId The tenant ID or domain the app is registered in. .PARAMETER Certificate The X509Certificate2 (with private key) to sign the assertion with. .EXAMPLE New-ECMA2GraphClientAssertion -ClientId $id -TenantId $tenant -Certificate $cert #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$ClientId, [Parameter(Mandatory)] [string]$TenantId, [Parameter(Mandatory)] [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate ) try { $tokenEndpoint = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token" $now = [DateTimeOffset]::UtcNow $x5t = [Convert]::ToBase64String($Certificate.GetCertHash()).TrimEnd('=').Replace('+', '-').Replace('/', '_') $header = @{ alg = 'RS256' typ = 'JWT' x5t = $x5t } | ConvertTo-Json -Compress $payload = @{ aud = $tokenEndpoint iss = $ClientId sub = $ClientId jti = [guid]::NewGuid().ToString() nbf = $now.ToUnixTimeSeconds() exp = $now.AddMinutes(10).ToUnixTimeSeconds() iat = $now.ToUnixTimeSeconds() } | ConvertTo-Json -Compress $encodedHeader = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($header)).TrimEnd('=').Replace('+', '-').Replace('/', '_') $encodedPayload = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($payload)).TrimEnd('=').Replace('+', '-').Replace('/', '_') $unsignedToken = "$encodedHeader.$encodedPayload" # $Certificate.GetRSAPrivateKey() is a C# extension method - PowerShell's dot-notation # method resolution doesn't see extension methods (in either Windows PowerShell 5.1 or # PowerShell 7), so it must be invoked as a static call on the extension class instead. $rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate) if (-not $rsa) { throw "Certificate does not have an accessible RSA private key." } $signature = $rsa.SignData( [System.Text.Encoding]::UTF8.GetBytes($unsignedToken), [System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1 ) $encodedSignature = [Convert]::ToBase64String($signature).TrimEnd('=').Replace('+', '-').Replace('/', '_') return "$unsignedToken.$encodedSignature" } catch { throw "Failed to build client assertion JWT: $_" } } |