Microsoft-identity-web: [Bug] AddMicrosoftIdentityWebApiAuthentication with default configuration results in Bearer error="invalid_token", error_description="The signature is invalid"

Created on 4 Nov 2020  路  6Comments  路  Source: AzureAD/microsoft-identity-web

Which version of Microsoft Identity Web are you using?
Note that to get help, you need to run the latest version.

Where is the issue?

  • Web API

    • [ X] Protected web APIs (validating tokens)

Is this a new or an existing app?
This is a new application
Repro
Followed SPA tutorial, I get my token, send the token to the backend (yes its missing awaits, hardly matters if it does not work)

 const configuration = {
                headers: {
                    "Authorization": "Bearer " + sessionStorage.getItem('DRAGONQUEST')
                }
            };
            fetch('api/test', configuration);

The only code I have is this:

services.AddMicrosoftIdentityWebApiAuthentication(Configuration, "AzureAd");

in the TestController.cs
[Route("api/test"), ApiController, Authorize]
    public class TestController : ControllerBase
    {
        [HttpGet]
        public IActionResult Get()
        {
            HttpContext.VerifyUserHasAnyAcceptedScope(ApplicationWideConstants.SCOPE_REQUIRED_BY_API);

            return Ok();

        }
    }

Expected behavior
I guess I would expect a call to api/test to work

Actual behavior
A clear and concise description of what happens, e.g. an exception is thrown, UI freezes.

Additional context / logs / screenshots
I'm willing to have a Teams meeting to show the issue live whenever is convenient.

Answered Faq question

Most helpful comment

@jrodrigoav

for what is worth, the token I get I can use to call https://graph.microsoft.com/v1.0/me using postman

  1. This is the issue. AzureAD does not give a token for several resources. you need to remove "user.read" from your scopes and have only "api://[SAME_AS_CLIENT_ID]/access_as_user" (otherwise you get a token for Microsoft Graph, not for your web API
  2. BTW this should be the clientID of your web API here (not of your SPA, unless they have the same clientID).

The following web API sample illustrates your scenario : https://github.com/Azure-Samples/active-directory-dotnet-native-aspnetcore-v2/tree/master/2.%20Web%20API%20now%20calls%20Microsoft%20Graph (but with a desktop app, instead of a SPA, but that's not the most difficult thing to change.

You might also be interested in these samples/tutorials:

cc: @kalyankrishna1 @derisen

All 6 comments

@jrodrigoav what is the SPA tutorial? can you please share repro steps that we can repro (a link, or some code)

for the moment with the elements you provided, I think you got a token for an API other than your API (for example a token for Graph), and try to use it for your web API ? but I could be wrong ... I'm missing context.

So sorry for getting back to this so late

This is the article
https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-spa-app-configuration?tabs=javascript

I followed the steps to acquire a token and then followed the steps to Protect the WebAPI using Azure AD:
https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-protected-web-api-overview

I can share the code but not sure it will do much without the unique identifiers, that I can't share because "security reasons".

For what is worth, the token I get I can use to call https://graph.microsoft.com/v1.0/me using postman

Here is the HTML I am using, it is pretty much the example.

<!doctype html>
<html lang="en">
<head>  
    <title>Index</title>
</head>
<body>
    <h1>Hello</h1>
    <script type="text/javascript" src="https://alcdn.msauth.net/browser/2.5.2/js/msal-browser.min.js"></script>
    <script>
        const msalConfig = {
            auth: {
                clientId: '[REDACTED]',
                authority: 'https://login.microsoftonline.com/[REDACTED_COMPANY_GUID]',
                redirectUri: 'https://localhost:44303/index.html'
            },
            cache: {
                cacheLocation: "sessionStorage",
                storeAuthStateInCookie: false
            }
        };

        const REQUEST_SCOPES = {
            scopes: ["openid", "profile", "User.Read","api://[SAME_AS_CLIENT_ID]/access_as_user"]
        };

        let account = null;
        const msalInstance = new msal.PublicClientApplication(msalConfig);

        function getAccounts(msalInstance) {
            let currentAccounts = [];
            let success = false;
            try {
                // In case multiple accounts exist, you can select
                currentAccounts = msalInstance.getAllAccounts();
                if (currentAccounts === null) {
                    // no accounts detected
                } else if (currentAccounts.length >= 1) {
                    account = { account: currentAccounts[0] };
                    success = true;
                }
            }
            catch (err) {
                console.log(err);
            }
            return success;
        }

        function aquireToken(msalInstance) {
            let success = false;
            const request = Object.assign(REQUEST_SCOPES, account);
            msalInstance.acquireTokenSilent(request).then(tokenResponse => {
                // Do something with the tokenResponse
                sessionStorage.setItem("DRAGONQUEST", tokenResponse.accessToken);
                testApi();
                success = true;
            }).catch(async (error) => {
                console.log(error);
            }).catch(error => {
                console.log(error);
            });
            return success;
        }

        function testApi() {
            const headers = new Headers();
            const token = sessionStorage.getItem('DRAGONQUEST');
            const bearer = `Bearer ${token}`;

            headers.append("Authorization", bearer);

            const options = {
                method: "GET",
                headers: headers
            };
//This is the call to the API hosting this html file it returns a 401
            fetch('api/test', options);
        }

        if (getAccounts(msalInstance) === false) {

            console.log('Calling microsoft');
            msalInstance.loginPopup(REQUEST_SCOPES)
                .then(function (loginResponse) {
                    //login success
                    console.log(loginResponse);
                }).catch(function (error) {
                    //login failure
                    console.log(error);
                });
        }
        else {
            aquireToken(msalInstance);
        }
    </script>
</body>
</html>

The Startup.cs

public class Startup
    {
        public IConfiguration Configuration { get; }
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddMicrosoftIdentityWebApi(Configuration);
            //services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddMicrosoftIdentityWebApi(Configuration.GetSection("AzureAd"));
            //services.AddMicrosoftIdentityWebApiAuthentication(Configuration, "AzureAd");


            services.AddControllers();
        }


        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            app.UseHttpsRedirection();

            app.UseStaticFiles();
            app.UseRouting();            
            app.UseDefaultFiles();
            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }

TestController.cs

[Route("api/test"), ApiController, Authorize]
    public class TestController : ControllerBase
    {
        [HttpGet]
        public IActionResult Get()
        {
            HttpContext.VerifyUserHasAnyAcceptedScope(ApplicationWideConstants.SCOPE_REQUIRED_BY_API);

            return Ok(User.Claims.Select(claim => new { claim.Type, claim.Value }));

        }
    }

@jrodrigoav

for what is worth, the token I get I can use to call https://graph.microsoft.com/v1.0/me using postman

  1. This is the issue. AzureAD does not give a token for several resources. you need to remove "user.read" from your scopes and have only "api://[SAME_AS_CLIENT_ID]/access_as_user" (otherwise you get a token for Microsoft Graph, not for your web API
  2. BTW this should be the clientID of your web API here (not of your SPA, unless they have the same clientID).

The following web API sample illustrates your scenario : https://github.com/Azure-Samples/active-directory-dotnet-native-aspnetcore-v2/tree/master/2.%20Web%20API%20now%20calls%20Microsoft%20Graph (but with a desktop app, instead of a SPA, but that's not the most difficult thing to change.

You might also be interested in these samples/tutorials:

cc: @kalyankrishna1 @derisen

Thank you, dropping kid to school and giving this a try.

And yes the ClientId is the same, the SPA and the webapi are the same project.

Yes that was it, thank you very much!

Was this page helpful?
0 / 5 - 0 ratings