Powershell Cert based authentication against the Graph API using a certificate from Keyvault

In automation scenarios it is common to use a service principal (app based) to work with the Graph API, or in my example, with PNP PowerShell against SharePoint (but both scenario’s work the same).

First, you’d need a client certificate, e.g. like this:

$folder = "c:\users\JosLieben\Desktop"
$cert=New-SelfSignedCertificate -Subject "CN=JOS" -CertStoreLocation "Cert:\CurrentUser\My"  -KeyExportPolicy Exportable -KeySpec Signature -HashAlgorithm "SHA256" -Provider "Microsoft Enhanced RSA and AES Cryptographic Provider"
Export-Certificate -Cert $cert -FilePath "$folder\jos.cer" 
Export-PfxCertificate -Password (ConvertTo-SecureString $clientCertPwd -AsPlainText -Force) -Cert $cert -FilePath "$folder\jos.pfx"

You’d then upload the .cer file as a certificate on your service principal to let Azure AD recognize your cert as a valid ‘password’ for your app registration.

Next you’d upload your .pfx file into Keyvault.

Finally, you can use Powershell to construct an access token for a given scope:

$tenantId = "YOURTENANTID"
$clientId = "YOURCLIENTID"
$scope = "https://graph.microsoft.com/.default" #or, e.g. https://$($tenantName)-admin.sharepoint.com/.default openid profile offline_access
$secret = Get-AzKeyVaultSecret -VaultName "YOURKEYVAULTNAME" -Name cippCert -AsPlainText
$clientCert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList @([Convert]::FromBase64String($secret),"",[System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable)

$header = @{
    alg = "RS256"
    typ = "JWT"
    x5t = [System.Convert]::ToBase64String(($clientCert.GetCertHash()))
} | ConvertTo-Json -Compress

$claimsPayload = @{
    aud = "https://login.microsoftonline.com/$tenantId/oauth2/token"
    exp = [math]::Round(((New-TimeSpan -Start ((Get-Date "1970-01-01T00:00:00Z" ).ToUniversalTime()) -End (Get-Date).ToUniversalTime().AddMinutes(2)).TotalSeconds),0)
    iss = $clientId
    jti = (New-Guid).Guid
    nbf = [math]::Round(((New-TimeSpan -Start ((Get-Date "1970-01-01T00:00:00Z" ).ToUniversalTime()) -End ((Get-Date).ToUniversalTime())).TotalSeconds),0)
    sub = $clientId
} | ConvertTo-Json -Compress

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

$preJwt = $headerjsonbase64 + "." + $claimsPayloadjsonbase64
$toSign = [System.Text.Encoding]::UTF8.GetBytes($preJwt)
$privateKey = $clientCert.PrivateKey
$alg = [Security.Cryptography.HashAlgorithmName]::SHA256
$padding = [Security.Cryptography.RSASignaturePadding]::Pkcs1
$signature = [Convert]::ToBase64String($privateKey.SignData($toSign,$alg,$padding)) -replace '\+','-' -replace '/','_' -replace '='

$jwt = $headerjsonbase64 + "." + $claimsPayloadjsonbase64 + "." + $signature

$Authbody = @{
    'tenant' = $tenantId
    'scope' = $scope
    'client_assertion_type' = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
    'client_id'     = $clientId
    'grant_type'    = 'client_credentials'
    'client_assertion' = $jwt

$accessToken = (Invoke-RestMethod -Method post -Uri "https://login.microsoftonline.com/$($tenantid)/oauth2/v2.0/token" -Body $Authbody -ErrorAction Stop).accesstoken

The token can then be used to used to call Graph. And an example that shows how to use a sharepoint scoped token for the Sharepoint PNP PowerShell moddule:

#use scope: https://$($tenantName)-admin.sharepoint.com/.default

connect-PnPOnline -Url "https://$($tenantName)-admin.sharepoint.com" -AccessToken $accessToken -ReturnConnection
Notify of

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Inline Feedbacks
View all comments