Pablo Cibraro

My notes about software development, security and other stuff.

Calling Azure Graph API from Powershell with a Client Certificate

The other day ran into scenario with Azure AD that is not very well documented (or documented at all) about calling the Azure Graph API with the client credential flow using a client certificate in Powershell.

This is only probably needed if you are doing automation work as I was doing for automatically register client or service applications.

When you use client certificates, you don't really pass the certificate but a handcraft JWT that is signed with that cert. This is much more complex as sending a plain text secret, but also more secure as it requires possession of the certificate.

The following script generates the JWT that you will need to negotiate an access token with Azure AD.

$TenantId = "Your tenant ID"
$AppId = "Your client App ID"

function GenerateJWT (){

    $thumbprint = "your certificate thumbprint here"

    $cert = Get-Item Cert:\LocalMachine\My\$Thumbprint

    $hash = $cert.GetCertHash()
    $hashValue = [System.Convert]::ToBase64String($hash) -replace '\+','-' -replace '/','_' -replace '='

    $exp = ([DateTimeOffset](Get-Date).AddHours(1).ToUniversalTime()).ToUnixTimeSeconds()
    $nbf = ([DateTimeOffset](Get-Date).ToUniversalTime()).ToUnixTimeSeconds()

$jti = New-Guid
    [hashtable]$header = @{alg = "RS256"; typ = "JWT"; x5t=$hashValue}
    [hashtable]$payload = @{aud = "https://login.microsoftonline.com/$TenantId/oauth2/token"; iss = "$AppId"; sub="$AppId"; jti = "$jti"; exp = $Exp; Nbf= $Nbf}

    $headerjson = $header | ConvertTo-Json -Compress
    $payloadjson = $payload | ConvertTo-Json -Compress

    $headerjsonbase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($headerjson)).Split('=')[0].Replace('+', '-').Replace('/', '_')
    $payloadjsonbase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($payloadjson)).Split('=')[0].Replace('+', '-').Replace('/', '_')

    $toSign = [System.Text.Encoding]::UTF8.GetBytes($headerjsonbase64 + "." + $payloadjsonbase64)

    $rsa = $cert.PrivateKey -as [System.Security.Cryptography.RSACryptoServiceProvider]

    $signature = [Convert]::ToBase64String($rsa.SignData($toSign,[Security.Cryptography.HashAlgorithmName]::SHA256,[Security.Cryptography.RSASignaturePadding]::Pkcs1)) -replace '\+','-' -replace '/','_' -replace '='

    $token = "$headerjsonbase64.$payloadjsonbase64.$signature"

    return $token
}

You will have to replace the TenantId, AppId (The client app where the client certificate was previously registered in Azure AD), and the client certificate thumbprint.

Once the JWT is generated, it can be used to get an access token for the graph api using the script below.


$RequestToken = GenerateJWT

$AccessTokenResponse = Invoke-WebRequest `
       -Method POST `
        -ContentType "application/x-www-form-urlencoded" `
        -Headers @{"accept"="application/json"} `
        -Body "scope=https://graph.microsoft.com/.default&client_id=$AppId&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion=$RequestToken&grant_type=client_credentials" `
        -Verbose `
        "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token" 

$AccessTokenJsonResponse = ConvertFrom-Json $AccessTokenResponse.Content
$AccessToken = $AccessTokenJsonResponse.access_token

Import Note. If you use a self-signed certificate, don't use one generated with Powershell as the private key will not be accessible. Try using a certificate generated with openssh.