Private/New-JwtAssertion.ps1
|
function New-JwtAssertion { <# .SYNOPSIS Creates a signed JWT assertion for certificate-based authentication. .DESCRIPTION Generates a JWT (JSON Web Token) signed with a certificate's private key for use in OAuth2 client_credentials flow with Azure AD. JWT Structure: - Header: { "alg": "RS256", "typ": "JWT", "x5t": "<thumbprint>" } - Payload: { "iss": "<clientId>", "sub": "<clientId>", "aud": "<tokenEndpoint>", "jti": "<guid>", "exp": <now+600>, "nbf": <now>, "iat": <now> } - Signature: RS256(header.payload, privateKey) .PARAMETER Certificate X509Certificate2 object with private key. .PARAMETER ClientId Azure AD application (client) ID. .PARAMETER TenantId Azure AD tenant ID. .PARAMETER ValiditySeconds JWT validity period in seconds (default: 600 = 10 minutes). .OUTPUTS [string] - The signed JWT assertion. .EXAMPLE $cert = Get-Item Cert:\CurrentUser\My\ABC123 $jwt = New-JwtAssertion -Certificate $cert -ClientId "xxx" -TenantId "yyy" #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory = $true)] [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate, [Parameter(Mandatory = $true)] [string]$ClientId, [Parameter(Mandatory = $true)] [string]$TenantId, [Parameter()] [int]$ValiditySeconds = 600 ) process { # Validate certificate has private key if (-not $Certificate.HasPrivateKey) { throw "Certificate does not contain a private key. Certificate-based authentication requires the private key." } # Get the private key (cross-platform compatible) $privateKey = $null # Try GetRSAPrivateKey() first (newer .NET) if ($Certificate.PSObject.Methods.Name -contains 'GetRSAPrivateKey') { $privateKey = $Certificate.GetRSAPrivateKey() } # Fallback to PrivateKey property (older approach) if (-not $privateKey -and $Certificate.PrivateKey) { $privateKey = $Certificate.PrivateKey } if (-not $privateKey) { throw "Failed to get RSA private key from certificate. Ensure the certificate uses RSA encryption and includes the private key." } # Calculate x5t (X.509 certificate SHA-1 thumbprint, base64url encoded) $thumbprintBytes = [System.Convert]::FromHexString($Certificate.Thumbprint) $x5t = ConvertTo-Base64UrlString -Bytes $thumbprintBytes # Build JWT Header $header = @{ alg = "RS256" typ = "JWT" x5t = $x5t } # Calculate timestamps $now = [DateTimeOffset]::UtcNow $nbf = [long]$now.ToUnixTimeSeconds() $iat = $nbf $exp = [long]$now.AddSeconds($ValiditySeconds).ToUnixTimeSeconds() # Build JWT Payload $tokenEndpoint = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token" $payload = @{ aud = $tokenEndpoint iss = $ClientId sub = $ClientId jti = [guid]::NewGuid().ToString() nbf = $nbf iat = $iat exp = $exp } # Convert to JSON and Base64URL encode $headerJson = $header | ConvertTo-Json -Compress $payloadJson = $payload | ConvertTo-Json -Compress $headerBase64 = ConvertTo-Base64UrlString -Text $headerJson $payloadBase64 = ConvertTo-Base64UrlString -Text $payloadJson # Create signature input $signatureInput = "$headerBase64.$payloadBase64" $signatureInputBytes = [System.Text.Encoding]::UTF8.GetBytes($signatureInput) # Sign with RSA-SHA256 $signatureBytes = $privateKey.SignData( $signatureInputBytes, [System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1 ) $signatureBase64 = ConvertTo-Base64UrlString -Bytes $signatureBytes # Construct final JWT $jwt = "$headerBase64.$payloadBase64.$signatureBase64" Write-Verbose "Created JWT assertion for client '$ClientId' valid for $ValiditySeconds seconds" Write-Verbose "JWT x5t (thumbprint): $x5t" return $jwt } } function ConvertTo-Base64UrlString { <# .SYNOPSIS Converts bytes or text to Base64URL encoding. .DESCRIPTION Base64URL encoding is Base64 with: - '+' replaced with '-' - '/' replaced with '_' - Trailing '=' padding removed #> [CmdletBinding(DefaultParameterSetName = 'Bytes')] [OutputType([string])] param( [Parameter(Mandatory = $true, ParameterSetName = 'Bytes')] [byte[]]$Bytes, [Parameter(Mandatory = $true, ParameterSetName = 'Text')] [string]$Text ) if ($PSCmdlet.ParameterSetName -eq 'Text') { $Bytes = [System.Text.Encoding]::UTF8.GetBytes($Text) } $base64 = [System.Convert]::ToBase64String($Bytes) # Convert to Base64URL $base64Url = $base64 -replace '\+', '-' -replace '/', '_' -replace '=+$', '' return $base64Url } |