Certificate Rotation

Provider Access API

Provider apps authenticate with a certificate, and certificates expire. Rotate yours from the My App page before that happens — this only works while your current certificate is still valid. If it's already expired, self-service rotation is no longer possible; contact support instead.

1 Generate a New Certificate

Generate a new public/private key pair the same way you did at registration. Keep the new private key — you'll need it going forward, and it never leaves your system.

2 Generate a Proof-of-Possession Token

Rotating a certificate without changing your Client ID requires Microsoft Entra ID to verify you still hold the current (not the new) certificate's private key. You prove that with a self-signed JWT — the "proof-of-possession" token — signed with your current private key. This is the same kind of signing operation you already perform to build the private_key_jwt client assertion for every access token request.

The token must contain exactly these claims:

Claim Value
aud 00000002-0000-0000-c000-000000000000
iss Your application's Client ID
nbf Not-before time (now)
exp nbf + 10 minutes (maximum lifetime — generate it right before you submit it)

Signed with your current private key (RS256). Generate this token locally in a terminal using your current private key file, then paste the resulting token into the My App page. Never generate this in a browser-based tool or anywhere your private key would have to leave your own machine — the key that signs this token is the same one you use to authenticate every access token request, and it must stay exactly as private. Microsoft's guide Generate PoP Tokens for Key Rolling and Certificate Updates covers the same token and claims with a C# sample implementation.

Assumes current-cert.pem (the public certificate already on file with us) and current-key.pem (its matching private key, kept only by you) as separate files — the usual layout when a cert/key pair is generated with OpenSSL.

#!/usr/bin/env bash
set -euo pipefail

CLIENT_ID="YOUR_CLIENT_ID"
CERT_PATH="current-cert.pem"    # your CURRENT certificate (public) — already on file with us
KEY_PATH="current-key.pem"      # its matching private key — never share this

base64url() { openssl base64 -A | tr '+/' '-_' | tr -d '='; }

now=$(date +%s)
exp=$((now + 600))  # 10 minutes — the maximum Graph allows

# x5t: base64url of the certificate's SHA-1 thumbprint, so Entra ID knows which
# registered key this proof corresponds to.
x5t=$(openssl x509 -in "$CERT_PATH" -outform DER | openssl dgst -sha1 -binary | base64url)

header=$(printf '{"alg":"RS256","typ":"JWT","x5t":"%s"}' "$x5t" | base64url)
payload=$(printf '{"aud":"00000002-0000-0000-c000-000000000000","iss":"%s","nbf":%d,"exp":%d}' \
    "$CLIENT_ID" "$now" "$exp" | base64url)

signing_input="${header}.${payload}"
signature=$(printf '%s' "$signing_input" | openssl dgst -sha256 -sign "$KEY_PATH" | base64url)

echo "${signing_input}.${signature}"

Same layout as the Bash example — a public current-cert.pem and a separate private current-key.pem. Requires PowerShell 7+ (uses RSA.ImportFromPem, not available in Windows PowerShell 5.1).

$ClientId = "YOUR_CLIENT_ID"
$CertPath = "current-cert.pem"   # your CURRENT certificate (public) — already on file with us
$KeyPath  = "current-key.pem"    # its matching private key — never share this

function ConvertTo-Base64Url([byte[]]$Bytes) {
    [Convert]::ToBase64String($Bytes).TrimEnd('=').Replace('+','-').Replace('/','_')
}

$now = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
$exp = $now + 600  # 10 minutes — the maximum Graph allows

$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($CertPath)
$x5t  = ConvertTo-Base64Url $cert.GetCertHash([System.Security.Cryptography.HashAlgorithmName]::SHA1)

$header  = @{ alg = "RS256"; typ = "JWT"; x5t = $x5t } | ConvertTo-Json -Compress
$payload = @{ aud = "00000002-0000-0000-c000-000000000000"; iss = $ClientId; nbf = $now; exp = $exp } | ConvertTo-Json -Compress

$headerB64  = ConvertTo-Base64Url ([System.Text.Encoding]::UTF8.GetBytes($header))
$payloadB64 = ConvertTo-Base64Url ([System.Text.Encoding]::UTF8.GetBytes($payload))
$signingInput = "$headerB64.$payloadB64"

$rsa = [System.Security.Cryptography.RSA]::Create()
$rsa.ImportFromPem((Get-Content $KeyPath -Raw))
$signatureBytes = $rsa.SignData(
    [System.Text.Encoding]::UTF8.GetBytes($signingInput),
    [System.Security.Cryptography.HashAlgorithmName]::SHA256,
    [System.Security.Cryptography.RSASignaturePadding]::Pkcs1)

"$signingInput.$(ConvertTo-Base64Url $signatureBytes)"
3 Submit the Rotation

On the My App page for your application, choose Rotate Certificate and paste in your new certificate (PEM) and the proof token from step 2. Submit within 10 minutes of generating the proof token, or generate a fresh one. Your new certificate takes effect immediately and the old one is removed — start signing requests with the new private key right away.