.net core 2.2 multiple bearer token authentication schemes

2.3k Views Asked by At

I am currently trying to use 2 different bearer tokens in a .net core 2.2 app. I would like to use an Identity Server token and an Azure AD bearer token. According to Microsoft this is possible (https://learn.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme?view=aspnetcore-2.2) but I am having no success getting it working.

I have the Identity Server token as the "default" authentication followed by the AzureAD token as documented in the aforementioned link:

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(o =>
    {
        o.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateAudience = true,
            ValidateIssuer = true,
            ValidateLifetime = true,
            ClockSkew = ClockSkew
        };
        o.Audience = Audience;
        o.Authority = IdentityIssuer;
        o.RequireHttpsMetadata = true;
    })
    .AddJwtBearer("AzureAd",o =>
    {
        o.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
        };
        o.Audience = AudienceUri;
        o.Authority = Authority
    });

Identity Server tokens validate as expected; however Azure AD tokens do not. They appear to always hit the default Bearer token handler.

2

There are 2 best solutions below

5
On

Try with something like this (I have 2 auth schemes; one for AAD and another one for custom Bearer auth)

var url = new MongoUrl(mongoSettings.ConnectionString); // I'm using MONGODB as databse ..but you can choose what you want 

var client = new MongoClient(url);

var database = client.GetDatabase(url.DatabaseName);

services.AddIdentity<ApplicationUser, ApplicationRole>(options =>
            {
                options.Password.RequireDigit = true;
                options.Password.RequiredLength = 6;
                options.Password.RequireNonAlphanumeric = false;
                options.Password.RequireUppercase = true;
                options.Password.RequireLowercase = true;

                // Lockout settings
                options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
                options.Lockout.MaxFailedAccessAttempts = 0;

                // ApplicationUser settings
                options.User.RequireUniqueEmail = false;
                //options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@.-_";
            }).RegisterMongoStores<ApplicationUser, ApplicationRole>(

p => database.GetCollection<ApplicationUser>("AspNetUsers"),
p => database.GetCollection<ApplicationRole>("AspNetRoles"))
              .AddDefaultTokenProviders();

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); // => remove default claims

var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(appConfiguration.Key));

var tokenValidationParameters = new TokenValidationParameters
            {
                //RequireExpirationTime = true,
                //RequireSignedTokens = true,
                //ValidateIssuerSigningKey = true,
                IssuerSigningKey = signingKey,
                ValidateIssuer = false,
                ValidIssuer = appConfiguration.SiteUrl,
                ValidateAudience = false,
                ValidAudience = appConfiguration.SiteUrl,
                //ValidateLifetime = true,
                ClockSkew = TimeSpan.Zero
            };

services.AddAuthentication(options =>
            {
                //options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme
                options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddJwtBearer("AAD", options =>
              {
                  //options.Audience = appConfiguration.SiteUrl;

                  //options.ClaimsIssuer = appConfiguration.SiteUrl;
                  options.IncludeErrorDetails = true;
                  options.Authority = "https://sts.windows.net/800859e2-e8c3-4842-b31a-3b3727070cb6/v2.0";
                  options.Audience = "5e2ddaf2-2ed3-4829-bbe8-9aa127a754ef";
                  options.SaveToken = true;

                  options.Events = new JwtBearerEvents()
                  {
                      OnMessageReceived = context =>
                      {
                          if ((context.Request.Path.Value.StartsWith("/videohub")
                              //|| context.Request.Path.Value.StartsWith("/looney")
                              //|| context.Request.Path.Value.StartsWith("/usersdm")
                             )
                              && context.Request.Query.TryGetValue("token", out StringValues token)
                          )
                          {
                              context.Token = token;
                          }

                          return Task.CompletedTask;
                      },
                      OnAuthenticationFailed = context =>
                      {
                          //TODO:
                          return Task.FromResult(0);
                      },
                      OnTokenValidated = context =>
                      {
                          //At this point, the security token has been validated successfully and a ClaimsIdentity has been created
                          var claimsIdentity = (ClaimsIdentity)context.Principal.Identity;

                          //get username
                          var preferred_username = claimsIdentity.Claims.ToList().Where(c => c.Type == "preferred_username").Select(c => c.Value).FirstOrDefault();

                          var username = !string.IsNullOrEmpty(preferred_username) ? preferred_username : claimsIdentity.Claims.ToList().Where(c => c.Type == "upn").Select(c => c.Value).FirstOrDefault();

                          //add your custom claims here
                          var serviceProvider = services.BuildServiceProvider();
                          var userservice = serviceProvider.GetService<IUsersService>();

                          var us = userservice.Find(xx => xx.UserName == username);
                          if (us == null) return Task.FromResult(0);

                          // ADD SCHEMA (so we know which kind of token is .. from AZURE ACTIVE DIRECTORY .. OR CUSTOM)
                          // TO RETRIEVE THE SCHEMA ..--> //var result = User.Claims.Where(c=>c.Type=="schema").FirstOrDefault().Value;
                          claimsIdentity.AddClaim(new Claim("schema", "AAD"));

                          //GET ROLES FROM DB
                          if (us != null && us.Roles.Any())
                          {
                              //add THEM
                              us.Roles.ForEach(rr =>
                               {
                                   claimsIdentity.AddClaim(new Claim(ClaimsIdentity.DefaultRoleClaimType, rr.ToUpper()));
                               });
                          }
                          else
                          {
                              //OR ADD A DEFAULT ONE
                              claimsIdentity.AddClaim(new Claim(ClaimsIdentity.DefaultRoleClaimType, Constant.ROLES.Dipendente));
                          }

                          // add MONGDB Id as ClaimTypes.NameIdentifier
                          claimsIdentity.AddClaim(new Claim(ClaimTypes.NameIdentifier, us.Id));

                          return Task.FromResult(0);
                      }
                  };
              }).AddJwtBearer("CUSTOM", options =>
               {
                   //options.Audience = appConfiguration.SiteUrl;

                   //options.ClaimsIssuer = appConfiguration.SiteUrl;

                   options.TokenValidationParameters = tokenValidationParameters;

                   options.SaveToken = true;
                   options.Events = new JwtBearerEvents()
                   {

                       OnAuthenticationFailed = context =>
                       {
                           //TODO:
                           return Task.FromResult(0);
                       },
                       OnTokenValidated = context =>
                       {
                           //At this point, the security token has been validated successfully and a ClaimsIdentity has been created
                           var claimsIdentity = (ClaimsIdentity)context.Principal.Identity;

                           //add your custom claims here

                           // ADD SCHEMA (so we know which kind of token is .. from AZURE ACTIVE DIRECOTY .. OR CUSTOM)
                           claimsIdentity.AddClaim(new Claim("schema", "CUSTOM"));

                           return Task.FromResult(0);
                       }
                   };
               });

then in yours Controller mark class or methid as :

   [Route("api/[controller]")]
    [ApiController]
    [Authorize(AuthenticationSchemes = "AAD,CUSTOM")] //<-- yours schema
    public class AccountController : Controller
    {
 // ...
}

Hope it helps you!!

0
On

Possible things you could try:

1 Set up the default policy

services.AddAuthorization(options => { 
        options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme, "AzureAD")
           .RequireAuthenticatedUser()
           .Build();

2 On the OnAuthenticationFailed > under one of the jwtOptions.Events, add a condition if it's authenticated then complete the task and don't show the error. Sometimes the user is authenticated already but the error from one provider prevents the proper response

 if (arg.HttpContext.User.Identity.IsAuthenticated)
    {
       return Task.CompletedTask;
    }

3 If this doesn't work. There's a hack to check if it's authenticated. Add more conditions per scheme.

      app.Use(async (context, next) =>
            {
                if (!context.User.Identity.IsAuthenticated)
                {
                    var result = await context.AuthenticateAsync("AzureAD");
                    if (result?.Principal != null)
                    {
                        context.User = result.Principal;
                    }
                }

                await next.Invoke();
            });