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?
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.