Create JWT Assertion for OAuth 2.0 using Certificate Credentials?

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:

  1. I set Postman authentication to OAuth 2.0: “Client Credentials” and “Send client credentials in body”.
  2. Use PowerShell to generate the “client_assertion” JWTPayLoad/“string”.
  3. 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}}
Screenshot of Postman Authoriztion

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

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.