Public/Get-M365GraphToken.ps1

function Get-M365GraphToken {
    <#
    .SYNOPSIS
        Acquires a Microsoft Graph API access token using client credentials.
    .DESCRIPTION
        Supports two authentication methods:
        - Client Secret: Pass -ClientSecret
        - Certificate: Pass -Certificate (X509Certificate2 with private key)
          or -CertificateThumbprint (loads from CurrentUser\My store)
    .OUTPUTS
        [string] Bearer access token
    #>

    [CmdletBinding(DefaultParameterSetName = 'ClientSecret')]
    param(
        [Parameter(Mandatory)]
        [string]$TenantId,

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

        [Parameter(Mandatory, ParameterSetName = 'ClientSecret')]
        [string]$ClientSecret,

        [Parameter(Mandatory, ParameterSetName = 'Certificate')]
        [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,

        [Parameter(Mandatory, ParameterSetName = 'CertificateThumbprint')]
        [string]$CertificateThumbprint
    )

    Write-M365Log "Authenticating to Microsoft Graph..."
    Write-M365Log " Tenant ID: $TenantId"
    Write-M365Log " Application ID: $AppId"

    $tokenEndpoint = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"

    if ($PSCmdlet.ParameterSetName -eq 'ClientSecret') {
        Write-M365Log " Auth method: Client Secret"
        $body = @{
            client_id     = $AppId
            scope         = 'https://graph.microsoft.com/.default'
            client_secret = $ClientSecret
            grant_type    = 'client_credentials'
        }
        $tokenResponse = Invoke-RestMethod -Uri $tokenEndpoint -Method Post -Body $body -ContentType 'application/x-www-form-urlencoded'
    }
    else {
        # Certificate auth — build JWT client assertion
        if ($PSCmdlet.ParameterSetName -eq 'CertificateThumbprint') {
            Write-M365Log " Auth method: Certificate (thumbprint: $CertificateThumbprint)"
            $Certificate = Get-ChildItem -Path "Cert:\CurrentUser\My\$CertificateThumbprint" -ErrorAction Stop
        }
        else {
            Write-M365Log " Auth method: Certificate (subject: $($Certificate.Subject))"
        }

        if (-not $Certificate.HasPrivateKey) {
            throw "Certificate does not contain a private key."
        }

        # x5t header: Base64url-encoded SHA-1 thumbprint
        $thumbprintBytes = [byte[]]::new($Certificate.Thumbprint.Length / 2)
        for ($i = 0; $i -lt $thumbprintBytes.Length; $i++) {
            $thumbprintBytes[$i] = [Convert]::ToByte($Certificate.Thumbprint.Substring($i * 2, 2), 16)
        }
        $x5t = [Convert]::ToBase64String($thumbprintBytes) -replace '\+', '-' -replace '/', '_' -replace '='

        $header = @{ alg = 'RS256'; typ = 'JWT'; x5t = $x5t } | ConvertTo-Json -Compress
        $now = [DateTimeOffset]::UtcNow
        $payload = @{
            aud = $tokenEndpoint
            iss = $AppId
            sub = $AppId
            jti = [Guid]::NewGuid().ToString()
            nbf = $now.ToUnixTimeSeconds()
            exp = $now.AddMinutes(10).ToUnixTimeSeconds()
        } | ConvertTo-Json -Compress

        $toBase64Url = {
            param([string]$text)
            $bytes = [System.Text.Encoding]::UTF8.GetBytes($text)
            [Convert]::ToBase64String($bytes) -replace '\+', '-' -replace '/', '_' -replace '='
        }

        $headerB64  = & $toBase64Url $header
        $payloadB64 = & $toBase64Url $payload
        $unsigned   = "$headerB64.$payloadB64"

        $rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate)
        $signatureBytes = $rsa.SignData(
            [System.Text.Encoding]::UTF8.GetBytes($unsigned),
            [System.Security.Cryptography.HashAlgorithmName]::SHA256,
            [System.Security.Cryptography.RSASignaturePadding]::Pkcs1
        )
        $signatureB64 = [Convert]::ToBase64String($signatureBytes) -replace '\+', '-' -replace '/', '_' -replace '='

        $body = @{
            client_id             = $AppId
            scope                 = 'https://graph.microsoft.com/.default'
            client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
            client_assertion      = "$unsigned.$signatureB64"
            grant_type            = 'client_credentials'
        }
        $tokenResponse = Invoke-RestMethod -Uri $tokenEndpoint -Method Post -Body $body -ContentType 'application/x-www-form-urlencoded'
    }

    Write-M365Log "Successfully acquired Graph access token"
    return $tokenResponse.access_token
}