I’m working with Microsoft Graph with App-only access (Application permissions). I am trying to get an JWT Bearer access token by using our own App Registration; as recommended/required (1). The documentation states:
…a certificate is the recommended credential type…
Using Openssl I create an encrypted private and public key pair, using those keys I created a certificate, issued the certificate using our Private Key Infrastructure (PKI / Certificate Authority), uploaded the certificate without the private key (.cer/.crt) to the App Registration, and installed the cert with the key pair (.pfx / PCKS) to my local machine. Using PowerShell, I can get the JWT token from login.microsoftonline.com/[Teanat-GUID]/oauth2/token.
Partial PowerShell script I used to get JWT token.
$TenantName = "tenantID-GUID-1234-1234-123456789abc"
$AppId = "12345678-anAP-PID0-GUID-87654321"
$Certificate = get-item Cert:\LocalMachine\My\thethumbprint45678901234567890123456789012
#if using ms graph
$Scope = "https://graph.microsoft.com/.default"
# Create base64 hash of certificate
$CertificateBase64Hash = [System.Convert]::ToBase64String($Certificate.GetCertHash())
# Create JWT timestamp for expiration
$StartDate = (Get-Date "1970-01-01T00:00:00Z" ).ToUniversalTime()
$JWTExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End (Get-Date).ToUniversalTime().AddMinutes(60)).TotalSeconds
$JWTExpiration = [math]::Round($JWTExpirationTimeSpan, 0)
# Create JWT validity start timestamp
$NotBeforeExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End ((Get-Date).ToUniversalTime())).TotalSeconds
$NotBefore = [math]::Round($NotBeforeExpirationTimeSpan, 0)
# Create JWT header
$JWTHeader = @{
alg = "RS256"
typ = "JWT"
# Use the CertificateBase64Hash and replace/strip to match web encoding of base64
x5t = $CertificateBase64Hash -replace '\+', '-' -replace '/', '_' -replace '='
}
# Create JWT payload
$JWTPayLoad = @{
# What endpoint is allowed to use this JWT
aud = "https://login.microsoftonline.com/$TenantName/oauth2/token"
# Expiration timestamp
exp = $JWTExpiration
# Issuer = your application
iss = $AppId
# JWT ID: random guid
jti = [guid]::NewGuid()
# Not to be used before
nbf = $NotBefore
# JWT Subject
sub = $AppId
}
# Convert header and payload to base64
$JWTHeaderToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTHeader | ConvertTo-Json))
$EncodedHeader = [System.Convert]::ToBase64String($JWTHeaderToByte)
$JWTPayLoadToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTPayload | ConvertTo-Json))
$EncodedPayload = [System.Convert]::ToBase64String($JWTPayLoadToByte)
# Join header and Payload with "." to create a valid (unsigned) JWT
$JWT = $EncodedHeader + "." + $EncodedPayload
# Get the private key object of your certificate
$PrivateKey = ([System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate))
# Define RSA signature and hashing algorithm
$RSAPadding = [Security.Cryptography.RSASignaturePadding]::Pkcs1
$HashAlgorithm = [Security.Cryptography.HashAlgorithmName]::SHA256
# Create a signature of the JWT
$Signature = [Convert]::ToBase64String(
$PrivateKey.SignData([System.Text.Encoding]::UTF8.GetBytes($JWT), $HashAlgorithm, $RSAPadding)
) -replace '\+', '-' -replace '/', '_' -replace '='
# Join the signature to the JWT with "."
$JWT = $JWT + "." + $Signature
# Create a hash with body parameters
$Body = @{
client_id = $AppId
client_assertion = $JWT
client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
scope = $Scope
grant_type = "client_credentials"
}
$Url = "https://login.microsoftonline.com/$TenantName/oauth2/v2.0/token"
# Use the self-generated JWT as Authorization
$Header = @{
Authorization = "Bearer $JWT"
}
# Splat the parameters for Invoke-Restmethod for cleaner code
$PostSplat = @{
ContentType = 'application/x-www-form-urlencoded'
Method = 'POST'
Body = $Body
Uri = $Url
Headers = $Header
}
$Request = Invoke-RestMethod @PostSplat
# View access_token
$Request.access_token
I can get they token by using Postman with some PowerShell by doing the following:
- I set Postman authentication to OAuth 2.0: “Client Credentials” and “Send client credentials in body”.
- Use PowerShell to generate the “client_assertion” JWTPayLoad/“string”.
- Then add the following parameters to Authorization > Advanced > Token Request:
Key | Value |
---|---|
client_assertion_type | urn:ietf:params:oauth:client-assertion-type:jwt-bearer |
client_assertion | {{JWTPayLoad}} |
Partly working Pre-request script
//Create JWT Header
const base64Hash = Buffer.from(pm.environment.get('JWTCertThumbprint'),'hex').toString('base64');
const jwtHeader = {
alg: "RS256",
typ: "JWT",
x5t: base64Hash.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
};
// Get the start date in UTC
const startDate = new Date("1970-01-01T00:00:00Z");
// Get the current date in UTC
const currentDate = new Date();
// Calculate the Not Before expiration time span in seconds
const notBeforeExpirationTimeSpan = (currentDate.getTime() - startDate.getTime()) / 1000;
const notBefore = Math.round(notBeforeExpirationTimeSpan);
// Calculate the expiration time span in minutes (60 minutes)
const expirationTimeSpan = (currentDate.getTime() - startDate.getTime()) / 1000 + 60 * 60;
// Round the expiration time span to the nearest whole number
const jwtExpiration = Math.round(expirationTimeSpan);
//JWT Payload
const jwtPayload = {
aud: `https://login.microsoftonline.com/${pm.environment.get('TenantID')}/oauth2/token`,
exp: jwtExpiration,
iss: pm.environment.get('ClientID'),
jti: pm.environment.get('AppCertificateGUID'),
nbf: notBefore,
sub: pm.environment.get('ClientID')
};
//JWT Sign Payload
//const jwtHeaderToByte = new TextEncoder().encode(JSON.stringify(jwtHeader));
//const encodedHeader = btoa(String.fromCharCode(...jwtHeaderToByte));
//https://dev.to/2ezpz2plzme/btoa-replacement-in-nodejs-3k6g
const encodedHeader = Buffer.from(JSON.stringify(jwtHeader)).toString('base64');
//const jwtPayloadToByte = new TextEncoder().encode(JSON.stringify(jwtPayload));
//const encodedPayload = btoa(String.fromCharCode(...jwtPayloadToByte));
const encodedPayload = Buffer.from(JSON.stringify(jwtPayload)).toString('base64');
// need option for getting private key from certificate or encrypted PEM
const jwt = encodedHeader + "." + encodedPayload;
pm.environment.set('JWTPayLoad',jwt)
I’ve figured out how to generate the JWT information using a pre-script / Node.js (JavaScript?). I am stuck trying to get the private key from the certificate or encrypted PEM string. So that, I can then sign my JWT header and payload and used to create the base64 string for the “client_assertion”. Postman says that crypto-JS is deprecated. Node.js recommended using “crypto”, but that is no included with Postman.
- Is a better way to get the token via OAuth 2.0 using certificate credentials?
- If not, how do I get the private key and sign the header and payload within a Postman pre-request script?
I am using: Postman for Windows 11.18.0 x64, Windows 11 Enterprise 23H2, Postman Basic Plan, Microsoft Graph v1.0, Microsoft Entra ID P2