Access Token verkrijgen onder de knie: Een certificaat gebruiken

Dit blogbericht maakt deel uit van een reeks. In dit bericht belichten we het gebruik van een certificaat. Bekijk de andere berichten over de andere autorisatiestromen:

Hoewel het gebruik van de client credentials flow met een client secret een van mijn favoriete manieren is om een access token te verkrijgen, is het gebruik van een certificaat een van de veiligste. Dit is hetzelfde als het gebruik van een client secret, maar dan geef je client_assertion en client_assertion_type op in de body van het verzoek in plaats van client_secret.

Terwijl de parameter client_assertion_type altijd dezelfde waarde heeft, draait deze vorm van authenticatie om de parameter client_assertion. Dit is een JWT (JSON Web Token) dat bestaat uit de volgende drie delen, gescheiden door een punt:

  • Een gecodeerde base64-header
  • Een gecodeerde base64-payload
  • Een gecodeerde base64-handtekening

De beveiliging in deze authenticatievorm is het feit dat je in de payload kunt aangeven hoe lang en tot wanneer precies de JWT geldig mag blijven. En het feit dat de handtekening digitaal is ondertekend met de privésleutel van je certificaat. Zo kan de App-registratie op authenticiteit controleren voordat een access token wordt verleend, omdat ditzelfde certificaat is geconfigureerd in de Entra App-registratie.

Om een access token te verkrijgen met deze autorisatiestroom moeten we 3 dingen doen:

  • Een certificaat aanmaken en uploaden in onze Entra App-registratie
  • Een JWT aanmaken
  • Deze JWT gebruiken om een access token te verkrijgen

Het certificaat aanmaken

Laten we eerst het certificaat aanmaken. We kunnen dit doen met PowerShell. Dit certificaat wordt toegevoegd aan de certificatenlocatie van de computer. Daarna exporteren we het zodat we het kunnen uploaden in onze Entra App-registratie. Met dit stukje PowerShell-code kunnen we het certificaat aanmaken en exporteren. Let op: ik gebruik de C:-locatie om mijn certificaat naar te exporteren, maar je kunt een locatie naar keuze kiezen.

$certname = "MySyperAwesomeCertificate"
$cert = New-SelfSignedCertificate -Subject "CN=$certname" -CertStoreLocation "Cert:\CurrentUser\My" -KeyExportPolicy Exportable -KeySpec Signature -KeyLength 2048 -KeyAlgorithm RSA -HashAlgorithm SHA256
Export-Certificate -Cert $cert -FilePath "C:\$certname.cer"

Als alles goed gaat, zie je zoiets als dit:

Nu we ons certificaat hebben, kunnen we het uploaden naar onze Entra App-registratie in de sectie Certificates & secrets:

  • Klik op Certificates & secrets
  • Klik op de sectie Certificates
  • Klik op Upload certificate

Vervolgens schuift er een paneel aan de rechterkant open. Selecteer je certificaat van de locatie waar je het naartoe hebt geëxporteerd en geef je certificaat een beschrijving (optioneel).

De JWT aanmaken

Voor het verkrijgen van een JWT op basis van je certificaat gebruik ik onderstaand PowerShell-script. In dit script moet je 3 parameters opgeven:

  • Je tenant-ID
  • Je client-ID
  • De vingerafdruk van je certificaat. Als je niet weet hoe je de vingerafdruk kunt ophalen, kun je je certificaat openen vanaf de locatie waar je het hebt geëxporteerd en op details klikken. Onderaan de details vind je de vingerafdruk.

Twee secties die ik wil uitlichten zijn de header en de payload. Dit zijn 2 objecten die uiteindelijk worden gecodeerd naar een base64-tekenreeks. De header bestaat uit de volgende parameters:

  • alg: (algoritme) Moet RS256 zijn
  • typ: (type) Moet JWT zijn
  • x5t: (X.509 Certificate SHA-1 Thumbprint) dit wordt gebruikt om de SHA-1-vingerafdruk aan te geven van een X.509-certificaat dat wordt gebruikt voor het ondertekenen van het JWT-token.

De payload bestaat uit de volgende parameters:

  • aud: (doelgroep) identificeert de ontvangers waarvoor de JWT bedoeld is. In dit geval is dit https://login.microsoft.com/[YOUR_TENANT_ID]/oauth2/v2.0/token.

  • exp: (vervaltijd) dit geeft aan wanneer de JWT moet verlopen. Dit zorgt ervoor dat de JWT niet onbeperkt geldig is, maar slechts voor een bepaalde periode. Het onderstaande codefragment gebruikt 5 minuten.

  • iss: (uitgever) Het geeft aan wie de JWT heeft uitgegeven, d.w.z. de entiteit die verantwoordelijk is voor het aanmaken en ondertekenen van het token. In dit geval de client-ID.

  • jwt: (JWT-ID) Een unieke identifier (GUID) voor het token.

  • nbf: (Not Before) De tijd vanaf wanneer het token geldig is.

  • sub: (onderwerp) Het onderwerp van het token, bijvoorbeeld de entiteit waarop het token van toepassing is. Moet hetzelfde zijn als iss, dus je client-ID.

  • iat: (uitgegeven op) De tijd waarop het token is uitgegeven.

$TenantId = "[YOUR_TENANT_ID]"
$ClientId = "[YOUR_CLIENT_ID]"

# Load the self-signed and uploaded certificate using its thumbprint
$Certificate = Get-Item Cert:\CurrentUser\My\[YOUR_THUMBPRINT]

# Convert the certificate to a base64 string hash of the certificate
$CertificateAsBase64 = [System.Convert]::ToBase64String($Certificate.GetCertHash())

# Create a start date for creating a timespan
$StartDate = (Get-Date "1970-01-01T00:00:00Z").ToUniversalTime()

#Make a timespan from StartDate and let it expire after 5 minutes
$ExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End (Get-Date).ToUniversalTime().AddMinutes(5)).TotalSeconds
$Expiration = [math]::Round($ExpirationTimeSpan, 0)

# Create a timespan before which the JWT MUST NOT be accepted for processing
$NotBeforeExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End ((Get-Date).ToUniversalTime())).TotalSeconds
$Nbf = [math]::Round($NotBeforeExpirationTimeSpan, 0)

# Create the header
$Header = @{
    alg = "RS256"
    typ = "JWT"
    x5t = $CertificateAsBase64 -replace '\+', '-' -replace '/', '_' -replace '='
}

# Create the payload
$Payload = @{
    aud = "https://login.microsoftonline.com/$TenantId/oauth2/token"
    exp = $Expiration
    iss = $ClientId
    jti = [guid]::NewGuid()
    nbf = $Nbf
    sub = $ClientId
}

# Convert header to base64
$HeaderAsBase64 = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes(($Header | ConvertTo-Json)))

# Convert header to base64
$PayloadAsBase64 = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes(($Payload | ConvertTo-Json)))

# Combine header and payload with "." to form an unsigned JWT
$UnsignedJWT = $HeaderAsBase64 + "." + $PayloadAsBase64

# Retrieve the private key object from the certificate
$PrivateKey = ([System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate))

# Create a RSA Signature padding
$RSASignaturePadding = [Security.Cryptography.RSASignaturePadding]::Pkcs1

# Create a hashalgoritm
$HashAlgorithmName = [Security.Cryptography.HashAlgorithmName]::SHA256

# Create the JWT signature
$JWTSignature = [Convert]::ToBase64String($PrivateKey.SignData([System.Text.Encoding]::UTF8.GetBytes($UnsignedJWT), $HashAlgorithmName, $RSASignaturePadding)) -replace '\+', '-' -replace '/', '_' -replace '='

# Combine the signature with the JWT using "."
$SignedJWT = $UnsignedJWT + "." + $JWTSignature

Het access token ophalen

Om het access token op te halen moeten we een POST-verzoek doen naar het endpoint https://login.microsoft.com/[YOUR_TENANT_ID]/oauth2/v2.0/token met de volgende parameters in de body van het verzoek:

  • client_id: de id van je client. Deze is te vinden op het startscherm van je app-registratie.
  • client_assertion: De JWT die we hebben samengesteld met het PowerShell-script hierboven ($SignedJWT)
  • client_assertion_type: urn:ietf:params:oauth:client-assertion-type:jwt-bearer
  • grant_type: client_credentials
  • scope: het bereik moet de resource-identifier zijn van de applicatie waartegen je je access token wilt gebruiken, aangevuld met .default. Voor Microsoft Graph zou dit bijvoorbeeld https://graph.microsoft.com/.default zijn.

Als alles goed gaat, zou je een reactie als deze moeten ontvangen: