0
votes

Goal: Sign in Azure AD user on frontend with Microsoft login dialog. Append token to backend request. Validate token on backend to ensure only authorized users can access the code. Tricky part: validate by hand because its not the only authentication in place.

I've managed to login and send the token but on validation I get errors like: IDX10511: Signature validation failed. Keys tried: ...

Here is what I have so far:

app.module.ts

@NgModule({
  declarations: [
    AppComponent,
    RestrictedPageComponent
  ],
  imports: [
    HttpClientModule,
    MsalModule.forRoot({
      auth: {
        clientId: '<CLIENT ID>',
      }
    }, {
      consentScopes: [
        'user.read',
        'openid',
        'profile',
      ],
      protectedResourceMap: [
        ['https://localhost:44323/v1/login', ['user.read']], // frontend
        ['https://localhost:5001/api/Login', ['user.read']] // backend
      ]
    }),
    BrowserModule,
    AppRoutingModule
  ],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: MsalInterceptor,
      multi: true
    },
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

login.component.ts

  login() {
    const loginRequest = { scopes: ['https://graph.microsoft.com/User.ReadWrite'] };
    this.authService.loginPopup(loginRequest);
  }

And this seems to work just fine. I can login via Microsoft login and the MsalInterceptor adds the bearer token to the backend request header.

On the backend I now just want to verify that the token is valid and the user is authenticated corretly.

private JwtSecurityToken Validate(string token)
{
    var stsDiscoveryEndpoint = "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration";
    // also tried "https://login.microsoftonline.com/<TENANT ID>/v2.0/.well-known/openid-configuration"

    var openIdConnectConfigurationRetriever = new OpenIdConnectConfigurationRetriever();
    var configManager = new ConfigurationManager<OpenIdConnectConfiguration>(stsDiscoveryEndpoint, openIdConnectConfigurationRetriever);

    var config = configManager.GetConfigurationAsync().Result;

    var validationParameters = new TokenValidationParameters
    {
        IssuerSigningKeys = config.SigningKeys,

        // just for now
        ValidateAudience = false,
        ValidateIssuer = false,
        ValidateLifetime = false
    };

    var tokenHandler = new JwtSecurityTokenHandler();
    IdentityModelEventSource.ShowPII = true;

    token = token.Replace("Bearer ", string.Empty); // weird - the token starts with "Bearer " and is not valid like this
    var result = tokenHandler.ValidateToken(token, validationParameters, out var jwt);

    return jwt as JwtSecurityToken;
}

In the call of tokenHandler.ValidateToken(... I always get an error like IDX10511: Signature validation failed. Keys tried: .... I'm not sure anymore if I understood and use the whole concept correctly. The more I read about the usage of Azure Bearer token and validation the more it confuses me.

I can parse the token on http://jwt.io but the signature is always invalid.

Can the backend validate the token without passing any shared secret or client id? Is this the right approach to begin with?

edit: I'm not sure if I used the right endpoints to call on frontend and backend and which role the different endpoints play (e.g.: with tenant id or without). Would be great if someone could explain.

BR Matthias

1
See answer below. Only in addition: For manual token validation I had to change the stsDiscoveryEndpoint to https://login.microsoftonline.com/<TENANT ID>/v2.0/.well-known/openid-configurationMatthias

1 Answers

2
votes

Looks like your front-end is acquiring an access token for Microsoft Graph API. That token is only meant for MS Graph API, not your API. Those Graph API tokens are also special in the way they are built and you should not attempt to validate them.

Instead, in your front-end you need to specify scopes for your API.

      consentScopes: [
        'your-api-client-id-or-app-id-uri/user_impersonation',
        'openid',
        'profile',
      ],
      protectedResourceMap: [
        ['https://localhost:44323/v1/login', ['user.read']], // frontend
        ['https://localhost:5001/api/Login', ['your-api-client-id-or-app-id-uri/user_impersonation']] // backend
      ]

For this you will need to go to the app registration of your API in Azure AD, go to the Expose an API tab, and add a scope there. Then you take the full scope id (which includes your API client id or app ID URI + the scope id), and use that as the scope when acquiring tokens. Azure AD should then give you a token that is meant for your API.

If you are using ASP.NET Core, the minimal configuration that you need to validate Azure AD tokens is:

// In ConfigureServices
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(o =>
    {
        o.Authority = "https://login.microsoftonline.com/your-tenant-id";
        o.Audience = "your-app-client-id";
    });

// In Configure (between UseRouting and UseEndpoints)
app.UseAuthentication();
app.UseAuthorization();

Now in some cases if your API is configured to receive v1 tokens in Azure AD, the token could potentially contain either the API client id or app ID URI. In that case you can configure multiple valid audiences:

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(o =>
    {
        o.Authority = "https://login.microsoftonline.com/your-tenant-id";
        o.TokenValidationParameters = new TokenValidationParameters
        {
            ValidAudiences = new[]
            {
                "your-api-client-id",
                "your-api-app-id-uri"
            }
        };
    })