Utilizing PowerShell for Testing the Client Credential Flow with Certificate
Overview
In the world of secure authentication, understanding the Client Credential flow supported by certificates is extremely important. However, grasping the details of this authentication method can be quite challenging. This is mainly because of the complex interaction between different parts that need careful setup to make sure everything works smoothly. This article explores the details of the Client Credential flow, explaining how it works, and also aims to help you get better at solving problems that might come up due to its complexities.
1. Creating the Certificate
1.1 Generating a new self-signed certificate using PowerShell
To do this, execute the following command:
New-SelfSignedCertificate -Subject "CN=ClientCredentialKey" -KeyExportPolicy Exportable -KeySpec Signature -KeyUsage DigitalSignature -Type Custom -FriendlyName "ClientCredentialFlowCert" -CertStoreLocation "Cert:\CurrentUser\My"
This command will create a self-signed certificate named “ClientCredentialFlowCert” along with its private key.
1.2 Export the public key of the self-signed certificate
Press Win + R to open the Run dialog.
Type certmgr.msc and press Enter. This will open the “Current User” certificate store.
Expand the Personal Store and locate the certificate created in the previous step.
Right-click on the certificate, navigate to “All Tasks,” and choose “Export.”
Follow the prompts, selecting “No, do not export the private key,” and save the exported public key.
Proceed to the Azure portal and find the app registration representing the client.
In the Azure portal, navigate to the “Certificates & secrets” menu of the app registration. Go to the Certificates tab and upload the previously exported public key of the certificate.
2. Testing Client Credential Flow with Certificate-based Authentication
2.1 Utilizing the PowerShell Script
The following PowerShell script utilizes the certificate generated in the “Creating the Certificate” step to establish authentication with AAD. This script automatically generates the required client_assertion and initiates the authentication request to AAD through the Client Secret flow.
# Replace with your own tenant name $TenantName = "rayaki.onmicrosoft.com" # Enter the Client/App ID of your app $AppId = "481d41e0-e265-4ae4-aaac-000000000000" # Replace the thumbprint to your self-signed cert $Certificate = Get-Item Cert:\CurrentUser\My\<thumbprint of your cert> # Example: "https://graph.microsoft.com/.default" $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(2)).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
Once the script is executed successfully, you will observe the retrieval of an access token.
Please take note that the Access token mentioned above corresponds to the final result, contains the process that sending the client assertion to the token endpoint. It’s important to understand that this script does not directly display the client assertion itself. For a clearer view of the client assertion within the HTTP trace, kindly refer to the subsequent section.
3. Deep Dive
3.1 Understanding the Script’s Actions
We can perform a Fiddler capture to observe the process. Within the capture, there’s a frame where the client_assertion is sent to the OAuth token endpoint in order to obtain the access token.
3.2 Decode the client_assertion
We can also trace the events in reverse. The x5t
represents the x509 certificate thumbprint. In the example below, this thumbprint is transformed into a Base64 string
As demonstrated below, this value can be converted into its hexadecimal representation, which corresponds to the value displayed on the certificate.
3.3 How we obtained the client_assertion ($JWT in the PsScript) parameter
Client_assertion is created using the System.IdentityModel.Tokens.Jwt library, which takes the client ID and the certificate as input, and creates a JWT security token. The JwtSecurityTokenHandler is then used to write the token to a string, which is then assigned to the client_assertion parameter. The JWT string is encoded with the certificate and includes a signature that the token endpoint can use to authenticate the client.
Initially, we observe the construction of the JWT parameter through EncodedHeader and EncodedPayload
# Join header and Payload with "." to create a valid (unsigned) JWT $JWT = $EncodedHeader + "." + $EncodedPayload
Ultimately, the signature is appended to create a comprehensive Client_assertion JWT
# Join the signature to the JWT with "." $JWT = $JWT + "." + $Signature
The signature value has been signed using the private key of the certificate.
# Get the private key object of your certificate $PrivateKey = ([System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate))
Recall the initial step when the private key was extracted from the personal store.
# Replace the thumbprint to your self-signed cert $Certificate = Get-Item Cert:\CurrentUser\My\<thumbprint of your cert>
Once we have the fully assembled Client_assertion JWT, we proceed to send it to the OAuth token endpoint in order to obtain the access token.