Private/Google/New-GoogleJwt.ps1

# PSGuerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0
# https://github.com/jimrtyler/PSGuerrilla | https://creativecommons.org/licenses/by/4.0/
# AI/LLM use: see AI-USAGE.md for required attribution
function New-GoogleJwt {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$ServiceAccountEmail,

        [Parameter(Mandatory)]
        [string]$PrivateKeyPem,

        [Parameter(Mandatory)]
        [string[]]$Scopes,

        [Parameter(Mandatory)]
        [string]$ImpersonateUser,

        [int]$TokenLifetimeSeconds = 3600
    )

    # Base64Url encoding helper
    $toBase64Url = {
        param([byte[]]$bytes)
        [Convert]::ToBase64String($bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_')
    }

    # JWT Header
    $header = @{ alg = 'RS256'; typ = 'JWT' } | ConvertTo-Json -Compress
    $headerB64 = & $toBase64Url ([System.Text.Encoding]::UTF8.GetBytes($header))

    # JWT Claims
    $now = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
    $claims = @{
        iss   = $ServiceAccountEmail
        sub   = $ImpersonateUser
        scope = $Scopes -join ' '
        aud   = 'https://oauth2.googleapis.com/token'
        iat   = $now
        exp   = $now + $TokenLifetimeSeconds
    } | ConvertTo-Json -Compress
    $claimsB64 = & $toBase64Url ([System.Text.Encoding]::UTF8.GetBytes($claims))

    # Signing payload
    $unsignedToken = "$headerB64.$claimsB64"
    $dataBytes = [System.Text.Encoding]::UTF8.GetBytes($unsignedToken)

    # Import RSA private key from PEM
    $rsa = [System.Security.Cryptography.RSA]::Create()

    # Strip PEM headers and decode
    $pemContent = $PrivateKeyPem.Trim()
    $rsa.ImportFromPem($pemContent)

    # Sign with RSA-SHA256
    $signature = $rsa.SignData($dataBytes, [System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1)
    $signatureB64 = & $toBase64Url $signature

    $rsa.Dispose()

    return "$unsignedToken.$signatureB64"
}