Search code examples
oauth-2.0jwtazure-ad-b2cazure-ad-msalreact-typescript

React MSAL access token has invalid signature


Setup

msal (in another file. Passed using MsalProvider):

const msalInstance = new PublicClientApplication({
    auth: {
        clientId: <B2C-Application-ID>,
        authority: "https://login.microsoftonline.com/<tenant-directory-id>",
        redirectUri: "http://localhost:3000",
    },
    cache: {
        cacheLocation: "sessionStorage",
        storeAuthStateInCookie: false,
    }
});

Import:

import * as msal from "@azure/msal-browser";
import {EventType, InteractionStatus} from "@azure/msal-browser";

import React, {createContext, FC, useState} from "react";
import {useIsAuthenticated, useMsal} from "@azure/msal-react";
import {AuthenticationContextType} from "../@types/authentication";
import {EndSessionRequest} from "@azure/msal-browser/dist/request/EndSessionRequest";
import jwtDecode, {JwtPayload} from "jwt-decode";

Variables:

const {instance, accounts, inProgress} = useMsal();
const isAuthenticated = useIsAuthenticated();
const [token, setToken] = useState<string | null>(null);

Login:

function loginRedirect() {
    instance.loginRedirect({
        scopes: ["User.Read"],
        prompt: "select_account"
    });
}

Acquire token:

function getToken(): string | null {
    if (token) {
        const decodedJwt = jwtDecode<JwtPayload>(token);
        if (decodedJwt.exp && decodedJwt.exp * 1000 > Date.now()) {
            return token; // Token is still valid
        }
    }

    // If token is not available or not valid anymore, acquire a new one
    if (instance.getActiveAccount() && inProgress === InteractionStatus.None) {
        const accessTokenRequest = {
            scopes: ["User.Read"],
            account: accounts[0]
        }

        instance.acquireTokenSilent(accessTokenRequest)
            .then(response => {
                console.log(`access token: ${response.accessToken}`);
                console.log(`id token: ${response.idToken}`);
                setToken(response.accessToken);
                return response.accessToken;
            })
            .catch(err => {
                if (err instanceof msal.InteractionRequiredAuthError) {
                    return instance.acquireTokenPopup(loginRequest)
                        .then(response => {
                            setToken(response.accessToken);
                            return response.accessToken;
                        })
                        .catch(err => {
                            console.log(err);
                        })
                } else {
                    console.log(err);
                }
            })
    } else {
        console.error("No account logged in to acquire token");
    }
    return null;
}

Problem

I acquire two tokens (ID and access) from msal (see console logs). The ID token is being validated successfully (on my API and jwt.io) but my access token is not (neither on my API nor jwt.io). Referring to this microsoft documentation I should use the access token to validate against an API.

As far as I can see, jwt.io does fetch the public key correctly from https://sts.windows.net/<tenant-directory-id>/discovery/v2.0/keys. This means this solution is either outdated, or doesn't solve my problem. To go sure I also tried to copy&paste the public key, which didn't work either.

I also found this solution which didn't work for me either. Changing the scopes leads to an endless login loop.

Versions:

"@azure/msal-browser": "^2.28.3",
"@azure/msal-react": "^1.4.7",
"jwt-decode": "^3.1.2",

Solution

  • 1. Scope

    For requesting B2C access tokens you have to specify a valid scope. These are also set in Azure (Azure AD B2C -> App registrations -> your application -> Manage -> API permissions). There you have to specify a scope. While acquiring the tokens you have to specify these scopes like this:

    const accessTokenRequest = {
        scopes: ["https://<tenant-name>.onmicrosoft.com/<app-id>/<scope>"],
    }
    
    await instance.acquireTokenSilent(accessTokenRequest)
        .then(response => {
            setIdToken(response.idToken);
            setAccessToken(response.accessToken);
        })
        .catch(async err => {
            if (err instanceof msal.InteractionRequiredAuthError) {
                await instance.acquireTokenPopup(accessTokenRequest)
                    .then(response => {
                        setIdToken(response.idToken);
                        setAccessToken(response.accessToken);
                    })
                    .catch(err => {
                        console.log(err);
                    })
            } else {
                console.log(err);
            }
        })
    

    tenant-name you can find this in the Application ID URI
    app-id is your Application (client) ID
    your-scope could be something like Subscriptions.Read

    A full example for a scope could be:
    https://mydemo.onmicrosoft.com/12345678-0000-0000-0000-000000000000/Subscriptions.Read

    2. Invalid token version

    For me the problem was 1. Scope but maybe this does not solve the problem for others. Here is something else to try:

    Following this article, the sts url is used vor the v1 endpoint. The documentation claims:

    The endpoint used, v1.0 or v2.0, is chosen by the client and only impacts the version of id_tokens. Possible values for accesstokenAcceptedVersion are 1, 2, or null. If the value is null, this parameter defaults to 1, which corresponds to the v1.0 endpoint.

    This means that the used endpoint (v2.0 in my case) affected only the id-token, which made it validate successfully. The access token was still v1 thus with no validated signature.

    Solution

    To change the version, accessTokenAcceptedVersion needs to be set to 2 inside the Manifest. It is located at portal.azure.com -> Azure AD B2C -> App registrations -> your application -> Manage -> Manifest:

    {
        ...
        "accessTokenAcceptedVersion": 2,
        ...
    }
    

    Save the changes and wait. For me it took several hours to wait for the change to be applied. And I had to apply solution 1. Scope as well. After that, the iss of new tokens should be https://login.microsoftonline.com/<tenant-directory-id>/v2.0 instead of the sts-uri