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

  • I found a much better way to achieve the outcome using the PnPjs libraries. This abstracts away the complexity of getting the Bearer token using a JWT via the MSAL libraries.

    Full working code below

    import { spfi } from "@pnp/sp";
    import { SPDefault } from "@pnp/nodejs";
    import {
      folderFromAbsolutePath,
      folderFromServerRelativePath,
    } from "@pnp/sp/folders/index.js";
    import "@pnp/sp/files/index.js";
    import "@pnp/sp/webs/index.js";
    import { readFileSync, createReadStream } from "fs";
    import "@azure/msal-node";
    
    const sharepointTenant = `https://tenant.sharepoint.com`; // replace with your tenant
    const sharepointSites = `${sharepointTenant}/sites/Foo`; // replace with your site
    const folderUrl = "Shared Documents/Bar"; // replace with your folder
    const sharePointFolder = `${sharepointSites}/${folderUrl}`;
    
    /**
     *
     * @returns
     */
    async function getSp() {
      const tenantId = "XXXX"; // replace from Azure Entra ID
      const clientId = "YYYY"; // replace from Azure Entra ID
      const thumbprint = "ZZZZ"; // replace from Azure Entra ID
    
      const buffer = readFileSync(
        "private.key" // the private key for JWT signing
      );
    
      const config = {
        auth: {
          authority: `https://login.microsoftonline.com/${tenantId}/`,
          clientId: clientId,
          clientCertificate: {
            thumbprint: thumbprint,
            privateKey: buffer.toString(),
          },
        },
      };
    
      console.log(`Config: ${JSON.stringify(config)}\n\n`);
    
      const sp = spfi().using(
        SPDefault({
          baseUrl: sharepointSites,
          msal: {
            config: config,
            scopes: [`${sharepointTenant}/.default`],
          },
        })
      );
    
      const w = await sp.web.select("Title", "Description")();
      console.log(`${JSON.stringify(w, null, 4)}\n\n`);
      return sp;
    }
    
    try {
      console.log(
        `TENANT:\t${sharepointTenant}\nSITES:\t${sharepointSites}\nFOLDER:\t${sharePointFolder}\n`
      );
    
      const sp = await getSp();
      const folderAbsolute = await folderFromAbsolutePath(sp.web, sharePointFolder);
      const folderRelative = folderFromServerRelativePath(sp.web, folderUrl);
    
      const folderInfo = await folderAbsolute();
      //const relativeFolderInfo = await folderRelative();
    
      console.log(`${JSON.stringify(folderInfo, null, 4)}\n\n`);
      //console.log(`${JSON.stringify(folderInfo,null,4)}\n\n`);
    
      const fileNamePath = "file.txt";
      const file = readFileSync(`./${fileNamePath}`, "utf8");
      const stream = createReadStream(fileNamePath);
      let result = await sp.web
        .getFolderByServerRelativePath(folderUrl)
        .files.addUsingPath(encodeURI(fileNamePath), file, { Overwrite: true });
    } catch (error) {
      console.error(error);
    }
    

    Cheers,
    Andrew