Certificate Rotation
Provider Access APIProvider 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.
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.
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)"
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.