27

Advanced techniques around ASP.NET Core Users and their claims

 2 years ago
source link: https://www.thereformedprogrammer.net/advanced-techniques-around-asp-net-core-users-and-their-claims/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

This article describes some advanced techniques around adding or updating claims of users when building ASP.NET Core applications. These advanced techniques are listed below with examples taken from theAuthPermissions.AspNetCorelibrary / repo.

This article is part of the AuthPermissions.AspNetCore (shortened to AuthP in this article) documentation. Others articles about the AuthP library are:

TL;DR; – Summary of this article

  • A logged-in user in an ASP.NET Core application has a HttpContext.User that has information (stored in claims) about the logged-in user and, optionally, data on what pages / features the user can access (known as authorization).
  • The user and their claims are calculated on login and then stored in a Cookie or a JWT Bearer Token. This makes subsequent accessed to the application by that user fast, as the user’s claims are stored and can be read in quickly.
  • You can add extra claims when a user logs in, either to add extra authorization, or value(s) that take a long time to calculate but change infrequently. 
  • The ways you add extra claims on login depends on which of the authentication handlers APIs and which way you store the claims.
  • In certain situations, you might want to recalculate the claims of an all-ready logged in user and this article describes two approaches to do this – a periodical approach and an event-driven approach.

Setting the scene – What the AuthP library adds to ASP.NET Core?

The AuthP library provides three main extra authorization features to a ASP.NET Core application. They are:

  • An improved Role authorization system where the features a Role can access can be changed by an admin user (i.e. no need to edit and redeploy your application when a Role changes).
  • Provides features to create a multi-tenant database system, either using one-level tenant or multi-level tenant (hierarchical).
  • Implements a JWT refresh token feature to improve the security of using JWT Token in your application.

The rest of this article describes various ways to add or change the user’s claims in an ASP.NET Core application. Each one either improves the performance of your application or solves an issue around logged-in user claims being out of date.

1. Adding extra claims on to authentication handlers on login

The ASP.NET Core’s authentication handlers are there to ensure that the person who is logging in is verified as a known user. On a successful login it adds information, in the form of claims, about the user and what they can access – known as authorization. Together these form a ClaimsPrincipal class.

This ClaimsPrincipal class is stored in a form that the user to access the application without logging in again. In ASP.NET Core there are two main ways: in a Cookie or in a JWT Bearer Token (shorted to JWT Token). These work in a different way, but they do the same thing – that is provide a secure version of the user’s log-in data that will create a HttpContext.User on every HTTP request the user makes.

The brilliant part of storing the claims in a Cookie or JWT Token is they are super-fast – the claims are calculated on login, which make some time, but for subsequent HTTP requests the claims just read in from the Cookie or JWT Token.

The AuthP library adds extra authorization claims to the ClaimsPrincipal class on login. It does this by intercepting a login event if the claims are stored in Cookie, or for JWT Token it adds code within the building of the Token. AuthP relies onunique string that the authentication handler creates for each user, referred to as the userId. The library has a class called ClaimsCalculator which when called with the user’s userId it returns the extra AuthP claims needed for that user.

Currently, the AuthP library has built-in connectors to individual user accounts and Azure Active Directory (Azure AD) versions when using a Cookie to store the logged-in user’s information. But if you are using a JWT Bearer Token to store the logged-in user’s informationthen its easy to connect to the AuthP’s claims, which is described later.

ASP.Net Core has lots ofauthentication handlers and if you want to link intoauthentication handler’s login you need to know how to tap into their login event. Thankfully, most of the main authentication providers you might want to do use two external services APIs, OAuth2 and OpenIdConnect. The two sections look at how to link into these two APIs to add AuthP claims.

1a. OAuth2: Used by Google, Facebook, Twitter, etc.

The 0Auth2 API is an industry-standard protocol for authorization and is used for lots of external authentication providers and was released in 2012. ASP.NET Core documentation uses OAuth2 for  social logins like Google, Facebook, Twitter, but you can use OpenID Connect for these too (see this article about using OpenID Connect to use Google social login).  

To add extra claims on login, you need to link the OnCreatingTicket event of the ASP.NET Core authentication handler. The event call to your method provides a OAuthCreatingTicketContext parameter which provides the current tokens / claims (at the OAuth2 level they are represented by the AuthenticationToken class). See the code in this section on adding extra tokens /claims on login).

NOTE: that the OAuthCreatingTicketContext contains a HttpContext class, which allows you to use dependency injects to get an instance of the AuthP’s ClaimsCalculator to get the AuthP’s claims, e.g., ctx.HttpContext.RequestServices.GetRequiredService<IClaimsCalculator>();

1b. OpenID Connect:

OpenID Connect is based on OAuth2 and came out in 2014 to fix some issues in OAuth2. Its design is more secure, standardises the authentication steps, and fixes some issues found on OAuth2. ASP.NET Core documentation uses OpenID Connect to use a Microsoft account / Azure AD as a external authentication source.

NOTE: Andrew Lock has written an excellent article about OpenID Connect in ASP.NET Core which explains how OpenID Connect works and how it differs from OAuth2.

As I said earlier, the AuthP library has connector to Azure AD via a method called SetupOpenAzureAdOpenId, which is an excellent example of how to build an OpenID Connect connector. This method links code to OnTokenValidated event that allows you to add / change the logging in user. Just like the OAuth2 event call the parameter contains the HttpContext class, so you can use manual dependency injection to get access to AuthP’s ClaimsCalculator.

1c. Telling AuthP you have set up a custom authentication connector

The last thing you need to do is to add the ManualSetupOfAuthentication method to the registering of the AuthP library in your Program class (or Startup class in NET 3.1). 

2. Adding an extra Claim(s) to a user via AuthP

In the AuthP repo, the Example3 project is a single-level multi-tenant web application called the Invoice Manager. When a companies can sign up to use the Invoice Manage the banner changes to show the company’s name. Styling the application to customer like that improves the user’s experience, but the downside that every HTTP request has a database access to get the company’s name twice (one for redirecting to the correct page and another to add the header). That hits the performance of the overall application so, how could I improve that?

As already explained, claims are calculated when a user logs in and then stored in a Cookie or a JWT Bearer Token. This means that if I added the company’s name as a claim, then I will remove two database queries for every HTTP request.

Version 2.3.0 of the AuthP library has a RegisterAddClaimToUser<TClaimsAdder> method that will allow you to extra claims when a user logs in. The class you register with this method must implement the IClaimsAdder interface and it then registered with the ASP.NET Core dependency injection. When the AuthP’s ClaimsCalculator is called it will add the default AuthP’s default claims and all of the claims you added via the RegisterAddClaimToUser method.

The code below shows the code I used to find the tenant company name. NOTE: if the user isn’t a tenant user, then no claim is added.

public class AddTenantNameClaim : IClaimsAdder
{
public const string TenantNameClaimType = "TenantName";
private readonly IAuthUsersAdminService _userAdmin;
public AddTenantNameClaim(IAuthUsersAdminService userAdmin)
{
_userAdmin = userAdmin;
}
public async Task<Claim> AddClaimToUserAsync(string userId)
{
var user = (await _userAdmin.FindAuthUserByUserIdAsync(userId)).Result;
return user?.UserTenant?.TenantFullName == null
? null
: new Claim(TenantNameClaimType, user.UserTenant.TenantFullName);
}
}

A simple test of displaying the company home page many times gave me the following timings:

  1. Before adding the tenantName claim: 20 to 25 ms
  2. After adding the tenantName claim: 19 to 20 ms

That’s a ~3 ms / ~10% performance improvement for a small amount of extra code, so I think that’s a win. But what happens if the tenant’s (company’s) is changed? Read the next two approaches for ways to overcome this.

NOTE: You can see this approach in the Example3 ASP.NET Core project. If your clone the  AuthPermissions.AspNetCore repo and run the Example3.MvcWebApp project then it will seed the database with demo data on its first run and you can try this out.

3. Periodically refreshing the logged-in user’s claims

There are a few times where your want the user’s claims to be updated. The main one is if you change some of the user’s authorization parts, like Roles, or you change the Permissions in an AuthP Role, then these only get changed when the user logs out and logs back in again. This can have security issues, as you might have a logged-in users and you want their authorization revoked in some way.

My answer to this issue is to recalculate the user’s claims every so often – how often you update the claims of a logged-in user is a compromise between performance and claims being ‘correct’. If you recalculate every second, then for applications with lots of users will become slow, as recalculating the AuthP’s claims needs two database accesses to set up the Permissions and the DataKey. Alternatively, updating the company name isn’t a security issue, so you might want to wait say 10 minutes or more between recalculating the claims.

How you do this depends on whether you are using a Cookie or a JWT Bearer Token to hold the calculated user’s claims. The two subsections show you how to do this:

3a. Refreshing the claims in a Cookie

Refreshing a user’s authentication Cookie is available via the cookie OnValidatePrincipal event. Lots of authentication handlers use an authentication Cookies by default, but its sometimes its hard to find the Cookie events. The individual user accounts authentication handler is easy to set up – see the code below as to you configure, with the setup of the event highlighted.

services.AddDefaultIdentity<IdentityUser>(options =>
options.SignIn.RequireConfirmedAccount = false)
.AddEntityFrameworkStores<ApplicationDbContext>();
services.ConfigureApplicationCookie(options =>
{
options.Events.OnValidatePrincipal =
PeriodicCookieEvent.PeriodicRefreshUsersClaims;
});

Finding the way to link cookie events when using OpenID Context to use an external Azure AD was much harder find, but the code below shows how to set up the PeriodicRefreshUsersClaims method.

.AddMicrosoftIdentityWebApp(identityOptions =>
{
var section = _configuration.GetSection("AzureAd");
identityOptions.Instance = section["Instance"];
identityOptions.TenantId = section["TenantId"];
identityOptions.ClientId = section["ClientId"];
identityOptions.CallbackPath = section["CallbackPath"];
identityOptions.ClientSecret = section["ClientSecret"];
}, cookieOptions =>
cookieOptions.Events.OnValidatePrincipal =
PeriodicCookieEvent.PeriodicRefreshUsersClaims);

You also need to create a claim holding the time when the user should be updated. Here is some code that adds a claim called TimeToRefreshUserClaim, which contains a time one minute in the future.

public class AddRefreshEveryMinuteClaim : IClaimsAdder
{
public Task<Claim> AddClaimToUserAsync(string userId)
{
var claimValue = DateTime.UtcNow.AddMinutes(1).ToString("O");;
var claim = new Claim(TimeToRefreshUserClaimType, claimValue)
return Task.FromResult(claim);
}
}

This claim will be used by the method linked to the Cookie’s OnValidatePrincipal event in the PeriodicCookieEvent class, which is shown below. The highlighted lines compares the time in the TimeToRefreshUserClaim claim with the current time (UTC) and updates the user’s claims (including the TimeToRefreshUserClaim) if the user’s time is older than DateTime.UtcNow.

NOTE: I’m recommend a refresh of 1 minute. I only used short time as it’s easier to check it’s working.

public static async Task PeriodicRefreshUsersClaims
(CookieValidatePrincipalContext context)
{
var originalClaims = context.Principal.Claims.ToList();
if (originalClaims.GetClaimDateTimeUtcValue
(TimeToRefreshUserClaimType) < DateTime.UtcNow)
{
//Need to refresh the user's claims
var userId = originalClaims.GetUserIdFromClaims();
if (userId == null)
//this shouldn't happen, but best to return
return;
var claimsCalculator = context.HttpContext.RequestServices
.GetRequiredService<IClaimsCalculator>();
var newClaims = await claimsCalculator
.GetClaimsForAuthUserAsync(userId);
newClaims.AddRange(originalClaims
.RemoveUpdatedClaimsFromOriginalClaims(newClaims));
var identity = new ClaimsIdentity(newClaims, "Cookie");
var newPrincipal = new ClaimsPrincipal(identity);
context.ReplacePrincipal(newPrincipal);
context.ShouldRenew = true;
}
}
private static IEnumerable<Claim> RemoveUpdatedClaimsFromOriginalClaims
(this List<Claim> originalClaims, List<Claim> newClaims)
{
var newClaimTypes = newClaims.Select(x => x.Type);
return originalClaims.Where(x => !newClaimTypes.Contains(x.Type));
}

This method is very quick if the user doesn’t need refreshing – it calls DateTime.Parse of a string in an already loaded claim and compare to a time which takes a few microseconds. That’s important because it will be called on every HTTP request. If the claims do need refreshing there are a few database accesses which takes milliseconds, which is why there is a compromise between performance and claims being ‘correct’.

NOTE: The line context.ShouldRenew = true at the end of the PeriodicRefreshUsersClaims method. This makes sure that the Cookie is updated. If you don’t add this line, then new claims work in this HTTP request, but the claims aren’t changed for the next HTTP request.

NOTE: You can see this approach in the Example3 ASP.NET Core project. If your clone the  AuthPermissions.AspNetCore repo and run the Example3.MvcWebApp project then it will seed the database with demo data on its first run and you can try this out.

3b. Refreshing the claims in a JWT Bearer Token

The basic JWT Bearer Token (shortened to JWT Token) can’t be updated as once it is created you can’t change it until it times out – maybe 8 hours later. But this long life of the JWT Token creates a security issue as of a hacker can get a copy of the JWT Token they can assess the application too. Also, there is logout of an JWT Token which is another security issue too.

The solution to these JWT Token security issues is adding a refresh token and the AuthP library contains an implementation of the refresh token in ASP.NET Core. And this implementation can refresh the user’s claims, but first let’s look at how the refresh token works.

The diagram below shows that the JWT Token times out quickly, in this case every five minutes, but the refresh token provides the authority to create a new JWT Token.

JWTRefreshProcess.png

In this scheme the JWT Token can still be copied but its only valid for a small time. The refresh token also provides extra security because it can only be used once, and it can be revoked which will log out the user. The short life JWT Token time and the one-time use of the refresh token makes the use of a JWT Token much more secure. It also provides a way to periodically update the user’s claims.

The AuthP library provides a TokenBuilder class that can create just a JWT Token or a JWT Token with an associated refresh token. When creating the JWT Token it uses AuthP’s ClaimsCalculator class to add the AuthP’s claims. This means if you are using the JWT Token with refresh, then the user’s claims are updated on every refresh.

NOTE: For more information on JWT Token with refresh you can look at a video I created explaining how the JWT Token with refresh works,  or look at the AuthP’s JWT Token refresh explained documentation.

4. Refreshing the logged-in user’s claims on an event

The next challenge is when we need to immediately refresh the claims in a logged-in user in a way that doesn’t make every HTTP slow. This “immediately refresh” requirement is needed when using the hierarchical multi-tenant “Move Tenant” feature. This feature changes the DataKey of the moved tenants, which means the DataKey in users linked to a tenant that was moved need an immediate updated of their DataKey claim.

Implementing the immediately refresh the claims in a logged-in user requires three parts:

  1. A way to know if a logged-in user’s claims need updating.
  2. A way to tell ASP.NET Core to immediately refresh logged-in user’s DataKey claim
  3. A way to detect when a DataKey is changed

NOTE: This only works with Cookie authentication because you can’t force an immediate update a JWT token.

NOTE: You can see this approach in the Example4 ASP.NET Core project. Make sure to look at the RefreshUsersClaims folder in the Example4.ShopCode project for the various code shown in this approach.

4a. A way to know if a logged-in user’s claims need updating.

In the periodic refresh of the logged-in user’s claims a claim contained the time when it should be updated. With the refresh on an event, we need the opposite –the time when the logged-in user’s claims were last updated. The code below adds a claim that contains the time the claims were last created / updated.

public class AddGlobalChangeTimeClaim : IClaimsAdder
{
public Task<Claim> AddClaimToUserAsync(string userId)
{
var claimValue = DateTime.UtcNow.ToString("O");
var claim = new Claim(EntityChangeClaimType, claimValue)
return Task.FromResult(claim);
}
}

4b. Telling ASP.NET Core to immediately refresh user’s claims

We use the same cookie OnValidatePrincipal event that the periodic update of claims, but now we need the last time a DataKey was changed. If you only have one instance of your application you could use a static variable, but if you have multiple instances of your application running (Azure calls this Scale Out), then a static variable won’t work – you need a global resource that all the instances can see.

For the “multiple instances” case you could use the database, but that would need a database access on every HTTP request, which would hit performance. An in-memory distributed cache like Redis would be quicker, but I used a simpler global resource – the application’s FileStore. The GlobalFileStoreManager class in the AuthP repo implements a global FileStore, storing the value in a text file stored in the ASP.NET Core wwwRoot directory. The GlobalFileStoreManager is quicker than a database, only taking 0.02 ms. (on my dev machine) to read.

So, the OnValidatePrincipal event can use the GlobalFileStoreManager to read the last time a DataKey was updated, and it can compare that to the last time the logged-in user’s claims and update the claims if they are older.

The code below shows the event method in the TenantChangeCookieEvent class. This is almost the same as the periodic refresh event, with the changed lines highlighted.

public static async Task UpdateIfGlobalTimeChangedAsync
(CookieValidatePrincipalContext context)
{
var originalClaims = context.Principal.Claims.ToList();
var globalTimeService = context.HttpContext.RequestServices
.GetRequiredService<IGlobalChangeTimeService>();
var lastUpdateUtc = globalTimeService.GetGlobalChangeTimeUtc();
if (originalClaims.GetClaimDateTimeUtcValue
(EntityChangeClaimType) < lastUpdateUtc)
{
//Need to refresh the user's claims
var userId = originalClaims.GetUserIdFromClaims();
if (userId == null)
return;
var claimsCalculator = context.HttpContext.RequestServices
.GetRequiredService<IClaimsCalculator>();
var newClaims = await claimsCalculator
.GetClaimsForAuthUserAsync(userId);
newClaims.AddRange(originalClaims
.RemoveUpdatedClaimsFromOriginalClaims(newClaims));
var identity = new ClaimsIdentity(newClaims, "Cookie");
var newPrincipal = new ClaimsPrincipal(identity);
context.ReplacePrincipal(newPrincipal);
context.ShouldRenew = true;
}
}

4a. A way to detect when a DataKey is changed

The version 2.3.0 of the AuthP library introduces an IRegisterStateChangeEvent interface, which allows you to use EF Core 5’s events to detect a series of events. This lets you trigger code when different events happen within the AuthP’s DbContext.

The code below shows the code to register the specific event(s) you want to use. In this case I want to detect a change to the ParentDataKey property in the Tenant entity class, so we add a private event hander called RegisterDataKeyChange to the ChangeTracker.StateChanged event. Once that happens it uses a service, which in turn uses the GlobalFileStoreManager to set the global

public class RegisterTenantDataKeyChangeService
: IRegisterStateChangeEvent
{
private readonly IGlobalChangeTimeService _globalAccessor;
public RegisterTenantDataKeyChangeService(
IGlobalChangeTimeService globalAccessor)
{
_globalAccessor = globalAccessor;
}
public void RegisterEventHandlers(AuthPermissionsDbContext context)
{
context.ChangeTracker.StateChanged +=
RegisterDataKeyChange;
}
private void RegisterDataKeyChange(object sender,
EntityStateChangedEventArgs e)
{
if (e.Entry.Entity is Tenant
&& e.NewState == EntityState.Modified
&& e.Entry.OriginalValues[nameof(Tenant.ParentDataKey)] !=
e.Entry.CurrentValues[nameof(Tenant.ParentDataKey)])
{
//A tenant DataKey updated, so set the global value
_globalAccessor.SetGlobalChangeTimeToNowUtc();
}
}
}

This is very efficient because it’s only a DataKey change that causes the immediate update of the logged-in user’s claims.

NOTE: There is a very small possibility that a logged-in user could start a database access before the DataKey is changed, and database access is delayed by the “Move Tenant” transaction. In this case the user’s database access will use the old DataKey. If you want to ensure this cannot happen you could put your application into a mode where the user is diverted to a “please wait” page for a few seconds prior to the “Move Tenant”. I have an old article about how make your application “down for maintenance” for ASP.NET MVC5, but the same approach would work with ASP.NET Core

Conclusion

Most ASP.NET Core authentication handlers provide the basic claims but little or no authorization claims. A few, like individual user accounts with Roles-based authorization provide a full  authentication / authorization solution. The AuthP library adds authorization claims similar to the Roles-based authorization for accessing features / pages and also the database (i.e. multi-tenant applications).

As you have learnt in this article claims are the key part of authentication and authorization. They are also a fast, because they are calculated on login, but every HTTP request after that the user’s claims are read in from a Cookie or a JWT Token. If you have a value that takes time to create and use in ever HTTP request, then consider turning that value into a claim on login.

The downside of the claims being stored in a Cookie or a JWT Token is that is fixed throughout the time the user is logged in. In a few cases this is a problem, and this article gives two ways to overcome this problem while not causing a slowdown of your application.

Most of the techniques in this article are advanced, but each one has a valid use in real-world applications. You most likely won’t use these techniques very often, but if you need something like this then you now have some example code to use.

Happy coding.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK