1
votes

I followed Microsofts article to implement my own issuer validation ("Customizing token validation" is the headline of the section).

This seemed to work with JWT-Tokens issued in an app-only context, but failed when the first call to my API was a JWT token issued through user delegation.

I found that this line of code is causing the problem:

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddMicrosoftIdentityWebApi(Configuration);
services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
{
  var existingOnTokenValidatedHandler = options.Events.OnTokenValidated;
  options.Events.OnTokenValidated = async context =>
  {
       await existingOnTokenValidatedHandler(context);
      // Your code to add extra configuration that will be executed after the current event implementation.
      options.TokenValidationParameters.ValidIssuers = new[] { /* list of valid issuers */ };
      options.TokenValidationParameters.ValidAudiences = new[] { /* list of valid audiences */};
  }
});

This is the original code from the link I posted above. I implemented my own issuer validation in the following way:

        services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
        {                
            var existingOnTokenValidatedHandler = options.Events.OnTokenValidated;
            options.TokenValidtionParameters.RoleClaimType = "roles";
            options.Events.OnTokenValidated = async context =>
            {
                await existingOnTokenValidatedHandler(context);
                options.Authority = "https://login.microsoftonline.com/common";
                var validTenants = FileTenantStore.Tenants.Select(x => x.AzureAdTenantId).ToList();
                options.TokenValidationParameters.ValidIssuers = GetValidIssuers(validTenants);
                options.TokenValidationParameters.IssuerValidator = ValidateIssuers;
            };
        });

I have a multi tenant app, so I need to let only some tenants pass and reject the most.

This solution behaves a little strange:

  • Calling the API with an App-Only token works always.
  • Calling the API with a Delegated token fails with the following error message and does not even jump into the callback:

Failed to validate the token. Microsoft.IdentityModel.Tokens.SecurityTokenInvalidIssuerException: IDW10303: Issuer: 'https://login.microsoftonline.com/{OUR_TENANT_ID}/v2.0', does not match any of the valid issuers provided for this application. at Microsoft.Identity.Web.Resource.AadIssuerValidator.Validate(String actualIssuer, SecurityToken securityToken, TokenValidationParameters validationParameters) at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateIssuer(String issuer, JwtSecurityToken jwtToken, TokenValidationParameters validationParameters) at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateTokenPayload(JwtSecurityToken jwtToken, TokenValidationParameters validationParameters) at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateToken(String token, TokenValidationParameters validationParameters, SecurityToken& validatedToken) at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.HandleAuthenticateAsync()

So in this case "OnTokenValidated" is never called.

  • Calling the API with an App-Only token on the first time, and then later with a delegated token works fine.

I could fix this problem with moving the lines in the "OnTokenValidated"-Callback one level above:

        services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
        {                
            var existingOnTokenValidatedHandler = options.Events.OnTokenValidated;

            options.TokenValidationParameters.RoleClaimType = "roles";
            var validTenants = FileTenantStore.Tenants.Select(x => x.AzureAdTenantId).ToList();
            options.TokenValidationParameters.ValidIssuers = GetValidIssuers(validTenants);
            options.TokenValidationParameters.IssuerValidator = ValidateIssuers;
            options.Events.OnTokenValidated = async context =>
            {                  
                await existingOnTokenValidatedHandler(context);
                options.Authority = "https://login.microsoftonline.com/common";

            };
        });

I could even remove the callback "OnTokenValidated" now, but this does not feel right, because of the Microsoft article that is giving clear instructions.
Can I do it like this, or is my solution a security problem?

1
What the returned by GetValidIssuers(validTenants)? Based on the error message, 'https://login.microsoftonline.com/{OUR_TENANT_ID}/v2.0', does not match any of the valid issuers returned by GetValidIssuers(validTenants). I guess that it returns the v1.0 version format: 'https://sts.windows.net/{TENANT_ID}'.Allen Wu
You are right. It returns an array with addresses like "login.microsoftonline.com{Tenant_id}/v2.0" and also "sts.windows.net{TENANT_ID}". Both.David Elsner
Can you take a JWT token and decode in jwt.ms to see what is the issuer (iss claim)? docs.microsoft.com/azure/active-directory/develop/…krishg
You got me wrong. I am a 100% sure the token is fine. The delegated token also works perfectly, after an application token was used. The question is: Why is OnTokenValidated not executed the first time my API is called with a user delegated JWT (iss is correct, I did this already)David Elsner

1 Answers

1
votes

If you are in netcore>3 could you use AddMicrosoftIdentityWebApiAuthentication extension method. Set subscribeToOpenIdConnectMiddlewareDiagnosticsEvents to true and enable debug logging to look at the event that terminate the check for your particular token.

Based on your code :

services.AddMicrosoftIdentityWebApiAuthentication(Configuration,
    jwtBearerScheme: JwtBearerDefaults.AuthenticationScheme,
    subscribeToJwtBearerMiddlewareDiagnosticsEvents: true);