azureoauth-2.0jwtmicrosoft-entra-id

Entra ID OAuth 2.0 /token request failing using client_assertion


I am attempting to get a JWT issued using authentication certificate credentials, per Microsoft identity platform application authentication certificate credentials In order to use an OAuth JWT to access the SharePoint Online API.

In order to generate the client_assertion I am using the following code:

  const privateKeyPEM = createPrivateKey(pem);
 
 
  const jwt = await new SignJWT({})
   .setProtectedHeader({ 
      alg: 'PS256', // or RS384 or PS256
      typ: 'JWT',
      x5t: 'REI1RREI1RDM2MTM1MDJFQjBEQTE3NkU2RDc3MkYxMUQ5OTdFQTc5MTlDQgo' // base 64 encoded 'X.509 SHA-1 Thumbprint (in hex)' field from Azure Key Vault
   })
   .setAudience('https://login.microsoftonline.com/<my tenant>/oauth2/v2.0/token')
   .setExpirationTime('1h')
   .setIssuer('aab7f7ac-XXXXX')
   .setJti(v4())
   .setNotBefore('1s')
   .setSubject('aab7f7ac-XXXXX')
   .setIssuedAt()
   .sign(privateKeyPEM);

The generated JWT looks fine, per Microsoft documentation and has the correct claims:

HEADER

{
  "alg": "PS256",
  "typ": "JWT",
  "x5t": "REI1RREI1RDM2MTM1MDJFQjBEQTE3NkU2RDc3MkYxMUQ5OTdFQTc5MTlDQgo"
}

PAYLOAD

{
  "aud": "https://login.microsoftonline.com/<my tenant>/oauth2/v2.0/token",
  "exp": 1749013438,
  "iss": "aab7f7ac-XXXXX",
  "jti": "e6cbe5c7-5dfe-4d57-bbde-f85aabcd121b",
  "nbf": 1749009839,
  "sub": "aab7f7ac-XXXXX",
  "iat": 1749009838
}

I am using the JWT as the client_assertion in the following Postman call

enter image description here

However, I am getting the following error:

{
    "error": "invalid_client",
    "error_description": "AADSTS700027: The certificate with identifier used to sign the client assertion is not registered on application. [Reason - The key was not found., Thumbprint of key used by client: '444235451108D510CCD8C4CCD4C0C91508C11104C4DCD914D910DCDCC918C4C510E4E4DD1504DCE4C4E50D0828', Please visit the Azure Portal, Graph Explorer or directly use MS Graph to see configured keys for app Id 'aab7f7ac-eea6-4f0a-9c8d-a15e6798c1ee'. Review the documentation at https://docs.microsoft.com/en-us/graph/deployments to determine the corresponding service endpoint and https://docs.microsoft.com/en-us/graph/api/application-get?view=graph-rest-1.0&tabs=http to build a query request URL, such as 'https://graph.microsoft.com/beta/applications/aab7f7ac-XXXXX']. Alternatively, SNI may be configured on the app. Please ensure that client assertion is being sent with the x5c claim in the JWT header using MSAL's WithSendX5C() method so that Azure Active Directory can validate the certificate being used. Trace ID: 1f02592a-a6e1-4c9e-a992-35677cf54e00 Correlation ID: ecc793cd-98dc-4a1d-964a-ce6b88ccf3ef Timestamp: 2025-06-04 04:05:08Z",
    "error_codes": [
        700027
    ],
    "timestamp": "2025-06-04 04:05:08Z",
    "trace_id": "1f02592a-a6e1-4c9e-a992-35677cf54e00",
    "correlation_id": "ecc793cd-98dc-4a1d-964a-ce6b88ccf3ef",
    "error_uri": "https://login.microsoftonline.com/error?code=700027"
}

I have confirmed that the Certificate in Key Vault is associated with the application. Further, I am successfully using the Certificate in an Azure DevOps pipeline to deploy artefacts to SharePoint online.

It just seems to be broken using Postman. Any thoughts would be gratefully received.

Cheers, Andrew


Solution

  • The error is because the certificate used to sign their JWT does not exactly match the public certificate uploaded in Azure AD.

    I uploaded the .cer certificate in the Microsoft Entra ID application:

    enter image description here

    Generated client assertion by using below code:

    import { readFileSync, writeFileSync } from 'fs';
    import { createPrivateKey, createHash } from 'crypto';
    import { SignJWT } from 'jose';
    import { v4 as uuidv4 } from 'uuid';
    
    
    const clientId = 'ClientID';
    const tenantId = 'TenantID';
    
    // Read your private key and cert
    const privateKey = createPrivateKey(readFileSync('./private.key', 'utf-8'));
    const publicCertPem = readFileSync('./public.crt', 'utf-8');
    
    // Remove PEM header/footer and newlines from cert
    const publicCertBase64 = publicCertPem
      .replace('-----BEGIN CERTIFICATE-----', '')
      .replace('-----END CERTIFICATE-----', '')
      .replace(/\r?\n|\r/g, '');
    
    // Convert Base64 cert to DER Buffer
    const derCertBuffer = Buffer.from(publicCertBase64, 'base64');
    
    // Compute SHA-1 thumbprint of DER cert and base64url encode it
    const sha1 = createHash('sha1').update(derCertBuffer).digest('base64');
    const base64url = (str) =>
      str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
    
    const x5t = base64url(sha1);
    
    console.log('SHA-1 Thumbprint (x5t):', x5t);
    
    const jwt = await new SignJWT({})
      .setProtectedHeader({
        alg: 'PS256',
        typ: 'JWT',
        x5c: [publicCertBase64],
        x5t: x5t,
      })
      .setIssuer(clientId)
      .setSubject(clientId)
      .setAudience(`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`)
      .setIssuedAt()
      .setExpirationTime('1h')
      .setJti(uuidv4())
      .sign(privateKey);
    
    console.log('\nYour JWT:\n\n', jwt);
    

    enter image description here

    Passed the above JWT as client_assertion and generated access token successfully:

    GET https://login.microsoftonline.com/TenantID/oauth2/v2.0/token
    
    client_id: ClientID
    scope: https://graph.microsoft.com/.default
    client_assertion_type: urn:ietf:params:oauth:client-assertion-type:jwt-bearer
    client_assertion: AboveJWT
    grant_type: client_credentials
    

    enter image description here