3

Scenario:

I have an angular5 client application, which uses hello.js to authenticate users using their office 365 credentials.

Client Code:

  hello.init({
      msft: {
        id: configuration.AppID,
        oauth: {
          version: 2,
          auth: 'https://login.microsoftonline.com/' + configuration.TenantID + '/oauth2/v2.0/authorize'
        },
        scope_delim: ' ',
        form: false
      },
    },
      { redirect_uri: configuration.redirecturl }
    );
  }


  login() {

    hello('msft').login({ scope: 'User.Read People.Read', display: 'popup' })
      .then((authData: any) => {  // console.log(authData);

        this.zone.run(() => {

          // get profile
}

A successful response is (Manipulated for security reasons)

{  
   "msft":{  
      "access_token":"REMOVED TOKEN HERE",
      "token_type":"Bearer",
      "expires_in":3599,
      "scope":"basic,User.Read,People.Read",
      "state":"",
      "session_state":"3b82898a-2b3f-445363f-89ae-d9696gg64ad3",
      "client_id":"672330148-2bb43-3080-9eee-1f46311f789c",
      "network":"msft",
      "display":"popup",
      "redirect_uri":"http://localhost:5653/",
      "expires":15245366.218
   }
}

The decoded access_token has these few keys:

Header:

1. nonce (requires some special processing, I couldn't find any documentation regarding special processing)

2. typ: JWT

PayLoad:

"aud": "https://graph.microsoft.com",

Once the access_token is received, I am sending the access_token in authorization header of every call to my backend API. The goal is to validate the token and only send a successful response if the access_token is validated and authorized. If unsuccessful, 401 Unauthorized is the response.

API Code to validate access_token, ASP .NET CORE 2, Following (https://auth0.com/blog/securing-asp-dot-net-core-2-applications-with-jwts/)

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

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void ConfigureServices(IServiceCollection services)
    {
      services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
          options.TokenValidationParameters = new TokenValidationParameters
          {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = Configuration["Jwt:Issuer"],
            ValidAudience = Configuration["Jwt:Issuer"],
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
          };
        });

      services.AddMvc();
    }
  }
}

// other methods
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseAuthentication();

    app.UseMvc();
}

In appsettings.json I have:

{   "Jwt": {
    "Key": "verySecretKey", **(I got the key from https://login.microsoftonline.com/common/discovery/keys with the kid value in access_token header)**
    "Issuer": "https://sts.windows.net/49bcf059-afa8-4bf9-8470-fad0c9cce27d/",   } }

Finally, the error I receive is : "WWW-Authenticate →Bearer error="invalid_token", error_description="The signature key was not found""

I have been stuck here since past few days, any help will be life savior.

Key Points:

  1. I tried to validate the access_token in jwt.io (https://nicksnettravels.builttoroam.com/post/2017/01/24/Verifying-Azure-Active-Directory-JWT-Tokens.aspx) but I was not able to.

  2. The aud here is https://graph.microsoft.com, I am not sure if I need to and why do I need to change aud to my client id. how do I do that?

  3. Is there something wrong in the code or do i need to tweak the way I am requesting header tokens.

Please let me know if you need more information.

juunas
  • 54,244
  • 13
  • 113
  • 149
CKS
  • 497
  • 3
  • 9
  • 16

3 Answers3

2

I tried to validate the access_token in jwt.io (https://nicksnettravels.builttoroam.com/post/2017/01/24/Verifying-Azure-Active-Directory-JWT-Tokens.aspx) but I was not able to.

Microsoft Graph API access tokens are signed differently from other access tokens from what I can see. You do not need to validate tokens that are meant for another API, it is their job.

The aud here is https://graph.microsoft.com, I am not sure if I need to and why do I need to change aud to my client id. how do I do that?

I don't know about HelloJS, but you should be able to get an Id token after authentication with response_type=id_token token. Then you need to attach that to the requests. It should have your client id as the audience.

Is there something wrong in the code or do i need to tweak the way I am requesting header tokens.

The only thing that stands out to me is that you are doing a lot of unnecessary configuration. Basically the configuration should be:

.AddJwtBearer(o =>
{
    o.Audience = "your-client-id";
    o.Authority = "https://login.microsoftonline.com/your-tenant-id/v2.0";
})

The handler will automatically fetch the public signing keys on startup. It's not really a good idea to hard-code signing keys in your app since your app will break when AAD finishes signing key rollover.

juunas
  • 54,244
  • 13
  • 113
  • 149
  • 1
    Hey @juunas, thank you for your response. so if Microsoft GRAPH API access tokens are meant to be validated by them, which flow should I follow to secure my backend API. I followed the authorization code grant protocol and I got an Id_token where the audience is my client_id but it has no information about the user signed in..Without user information, how do I validate the token & authorize the user? – CKS May 02 '18 at 15:23
  • Your backend API should not use authorization codes, it should use JWT Bearer tokens. You can get the user information from the access token sent to your API. – juunas May 02 '18 at 15:29
  • Yes, I am not using authorization code, I use the code to get the access token and ID token. (https://learn.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols-oauth-code). So do you mean I have to send two tokens (id_token and access token) to my backend API and validate ID_token and decode access token to get the user information? – CKS May 02 '18 at 15:48
  • The id token is technically not meant for your backend :) If you decode you can see it has a different audience (your frontend). – juunas May 02 '18 at 16:00
  • I am so confused, do you have any demo/tutorial that I can follow? Appreciate your help – CKS May 03 '18 at 18:48
  • Here is an Angular app with a back-end API: https://github.com/Azure-Samples/active-directory-angularjs-singlepageapp. Using the PowerShell script there to configure it in your tenant uses just one app registration for both parts though, so it is a bit different from your scenario. You can find more info on the token types here: https://learn.microsoft.com/en-us/azure/active-directory/develop/active-directory-token-and-claims – juunas May 04 '18 at 18:34
  • Your front-end Single Page App receives an Id token as a result of authentication at least. This is a token meant for the app which requested authentication, and tells who the user is. Your SPA can also acquire access tokens to call APIs. Those access tokens also contain limited set of info about the user + info about the app that acquired the access token. The access token should be sent along with requests to the API. It can then look at the token to know which application is calling it, which permissions it has, and also which user the application is impersonating. – juunas May 04 '18 at 18:37
  • If you used only a single app registration in Azure AD for both parts (so they have the same client id), then your front-end SPA would actually use the Id token instead of an access token to authenticate requests to the back-end. But if you use 2 app registrations, then you have to acquire an access token and use that when calling the back-end API. I prefer using 2 apps. – juunas May 04 '18 at 18:38
  • v2 endpoints provide only a subset of what v1 offers so be careful. Graph scopes will assume that aud. Using id_token for api access is okay but not strictly compliant - some js libraries do this. – davidcarr Oct 06 '18 at 18:06
  • Officially it's been said that you should not use the id token like that. One issue is that the API cannot exchange the id token for another access token if it needs to call APIs on behalf of the current user as well. – juunas Oct 07 '18 at 08:45
0

I also spent a lot of time trying to validate it, but the bottom line is that you can't:

Access tokens are opaque blobs of text that are for the resource only. If you're a client getting a token for Graph, assume that it's an encrypted string that you should never look at - sometimes it will be. We use a special token format for Graph that they know how to validate - you shouldn't be looking at access tokens if they're not for you. (source: https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/609)

Instead of using the access token, you should create an ID token, which is a regular JWT token that can be validated like any other JWT:

  1. Get the public key from the Microsoft directory
  2. Validate the signature, audience, issuer, etc.

To get an ID token using the MSAL API after login you can do (javascript example):

const { instance, accounts } = useMsal();
const request = {
    scopes: ["User.Read"],
    account: accounts[0]
};
const idToken = await instance.acquireTokenSilent(request).idToken;

For more information on ID Tokens, please check:

https://learn.microsoft.com/en-us/azure/active-directory/develop/id-tokens

For more information on opaque tokens, please check:

https://zitadel.com/blog/jwt-vs-opaque-tokens

Bruno Marotta
  • 443
  • 5
  • 12
-1

Yeah, this took a bit to work through. For anyone else researching this, here's my understanding.

You don't use the Microsoft Graph API to secure your web api. Instead:

  1. The client continues to use the Microsoft Identity Platform to authenticate.

  2. The client uses the resulting JWT access token to call the Web API as normal for OAuth 2.0 flow

  3. The web API uses JwtBearerAuthenticationScheme, setting the authority to the Microsoft identity platform. See this example and search for JwtBearerAuthenticationScheme.

  4. The web API uses the provided access token to obtain an 'On Behalf Of' user token.

  5. The web API calls the Graph API using this 'On Behalf Of' token. This token has a different lifespan than the token the client obtained, and refreshes must be handled separately.

This is a very distilled version of this example. Disclaimer: I haven't put this into practice yet.

Celeste
  • 135
  • 9