28

In my API project I am handling authentication with JwtBearer (users login using Azure). When the API is called the token is being validated with the defined Azure instance and this all works fine.

When a token is being validated successfully, the logged in user is being inserted in our own database with the proper roles. The way this is being handled now is as follow:

// Add authentication (Azure AD)
services
    .AddAuthentication(sharedOptions =>
    {
        sharedOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        sharedOptions.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; 
        sharedOptions.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; 
    })
    .AddJwtBearer(options =>
    {
        options.Audience = this.Configuration["AzureAd:ClientId"];
        options.Authority = $"{this.Configuration["AzureAd:Instance"]}{this.Configuration["AzureAd:TenantId"]}";

        options.Events = new JwtBearerEvents()
        {
            OnTokenValidated = context =>
            {
                // Check if the user has an OID claim
                if (!context.Principal.HasClaim(c => c.Type == "http://schemas.microsoft.com/identity/claims/objectidentifier"))
                {
                    context.Fail($"The claim 'oid' is not present in the token.");
                }

                ClaimsPrincipal userPrincipal = context.Principal;
                
                // Check is user exists, if not then insert the user in our own database
                CheckUser cu = new CheckUser(
                    context.HttpContext.RequestServices.GetRequiredService<DBContext>(),
                    context.HttpContext.RequestServices.GetRequiredService<UserManager<ApplicationUser>>(),
                    userPrincipal);

                cu.CreateUser();

                return Task.CompletedTask;
            },
        };
    });

This is working fine but it is not the most beautiful / proper way to do it. I would say I should use Dependency Injection / Overriding the OnTokenValidated event and integrate the 'CheckUser' logic there so the startup class stays uncluttered.

Sadly my knowledge about the DI is lacking and I am not entirely sure what the best way is to handle this properly. Therefore I looked a bit around and found a post which was exactly describing my problem:

Problems handling OnTokenValidated with a delegate assigned in startup.cs

After reading this post I tried to modify it a bit with my own logic, I ended up with the following:

In the Startup:

services.AddScoped<UserValidation>();

services
    .AddAuthentication(sharedOptions =>
    {
        sharedOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        sharedOptions.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        sharedOptions.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; 
    })
    .AddJwtBearer(options =>
    {
        options.Audience = this.Configuration["AzureAd:ClientId"];
        options.Authority = $"{this.Configuration["AzureAd:Instance"]}{this.Configuration["AzureAd:TenantId"]}";

        options.EventsType = typeof(UserValidation);
    });

The Custom JwtBearerEvents class:

public class UserValidation : JwtBearerEvents
{
    private string UserID { get; set; }

    private string UserEmail { get; set; }

    private string UserName { get; set; }

    public override async Task TokenValidated(TokenValidatedContext context)
    {
        try
        {
            TRSContext context2 = context.HttpContext.RequestServices.GetRequiredService<TRSContext>();
            UserManager<ApplicationUser> userManager = context.HttpContext.RequestServices.GetRequiredService<UserManager<ApplicationUser>>();

            ClaimsPrincipal userPrincipal = context.Principal;

            this.UserID = userPrincipal.Claims.First(c => c.Type == "http://schemas.microsoft.com/identity/claims/objectidentifier").Value;

            if (userPrincipal.HasClaim(c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"))
            {
                this.UserEmail = userPrincipal.Claims.First(c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress").Value;
            }

            if (userPrincipal.HasClaim(c => c.Type == "name"))
            {
                this.UserName = userPrincipal.Claims.First(c => c.Type == "name").Value;
            }

            var checkUser = userManager.FindByIdAsync(this.UserID).Result;
            if (checkUser == null)
            {
                checkUser = new ApplicationUser
                {
                    Id = this.UserID,
                    Email = this.UserEmail,
                    UserName = this.UserEmail,
                };

                var result = userManager.CreateAsync(checkUser).Result;

                // Assign Roles
                if (result.Succeeded)
                {
                    return;  
                }
                else
                {
                    throw new Exception(result.Errors.First().Description);
                }
            }
        }
        catch (Exception)
        {
            throw;
        }
    }
}

This is however not working for some reason. There is no error and UserValidation is never being hit (tried to set a debug point but it never hits) and it doesn't insert new users (it does when using the old code).

Anyone knows what I am doing wrong here or perhaps has some better ideas how to handle this?

Nicolas
  • 2,277
  • 5
  • 36
  • 82
  • Are you getting any errors? What exactly is the problem here? – Brad Jun 08 '18 at 09:11
  • My bad, forgot to put that in the main post. I am getting no errors, the Custom JWTBearerEvents class is never being hit (tried to set a debug point at the start of it but it never hits). I logged in with Azure under my account (which is not present in the database) so it should insert me, but nothing happens. I have edited the main post with the problem I am encountering. – Nicolas Jun 08 '18 at 09:50
  • Are you calling `app.UseAuthentication();` in `Startup.Configure()` method? – Brad Jun 08 '18 at 10:28
  • Yep: `public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseAuthentication(); app.UseMvc(); }` – Nicolas Jun 08 '18 at 10:40
  • 4
    I notice you're not awaiting anything in the `TokenValidated()` method but you mark it as `async`. – Brad Jun 08 '18 at 11:23
  • 1
    ... stupid by me. By accident left the async in my task.. after removing it and giving it a proper return it works.. thanks a lot! – Nicolas Jun 08 '18 at 11:48
  • 2
    Just a quick sidenote: do not remove the async keyword from the method, but instead remove the various .Result calls from the implementation, and instead await those. your code could otherwise suffer from unexpected deadlocks. – pcreyght Jan 16 '19 at 09:07
  • Did you ever find a solution to this? The suggested answer is missing out on certain cases impossible to cover with policies, such as `OnMessageReceived`. – Beltway Nov 01 '21 at 10:13

2 Answers2

1

Try configuring JwtBearerOptions this way:

services.AddAuthentication(options => { ... });

// or nowadays (with Microsoft identity platform) it is usually something like this: 
// services
//    .AddMicrosoftIdentityWebApiAuthentication(Configuration)
//    .EnableTokenAcquisitionToCallDownstreamApi()
//    .AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
//    .AddInMemoryTokenCaches();

services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
{
    // Hooking into the token validation event preserving the existing handler(s) if any
    options.Events ??= new JwtBearerEvents();
    var onTokenValidated = options.Events.OnTokenValidated;

    // Configure other token validation parameters if needed
    options.TokenValidationParameters.NameClaimType = "name";
    // options.TokenValidationParameters.Validate... = ...;

    options.Events.OnTokenValidated = async context =>
    {
        await onTokenValidated(context);

        if (context.Principal == null) context.Fail("No user");

        // returns ClaimsPrincipal as you might add some extra user claims
        // in YourGetOrCreteUserMethod to it from your user DB record
        context.Principal = await context.HttpContext.RequestServices
            .GetRequiredService<IYourUsersService>()
            .YourGetOrCreateUserMethod(context.Principal); 
    };

});

Related samples for using Microsoft identity platform for Authentication in Azure: How to secure a Web API built with ASP.NET Core using the Azure AD B2C

Dmitry Pavlov
  • 30,789
  • 8
  • 97
  • 121
0

I would suggest that you do basic token validation (things like Authority and Audience) in the startup as you have shown. I would suggest you use policy-based validation for specific claim validation. See Policy-based authorization in ASP.NET Core

The result will be code that is simpler, and easier to maintain.

GlennSills
  • 3,977
  • 26
  • 28