Summary
Continuing from the last post, this entry explores how to achieve the desired outcome using PowerShell—step by step.
Step By Step Solution
Step # 1: Create a self-signed cert and export a PFX for Azure AD app authentication
Intro: This script creates a self-signed certificate in your CurrentUser certificate store, then exports it (including the private key) to a PFX file you can use for automated client authentication.
$certName = "CN=MyAzureADAppCert"
$certPath = "C:\SRC\MyAzureADAppCert.pfx" # CHANGE the PATH to your need
$certPassword = ConvertTo-SecureString -String "[Change to your need]" -Force -AsPlainText
# Create self-signed certificate in the CurrentUser\My store
$cert = New-SelfSignedCertificate -Subject $certName `
-CertStoreLocation "Cert:\CurrentUser\My" `
-KeyExportPolicy Exportable `
-KeySpec Signature `
-KeyLength 2048 `
-NotAfter (Get-Date).AddYears(2) `
-HashAlgorithm "SHA256"
# Export the certificate with private key to a PFX file
Export-PfxCertificate -Cert $cert `
-FilePath $certPath `
-Password $certPassword
# Output thumbprint and path
Write-Host "Certificate Thumbprint: $($cert.Thumbprint)"
Write-Host "Certificate exported to: $certPath"
What the script does (line-by-line)
a. Variables
$certName = “CN=MyAzureADAppCert” — subject name for the certificate.
$certPath — path where the PFX (private key + cert) will be written.
$certPassword — secure string used to protect the exported PFX.
b. Create the certificate
CertStoreLocation — where cert is saved (CurrentUser\My).
KeyExportPolicy Exportable — allows exporting the private key (needed to create a PFX).
KeySpec Signature — key intended for signing (used to sign the JWT client_assertion).
KeyLength, NotAfter, HashAlgorithm — cryptographic choices (2048-bit RSA, two-year lifetime, SHA256).
c. Export the cert+private key to a PFX
This creates MyAzureADAppCert.pfx that contains the private key — keep it secret.
Output thumbprint and path
Write-Host prints the certificate thumbprint; you’ll use that thumbprint to reference the cert (e.g., as kid/x5t in JWT headers).
d. Why this is useful?
Apps that need non-interactive auth (service-to-service) should use certificate-based client credentials instead of long-lived client secrets. The PFX holds the private key used to sign a JWT that Azure AD will verify using the public certificate uploaded to the App Registration.
Security note
NOTE: (IMPORTANT)
- Protect the PFX (private key). Do not commit to source control.
- Upload only the public certificate (.cer) to Azure AD.
Step # 2: Use the PFX to obtain an access token (certificate-based client_credentials)
High-level steps:
- Export the public cert (.cer) and upload it to the Azure AD App registration.
- Use the PFX (private key) locally to build and sign a client_assertion JWT.
- POST to Azure AD token endpoint with client_assertion to get an access token.
- Call the resource API (e.g., Azure Digital Twins) with the returned access token.
# Get certificate
$cert = Get-Item "Cert:\CurrentUser\My\[REPLACE WITH THUMBPRINT YOUR VALUE]"
$now = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
$exp = $now + 3600
$client_id = "REPLACE WITH YOUR VALUE"
$tenant_id = "REPLACE WITH YOUR VALUE"
# Replace these variables with your values
$adt_instance_url = "[TODO REPLACE YOUR VALUE].azure.net" # e.g., myinstance.api.wus2.digitaltwins.azure.net
$api_version = "2023-10-31" # Make sure you are using the latest version
# Compute SHA-1 thumbprint and base64url encode it for x5t
$sha1 = $cert.Thumbprint
# Convert hex thumbprint to byte array
$bytes = for ($i=0; $i -lt $sha1.Length; $i+=2) { [Convert]::ToByte($sha1.Substring($i,2),16) }
# Base64 encode, then base64url encode
$b64 = [Convert]::ToBase64String($bytes)
$x5t = $b64.Replace('+','-').Replace('/','_').Replace('=','')
# Build JWT header with x5t
$header = @{ alg = "RS256"; typ = "JWT"; x5t = $x5t } | ConvertTo-Json -Compress
# Build payload
$payload = @{
aud = "https://login.microsoftonline.com/$tenant_id/oauth2/v2.0/token"
iss = $client_id
sub = $client_id
jti = [guid]::NewGuid().ToString()
nbf = $now
exp = $exp
}
# Make sure you install the JWT module for New-Jwt
# clone the git to SRC
# git clone https://github.com/SP3269/posh-jwt.git
# Import-Module C:\SRC\posh-jwt\JWT\JWT.psd1
# Create JWT with custom header
$jwt = New-Jwt -Cert $cert -Header $header -PayloadJson ($payload | ConvertTo-Json -Compress)
Write-Host "Signed JWT:" $jwt
# Prepare token request
$body = @{
client_id = $client_id
scope = "https://digitaltwins.azure.net/.default"
client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
client_assertion = $jwt
grant_type = "client_credentials"
}
$loginResponse = Invoke-RestMethod -Method Post -Uri "https://login.microsoftonline.com/$tenant_id/oauth2/v2.0/token" -Body $body
$access_token = $loginResponse.access_token
# Prepare the query body
$body = @{
query = "SELECT * FROM DIGITALTWINS"
} | ConvertTo-Json
# Prepare headers
$headers = @{
"Authorization" = "Bearer $access_token"
"Content-Type" = "application/json"
}
# Invoke the REST API
$response = Invoke-RestMethod -Method Post `
-Uri "https://$adt_instance_url/query?api-version=$api_version" `
-Headers $headers `
-Body $body
$response
Explanation of the above PowerShell code
- Export public certificate (no private key) Follow Certmgr.msc or Certificate Manager in Windows 11/10
- From the certificate store (thumbprint from script output) export the public cert:
- The
.cerfile contains the public key only — safe to upload. - Portal path: Azure Portal -> Azure Active Directory -> App registrations -> Your app -> Certificates & secrets -> Upload certificate -> select the
.cer. - Do NOT upload the PFX to Azure AD (unless you’re using a managed identity or special scenario). PFX contains the private key — it must remain private and protected by you.
- What Azure stores: public key and certificate metadata; Azure uses this to validate signatures of JWTs signed by the corresponding private key.
- When you POST the signed JWT (client_assertion), Azure AD looks for a matching public cert uploaded under the app. It uses the JWT header
x5torkidto pick the right public key. If not found, you get invalid_client / invalid JWT errors. - Generate and sign client_assertion JWT (PowerShell outline)
- JWT header must include either
x5t(base64url-encoded SHA-1 thumbprint) orkidthat matches the uploaded certificate identity. Azure AD requires one to map token to the uploaded public key. - Minimal PowerShell (using the posh-jwt approach you have available — adapt thumbprint and paths):
- Call Azure AD token endpoint using the client_assertion
- If the token call succeeds, Azure AD validated the signed JWT using the public cert you uploaded and returned an access token.
- Use the token to call the resource API (ADT example)
Conclusion
- The above code creates and exports a PFX with a private key for signing client_assertion JWTs.
- Upload the public
.certo Azure AD App -> Certificates & secrets. - Use the PFX locally to sign a JWT (include
x5torkidin header) then POST it asclient_assertionto the token endpoint. - Use returned access token to call the resource API.
Reference