Overriding OnTokenValidated JwtBearerEvents with Custom function .NET Core 2
Asked Answered
C

2

29

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?

Ciapas answered 8/6, 2018 at 8:28 Comment(8)
Are you getting any errors? What exactly is the problem here?Thulium
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.Ciapas
Are you calling app.UseAuthentication(); in Startup.Configure() method?Thulium
Yep: public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseAuthentication(); app.UseMvc(); }Ciapas
I notice you're not awaiting anything in the TokenValidated() method but you mark it as async.Thulium
... 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!Ciapas
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.Hepatic
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.Ululant
F
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

Franfranc answered 7/2, 2023 at 12:59 Comment(0)
H
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.

Higgins answered 18/2, 2021 at 14:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.