Overriding OnTokenValidated JwtBearerEvents with Custom function .NET Core 2

Nicolas picture Nicolas · Jun 8, 2018 · Viewed 15k times · Source

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?

Answer

GlennSills picture GlennSills · Feb 18, 2021

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.