4

I am trying to create a Web API server using ASP.NET Core 2.0 which uses azure ad v2 endpoint token authorization. I also have an Angular 2 app where the office365 login happens. I get a token from there and then send a simple request to an authorized action in the Web API server. However my token doesn't pass the authorization checks and I get a 401 Unauthorized response. The description provided is:

Bearer error="invalid_token", error_description="The signature key was not found"

I decoded the token and the decoder throws an invalid signature error as well. Here are the important parts of my code I use for configuration and token authorization:

Web API Server:

appsettings.json

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "ClientId": "my-registered-app-client-id",
  },
  "Logging": {
    "IncludeScopes": false,
    "Debug": {
      "LogLevel": {
        "Default": "Warning"
      }
    },
    "Console": {
      "LogLevel": {
        "Default": "Warning"
      }
    }
  }
}

AzureAdAuthenticationBuilderExtensions.cs

public static class AzureAdServiceCollectionExtensions
{
    public static AuthenticationBuilder AddAzureAdBearer(this AuthenticationBuilder builder)
        => builder.AddAzureAdBearer(_ => { });

    public static AuthenticationBuilder AddAzureAdBearer(this AuthenticationBuilder builder, Action<AzureAdOptions> configureOptions)
    {
        builder.Services.Configure(configureOptions);
        builder.Services.AddSingleton<IConfigureOptions<JwtBearerOptions>, ConfigureAzureOptions>();
        builder.AddJwtBearer();
        return builder;
    }

    private class ConfigureAzureOptions: IConfigureNamedOptions<JwtBearerOptions>
    {
        private readonly AzureAdOptions _azureOptions;

        public ConfigureAzureOptions(IOptions<AzureAdOptions> azureOptions)
        {
            _azureOptions = azureOptions.Value;
        }

        public void Configure(string name, JwtBearerOptions options)
        {
            options.Audience = _azureOptions.ClientId;
            options.Authority = $"{_azureOptions.Instance}common/v2.0";

            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = false,
            };
        }

        public void Configure(JwtBearerOptions options)
        {
            Configure(Options.DefaultName, options);
        }
    }
}

Startup.cs

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

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(sharedOptions =>
        {
            sharedOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddAzureAdBearer(options => Configuration.Bind("AzureAd", options));

        services.AddMvc();
        services.AddCors(options =>
        {
            options.AddPolicy("AllowAllOrigins",
             builder =>
             {
                 builder.AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin();
             });
        });
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        loggerFactory.AddConsole(Configuration.GetSection("Logging"));
        loggerFactory.AddDebug();

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseCors("AllowAllOrigins");

        app.UseAuthentication();
        app.UseMvc();
    }
}

Now here is the code I use to authenticate in my Angular2 app:

import { Injectable } from '@angular/core';
import { Headers } from '@angular/http';
import * as hello from 'hellojs/dist/hello.all.js';

import * as MicrosoftGraph from "@microsoft/microsoft-graph-types";
import * as MicrosoftGraphClient from "@microsoft/microsoft-graph-client";
import { Configs } from "../../../shared/configs"

@Injectable()
export class HttpService {
  url = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${Configs.appId}&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%2Fmyapp%2F&response_mode=query&scope=openid%20offline_access%20https%3A%2F%2Fgraph.microsoft.com%2Fmail.read&state=12345`;

  getAccessToken() {
    const msft = hello('msft').getAuthResponse();
    const accessToken = msft.access_token;
    return accessToken;
  }


  getClient(): MicrosoftGraphClient.Client
  {
    var client = MicrosoftGraphClient.Client.init({
      authProvider: (done) => {
          done(null, this.getAccessToken()); //first parameter takes an error if you can't get an access token
      },
      defaultVersion: 'v2.0'
    });
    return client;
  }
}

When a token is returned from the endpoint I send a request to a valid endpoint on my Web API server.

Important note: I am using the same AppId in both my Web API and Angular app because it is required by the AzureAd v2.0 endpoint.

My point is that I think I'm doing everything by the book but there is obviously something missing. If anyone could tell me what I did wrong in my configuration, I'd be immeasurably grateful!

aud property of decoded token is:

https://graph.microsoft.com

Abraxas
  • 99
  • 1
  • 12
  • Hmm, why are you using "MicrosoftGraphClient" on your front-end if you should be calling your API? Could you inspect the access token at e.g. https://jwt.ms and mention what is the value of the *aud* claim? – juunas Dec 24 '17 at 12:49
  • I sure will when I get back to my computer. But the main idea for the graph library on my front-end is to just do proper and secure authorization and get the token, which I am planning to use to authorize in my requests to my API. – Abraxas Dec 24 '17 at 13:38
  • Microsoft Graph is not related to authentication :) It is another API that you can call by getting a token for it from Azure AD. – juunas Dec 24 '17 at 14:03
  • 1
    The code using the MS Graph library in the Angular app is copied from one of the Azure AD example repositories in GitHub. I know that they are two separate things but wanted to be sure that I am not messing up any configuration so I decided to use their code for that part. – Abraxas Dec 24 '17 at 16:41
  • @juunas I added the aud claim of the decoded token at the bottom of the post. – Abraxas Dec 26 '17 at 21:04
  • @juunas are you still alive after the holidays mate? :D – Abraxas Dec 29 '17 at 10:39
  • Heh, sure am, thanks for the notification :) Yeah, that aud claim says the token is meant for the Microsoft Graph API, and thus your API would not accept it. – juunas Dec 29 '17 at 10:39
  • @juunas I tried using the msal library in the frontend instead of the ms graph library and the aud claim was still the same :(. Any idea what is the right way to get an access token for my application through angular? – Abraxas Dec 29 '17 at 10:43
  • Somewhere in your front-end app you must configure what you want to use the token for. In AAD v1, this was the `resource`. In v2, it is the `scope`. You need to make sure you use something like `api://25f66106-edd6-4724-ae6f-3a204cfd9f63/access_as_user` as the scope. You can find this identifier in your app's configuration at https://apps.dev.microsoft.com – juunas Dec 29 '17 at 11:13
  • @juunas I think we're making progress here. I did what you told me. I added web api to my registered app in [link](apps.dev.microsoft.com) and changed the scope to "api://xxxxxxxxxxxxxxxxxxx/access_as_user". Now the error I get when trying to get the token is: "AADSTS65005%3a The application 'App_Name' asked for scope 'openid' that doesn't exist on the resource. Contact the app vendor." – Abraxas Dec 29 '17 at 14:43
  • Hmm... Have you tried removing the other scopes from the URL? – juunas Dec 29 '17 at 14:45
  • @juunas Yes, I only have this in my scope and it doesn't work. – Abraxas Dec 29 '17 at 14:53
  • @juunas I changed the scope to "openid" only, and now I get a valid v2 token, according to jwt.ms. My aud is "APP_ID" and the iss is "https://login.microsoftonline.com/TENANT_ID/v2.0". It all looks good but when I send a request, I get "Invalid token" => "The issuer is invalid". – Abraxas Dec 30 '17 at 08:52
  • That's odd. Since you have issuer validation off, it should not happen.. – juunas Dec 30 '17 at 09:05
  • @juunas That was on me. I have changed the ValidateIssuer property to true. I changed the value back to false and it's all good. I get authorized. But only with the owner account. I tried to login with other accounts in the same organization but when I try to get the token the following error is thrown: "The cache contains multiple tokens satisfying the requirements.Call AcquireToken again providing more requirements like authority: multiple_matching_tokens_detected". – Abraxas Dec 30 '17 at 11:00
  • @juunas Nevermind, it's all working now. I had to specify different scope for the method acquireTokenSilent in order to get it all done. I used "User.Read" as scope. There was a problem with the admin consent though, but using acquireTokenRedirect solves it perfectly. I am really grateful for your help. How can I give you proper credit for it. Could you post somethig like: "See question's comment thread to find the answer" so I can mark it as an answer? – Abraxas Dec 30 '17 at 13:22
  • Good to hear :) I added an answer where I tried to summarize our discussion. Feel free to leave a comment if I missed something. – juunas Dec 30 '17 at 13:42
  • @Abraxas I am facing the exact issue as you been through. I am receiving "AADSTS65005%3a The application 'App_Name' asked for scope 'openid' that doesn't exist on the resource. Contact the app vendor." I have tried putting openid into the scope - didn't make any difference. Would you please share a bit more how you put "only openid" into the scope and it resolved for you? Would be a great help to me. Thanks in advance! – Moim Feb 05 '18 at 07:18
  • @Moim are you using msal? If you are, I don't quite remember how I got it working. I would recommend you to use only "User.Read" in the scope and then when trying to login with msal you should use loginRedirect(scope) and then you should get the Azure AD v2.0 token in the callback function. Let me know if it worked. – Abraxas Feb 05 '18 at 11:19

1 Answers1

5

After a not-so-short discussion in the comments the issue was resolved.

The key points from the discussion:

  • The access token contained an aud claim with the value of https://graph.microsoft.com, which means the token is meant for the Microsoft Graph API, not their API
  • A Web API needed to be registered at https://apps.dev.microsoft.com/, after which the app needed to ask for an access token using a scope similar to: api://25f66106-edd6-4724-ae6f-3a204cfd9f63/access_as_user

So make sure that the aud claim contains the client ID or app ID URI for your API. That means it is meant for your API.

The token also needs to contain the necessary scopes.

When asking for an access token from AAD, make sure you specify the correct scopes.

Also, if you are using the v1 endpoints, make sure to use ADAL, not MSAL. In v1 also instead of scope, you have to use resource, which must have a value set to either the client ID or app ID URI of the API.

juunas
  • 54,244
  • 13
  • 113
  • 149
  • Also it is quite important to choose the correct library. Adal.js for example points to the v1 endpoint whilst msal.js is the one you should use for the v2 endpoint. – Abraxas Dec 30 '17 at 14:51
  • @Abraxas , Hello, How to make sure that the aud claim contains the client ID or app ID URI for your API ? https://stackoverflow.com/questions/49933215/how-to-validate-microsoft-graph-api-jwt-access-token-and-secure-your-api – CKS Apr 20 '18 at 04:36