Search code examples
reactjsauthenticationazure-active-directory.net-6.0next-auth

Acquire Azure on-behalf-of access token in react next-app


I'm encountering an issue with the "on behalf of" (OBO) authentication flow in my application. My infrastructure consists of the following components:

  • A Next.js React app that utilizes the next-auth package for authentication.
  • An API built with .NET 6.
  • An Application Registration configured in the Azure portal.

I've implemented the standard AzureADProvider in my api/auth/[...nextauth]/route.ts file. I've parameterized it with the client ID, client secret, and tenant ID from my application registration. Everything is functioning smoothly and I'm able to obtain a token from Azure. However, this token is intended for the Microsoft Graph resource, making it unsuitable for authenticating requests to my .NET 6 API.

To manage with this I would like to use the on-behalf-of flow to acquire an appropriate token for my API resource. Below is the next-auth JWT callback code:

const authority = `https://login.microsoftonline.com/${process.env.AZURE_AD_TENANT_ID}`;
const config = {
  auth: {
    clientId: process.env.AZURE_AD_CLIENT_ID as string,
    authority,
    clientSecret: process.env.AZURE_AD_CLIENT_SECRET as string,
    knownAuthorities: [authority],
  },
};

const cca = new msal.ConfidentialClientApplication(config);
const acquireOnBehalfOfAccessToken = async (assertion: string) =>
  await cca
    .acquireTokenOnBehalfOf({
      oboAssertion: assertion,
      scopes: [process.env.API_SCOPE as string],
    })
    .catch((err) => console.log(err));

export const authOptions: NextAuthOptions = {
  ...
  callbacks: {
    async jwt({ token, user, account }) {
      if (account && account.access_token && account.expires_at) {
        if (Date.now() < account.expires_at * 1000) {
          token.apiTokenDetails = await acquireOnBehalfOfAccessToken(
            account.access_token as string
          );
          return token;
        }
      }

      return token;
    },
  },
};

Unfortunately, I'm encountering an error:

AADSTS50013: Assertion failed signature validation. [Reason - The key was not found., Thumbprint of key used by client: 'xxxx'

Surprisingly, I'm able to obtain my API token using a refresh token through the following method:

const generateApiAccessToken = async (refreshToken: string) =>
  await cca
    .acquireTokenByRefreshToken({
      scopes: [process.env.API_SCOPE as string],
      refreshToken,
    })
    .catch((err) => console.log(err));

How is possible, that I'm able to retrieve an access token using the refresh token method, yet I'm encountering difficulties with the on-behalf-of flow. The OBO flow seems like a more elegant approach for implementing authentication in both my React and .NET applications.


Solution

  • The error may occur if you pass wrong assertion generated with scopes other than exposed API, to get access token in on-behalf of flow.

    I have one application named Web API where I exposed API scopes as below:

    enter image description here

    Now I registered one client application and added API permissions as below:

    enter image description here

    Now, I generated access token using authorization code flow with API scope via Postman:

    POST https://login.microsoftonline.com/<tenantID>/oauth2/v2.0/token
    client_id: <OBO_app_ID>
    grant_type:authorization_code
    scope: api://xxxxxxxxxxx/custom.scope
    code: code
    redirect_uri: https://jwt.ms
    client_secret: <secret> 
    

    Response:

    enter image description here

    You can decode the above token in jwt.ms to check aud and scp claims:

    enter image description here

    Now, pass above access token as assertion value in on-behalf of flow with Microsoft Graph scope. I generated access token using on-behalf of flow via Postman with below parameters:

    POST https://login.microsoftonline.com/<tenantID>/oauth2/v2.0/token
    client_id: <API_app_ID>
    client_secret: <API_app_secret>
    scope: https://graph.microsoft.com/.default
    grant_type: urn:ietf:params:oauth:grant-type:jwt-bearer
    assertion: <paste_token_from_above_request>
    requested_token_use: on_behalf_of
    

    Response:

    enter image description here

    To confirm that, I decoded this access token in jwt.ms and got audience of Microsoft Graph with User.Read in scp claim.

    enter image description here

    When I used this token to call Microsoft graph, I got response successfully like below:

    GET https://graph.microsoft.com/v1.0/me
    

    Response:

    enter image description here

    In your case, you need to first generate access token from middle-tier API with exposed API scope and use it as assertion for the OBO call to acquire token for Microsoft Graph.

    References:

    Microsoft identity platform and OAuth2.0 On-Behalf-Of flow

    azure active directory - On behalf of flow returns AADSTS50013 - Stack Overflow by Chauncy Zhou