An API that connects multiple Microsoft services, enabling data access and automation across platforms
Working solution using Microsoft.Graph.Authentication Module only.
Tested under powershell 7.4.1 and Windows PowerShell 5.1.22621.2506
For me the error was that I did not remove the padding in both jwt headers, I also don't see anything inherently wrong with your version, but it might be that your datetimes are not in UTC. not that you are likely to read this two and a half years later.
# Replace for your Environment
Connect-MgGraph -TenantId $graphConfig.tenantID -ClientId $graphConfig.unprivClientID -CertificateThumbprint $graphConfig.thumb
# Application / Client ID of the App you are rotating the Certificate on
$clientID = $graphConfig.unprivClientID
# Load Certificate from Windows Store
$cert = Get-Item "Cert:\CurrentUser\My\$($graphConfig.thumb)" -ErrorAction Stop
# Alternative from a Certificate file, drop in Replacement
# $cert = Get-PfxCertificate "[pathToCer]"
# Get base64 hash of certificate in Web Encoding
$CertificateBase64Hash = [System.Convert]::ToBase64String($cert.GetCertHash()) -replace '\+', '-' -replace '/', '_' -replace '='
# Very far removed, but still based on https://adamtheautomator.com/powershell-graph-api/
$StartDate = (Get-Date "1970-01-01T00:00:00Z").ToUniversalTime()
$now = (Get-Date).ToUniversalTime()
# Create JWT timestamp for expiration
$JWTExpirationTimeSpan = ( New-TimeSpan -Start $StartDate -End $now.AddMinutes(2) ).TotalSeconds
$JWTExpiration = [math]::Round($JWTExpirationTimeSpan, 0)
# Create JWT validity start timestamp
$NotBeforeExpirationTimeSpan = ( New-TimeSpan -Start $StartDate -End $now ).TotalSeconds
$NotBefore = [math]::Round($NotBeforeExpirationTimeSpan, 0)
# Create JWT header
$JWTHeader = @{
alg = "RS256"
typ = "JWT"
x5t = $CertificateBase64Hash
}
# Create JWT payload
$JWTPayLoad = @{
aud = "00000002-0000-0000-c000-000000000000"
exp = $JWTExpiration
iss = $clientID
jti = [guid]::NewGuid()
nbf = $NotBefore
sub = $clientID
}
# Check if there are Equals in the JWT to replace, since that is referenced in one of the articles (add)
# Convert header and payload to base64
$JWTHeaderToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTHeader | ConvertTo-Json))
$EncodedHeader = [System.Convert]::ToBase64String($JWTHeaderToByte) -replace '='
$JWTPayLoadToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTPayload | ConvertTo-Json))
$EncodedPayload = [System.Convert]::ToBase64String($JWTPayLoadToByte) -replace '='
$JWT = $EncodedHeader + "." + $EncodedPayload
# Define RSA signature and hashing algorithm
$RSAPadding = [Security.Cryptography.RSASignaturePadding]::Pkcs1
$HashAlgorithm = [Security.Cryptography.HashAlgorithmName]::SHA256
# Sign the JWT
$rsaCert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
$Signature = [Convert]::ToBase64String(
$rsaCert.SignData([System.Text.Encoding]::UTF8.GetBytes($JWT), $HashAlgorithm, $RSAPadding)
) -replace '\+', '-' -replace '/', '_' -replace '='
# Add Signature to JWT
$JWT = $JWT + "." + $Signature
# Load the new Certificate, again use Get-PfxCertificate as a drop in Replacement
$newCert = Get-Item "Cert:\CurrentUser\My\$($graphConfig.newThumb)" -ErrorAction Stop
$params = @{
keyCredential = @{
type = "AsymmetricX509Cert"
usage = "Verify"
key = [convert]::ToBase64String($newCert.GetRawCertData())
}
passwordCredential = $null
proof = $JWT
}
Invoke-MgGraphRequest POST "/v1.0/applications(appId='$clientID')/addKey" -Body $params