Quantcast
Channel: The Reformed Programmer
Viewing all articles
Browse latest Browse all 108

Part 5: A better way to handle authorization – refreshing user’s claims

$
0
0

This article focuses on the ability to update a ASP.NET Core’s logged in user’s authorization as soon as any of the authorization classes in the database are changed – I refer to this as “refresh claims” (see “Setting the Scene” section for a longer explanation). This article was inspired by a number of people asking for this feature in my alternative ASP.NET Core feature/data authorization approach originally described in the first article in this series.

The original article is very popular, with lots of questions and comments. I therefore came back about six months after the first article to answer some of the more complex question by creating a new PermissionAccessControl2 example web application and the following three extra articles:

UPDATE: See my NDC Oslo 2019 talk which covers these three articles.

You can find the original articles at:

NOTE: You can see the “refresh claims” feature in action by cloning the PermissionAccessControl2 example web application and then running the PermissionAccessControl2 project. By default, it is set up to use in-memory databases seeded with demo data and the “refresh claims” feature. See the “refresh Claims” menu item.

TL;DR; – summary

  • Typically, when you log in to an ASP.NET Core web app the things you can do, known as authorization, is “frozen”, i.e. it is fixed for however long you stay logged in.
  • An alternative is to update a logged-in user’s authorization whenever the internal, database versions of the authorization is updated. I call this “refreshing claims” because authorization data is stored in a user’s Claims.
  • This article describes how I have added this “refresh claims” feature to my alternative feature/data authorization approach described in this series.
  • While the “refresh claims” code I show applies to my alternative feature/data authorization code the same approach can be applied to any standard ASP.NET Core Role or Claim authorization system.
  • There are some (small?) downside to adding this feature around complexity and performance, which I cover near the end of this article.
  • The code in this article can be found in the open-source PermissionAccessControl2 GitHub repo, which also includes a runnable example web application.

Setting the scene – why refresh the user’s claims?

If you are using the built-in ASP.NET Core’s Identity system, then when you log in you get a set of Role and Claims which defined what you can do – known as authorisation in ASP.NET. By default, once you are logged in your authorisation is fixed for however long you stay logged in. This means that any changed the internal authorisation setting, then you need to log out and log back in again before you inherit these new settings. Most systems work this way because it’s simple and covers most of the authentication/authorization requirements of standard web apps.

But there are situations where you need any change to authorization to be immediately applied to the user – what I call “refreshing claims”. For instance, in a high-security system like a bank you might want to be able revoke certain authentication features immediately from a logged-in user/users. Another use case would be where users can trial a paid-for feature, but once the trial period you want the feature to turn off immediately.

So, if you need refresh feature then how can you implement it? One approach would be to recalculate the user’s authorisation settings every time they access the system – that would work but would add a performance hit due to all the extra database accesses and recalculations required on every HTTP request. Another approach would be to revoke/time-out the authentication token or cookie and have the system recalculate the authentication token or cookie again.

In the next section I describe how I added the “refresh claims” to my feature authentication approach.

The architecture of my “refresh claims” feature

In the earlier articles I described a replacement authorization system which had the advantage over the ASP.NET Core’s Roles-based authorisation in that the Admin user can change all aspects of the user’s authorisation (with ASP.NET Core’s Roles-based authorisation you need to edit/redeploy the code to alter what controller methods a Role can access).

The figure below shows an abbreviated version of how the feature part of authorisation process which is run on login (see the Part3 article for a more in-depth explanation).

But to implement the “refresh claims” feature I need a way to alter the permissions while the user is logged in. My solution is to use an authorization cookie event that happens every HTTP request. This allows me to change the user’s authorisations at any time.

To make this work I set a “LastUpdated” time when any of the database classes that manages authorization are changed. This is then compared with the “LastUpdated” claim in the user’s Claims – see the diagram below which shows this process. Parts in blue bold text show what changes over time.

I’m going to describe the stages involved in this in the following order

  1. How to detect that the Roles/Permissions have changed.
  2. How to store the last time the Roles/Permissions changed.
  3. Linking the authorization code to the database cache.
  4. How to detect/update the user’s permissions Claim.
  5. How to tell your front-end that the Permissions have changed.

1. How to detect that the Roles/Permissions have changed.

A user’s Permissions could be out of date whenever the User Roles, the Role’s Permissions, or the User Modules are changed. To do this I add some detection code to the SaveChanges/ SaveChangesAsync methods in DbContext that manages those database classes, called ExtraAuthorizeDbContext.

NOTE: Putting the detection code inside the SaveChanges and SaveChangesAsync methods provides a robust solution because it doesn’t rely on the developer to adding code to all the services that changes the authorization database classes.

Here is the code in the SaveChanges method (link to ExtraAuthorizeDbContext class which contains this code)

// I only have to override these two versions of SaveChanges,
// as the other two SaveChanges versions call these 
public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
    if (ChangeTracker.Entries().Any(x => 
        (x.Entity is IChangeEffectsUser && x.State == EntityState.Modified) || 
              (x.Entity is IAddRemoveEffectsUser && 
                    (x.State == EntityState.Added || 
                     x.State == EntityState.Deleted)))
    {
        _authChange.AddOrUpdate(this);
    }
    return base.SaveChanges(acceptAllChangesOnSuccess);
}

The things to note are:

      • Lines 6 to 9: This is looking for changes that could affect an existing user. The table below shows what type of changes could affect a user’s current Permissions. There are three classes UserToRole, ModulesForUsers and RolesToPermissions which I decorate with the approriate interfaces to detect any changes that could effect a user’s Permissions.
      • Line ???: If a change is found it calls the AddOrUpdate method in the IAuthChange instance that is injected into the ExtraAuthorizeDbContext. I describe the AuthChange class in section 3.

    2. How to store the last time the Roles/Permissions changed.

    Once the SaveChanges have detected a change we need to store the time that change happens. This is done via a class called TimeStore which is shown below.

    public class TimeStore
    {
        [Key]
        [Required]
        [MaxLength(AuthChangesConsts.CacheKeyMaxSize)]
        public string Key { get; set; }
    
        public long LastUpdatedTicks { get; set; }
    }
    

    This is a Key/Value cache, where the Value is a long (Int64) containing the time as ticks when the item was changes. I did this way because I would use this same store to contain changes in any my hierarchical DataKeys (see Part4 article), which I don’t cover in this article.

    NOTE: In the Part4 article I describe a multi-tenant system which is hierarchical. In that case if I move a SubGroup (e.g. West Coast division) to a different parent, then the DataKey would change, along with all its “child” data. In this case you MUST refresh any logged-in user’s DataKey otherwise a logged-in user would have access to the wrong data. That is why I used a generalized TimeStore so that I could add a per-company “LastUpdated” value.

    I also add a the ITimeStore interface ExtraAuthorizationDbContext which the AuthChanges class (see next section) can use. The ITimeStore defines two methods:

    1. GetValueFromStore, which reads a value from the TimeStore
    2. AddUpdateValue, which adds or update the TimeStore

    You will see these being used in the next section.

    3. Linking the authorization code to the database cache.

    I created a small project called CommonCache which lives at the bottom of the solution structure, i.e. it doesn’t reference to any other project. This contains AuthChange class, which links between the database and the code handling the authorization.

    This AuthChange class provides a method that the authorization code can call to check if the user’s authorization Claims are out of date. And at the database end it creates the correct cache key/value when the database detects a change in the authorization database classes.

    Here is the AuthChange class code below (see this link to actual code).

    public class AuthChanges : IAuthChanges
    {
        public bool IsOutOfDateOrMissing(string cacheKey, 
            string ticksToCompareString, ITimeStore timeStore)
        {
            if (ticksToCompareString == null)
                //if there is no time claim then you do need to reset the claims
                return true;
    
            var ticksToCompare = long.Parse(ticksToCompareString);
            return IsOutOfDate(cacheKey, ticksToCompare, timeStore);
        }
    
        private bool IsOutOfDate(string cacheKey, 
            long ticksToCompare, ITimeStore timeStore)
        {
            var cachedTicks = timeStore.GetValueFromStore(cacheKey);
            if (cachedTicks == null)
                throw new ApplicationException(
                    $"You must seed the database with a cache value for the key {cacheKey}.");
    
            return ticksToCompare < cachedTicks;
        }
    
        public void AddOrUpdate(ITimeStore timeStore)
        {
            timeStore.AddUpdateValue(AuthChangesConsts.FeatureCacheKey, 
                 DateTime.UtcNow.Ticks);
        }
    }
    

    The things to note are:

    • Lines 3 to 12: The IsOutOfDateOrMissing method is called by the ValidateAsync method (described in the next section) uses to find out if the User’s claims need recalculating, i.e. it returns true if the User’s claims “LastUpdated” is missing, or it is earlier then the database “LastUpdated” time. You can see the cache read in line 17 inside the private method that does the time compare.
    • Lines 25 to 29: The AddOrUpdate method makes sure the ITimeStore has an entry under the key defined by FeatureCacheKey which has the current time in ticks. This is referred to as the database “LastUpdated” value.

    3. How to detect/update the user’s permissions Claim

    In the Part1 article I showed how you can add claims to the user at login time via the Authentication Cookie’s OnValidatePrincipal event, but these claims are “frozen”. However, this event is perfect for our “refresh claims” feature because the event happens on every HTTP request. So, in the new version 2 PermissionAccessControl2 code I have altered the code to add the “refresh claims” feature. Below is the new version of the ValidateAsync method, with comments on the key parts of the code at the bottom. (use this link to go to the actual AuthCookieValidate class which contains these methods)

    public async Task ValidateAsync(CookieValidatePrincipalContext context)
    {
        var extraContext = new ExtraAuthorizeDbContext(
            _extraAuthContextOptions, _authChanges);
        //now we set up the lazy values - I used Lazy for performance reasons
        var rtoPLazy = new Lazy<CalcAllowedPermissions>(() => 
            new CalcAllowedPermissions(extraContext));
        var dataKeyLazy = new Lazy<CalcDataKey>(() => 
            new CalcDataKey(extraContext));
    
        var newClaims = new List<Claim>();
        var originalClaims = context.Principal.Claims.ToList();
        if (originalClaims.All(x => 
            x.Type != PermissionConstants.PackedPermissionClaimType) ||
            _authChanges.IsOutOfDateOrMissing(AuthChangesConsts.FeatureCacheKey, 
                originalClaims.SingleOrDefault(x => 
                     x.Type == PermissionConstants.LastPermissionsUpdatedClaimType)?.Value,
                extraContext))
        {
            var userId = originalClaims.SingleOrDefault(x => 
                 x.Type == ClaimTypes.NameIdentifier)?.Value;
            newClaims.AddRange(await BuildFeatureClaimsAsync(userId, rtoPLazy.Value));
        }
    
        //… removed DataKey code as not relevant to this article
    
        if (newClaims.Any())
        {
            newClaims.AddRange(RemoveUpdatedClaimsFromOriginalClaims(
                  originalClaims, newClaims));
            var identity = new ClaimsIdentity(newClaims, "Cookie");
            var newPrincipal = new ClaimsPrincipal(identity);
            context.ReplacePrincipal(newPrincipal);
            context.ShouldRenew = true;             
        }
        extraContext.Dispose(); //be tidy and dispose the context.
    }
    
    private IEnumerable<Claim> RemoveUpdatedClaimsFromOriginalClaims(
        List<Claim> originalClaims, List<Claim> newClaims)
    {
        var newClaimTypes = newClaims.Select(x => x.Type);
        return originalClaims.Where(x => !newClaimTypes.Contains(x.Type));
    }
    
    private async Task<List<Claim>> BuildFeatureClaimsAsync(
        string userId, CalcAllowedPermissions rtoP)
    {
        var claims = new List<Claim>
        {
            new Claim(PermissionConstants.PackedPermissionClaimType, 
                 await rtoP.CalcPermissionsForUserAsync(userId)),
            new Claim(PermissionConstants.LastPermissionsUpdatedClaimType,
                 DateTime.UtcNow.Ticks.ToString())
        };
        return claims;
    }
    

    The things to note are:

    • Lines 13 to 18: This checks if the PackedPermissionClaimType Claim is missing, or that the LastPermissionsUpdatedClaimType Claim’s value is either out of date or missing. If either of these are true then it has to recalculate the user’s Permissions, which you can see in lines 19 to 23.
    • Lines 46 to 57: This adds the two claims needed: the PackedPermissionClaimType Claim with the user’s recalculated Permissions, and the LastPermissionsUpdatedClaimType Claim which is given the current time.

    5. How to tell your front-end that the Permissions have changed

    If you are using some form of front-end framework, like React.js, Angular.js, Vue.js etc. then you will use the Permissions in the front-end to select what buttons, links etc to show. In the Part3 article I showed a very simple API to get the Permissions names, but now we need to know when to update the local Permissions in your front end.

    My solution is to add a header to every HTTP return that gives you the “LastUpdated” time when the current user’s Permissions where updated. By saving this value in the JavaScript SessionStorage you can compare the time provided in the header with the last value you had – if they are different then you need to re-read the permissions for the current user.

    Its pretty easy to add a header, and here is the code inside the Configure method inside the Startup class in your ASP.NET Core project. Here is the code (with thanks to SO answer https://stackoverflow.com/a/48610119/1434764) and here is a link to the actual code in the startup class to see where it goes.

    //This should come AFTER the app.UseAuthentication() call
    if (Configuration["DemoSetup:UpdateCookieOnChange"] == "True")
    {
        app.Use((context, next) =>
        {
            var lastTimeUserPermissionsSet = context.User.Claims
                .SingleOrDefault(x => 
                     x.Type == PermissionConstants.LastPermissionsUpdatedClaimType)
                ?.Value;
            if (lastTimeUserPermissionsSet != null)
                context.Response.Headers["Last-Time-Users-Permissions-Updated"] 
                     = lastTimeUserPermissionsSet;
            return next.Invoke();
        });
    }
    

    The downsides of adding “refresh claims” feature

    While the “refresh claims” feature is useful it does have some downsides. Firstly it is a lot more complex than using the UserClaimsPrincipalFactory approach explained in the Part3 article. Complexity makes the application harder to understand and can be harder to refactor.

    Also, I only got the “refresh claims” feature to work for Cookie authentication, while the “frozen” implementation I showed in the Part3 article works with both Cookie or Token authentication. If you need a token solution then a good starting point is the https://www.blinkingcaret.com/ blog (you might find this article useful “Refresh Tokens in ASP.NET Core Web Api”).

    The other issue is performance. For every HTTP request a read of the TimeStore is required. Now that request is very small and only take about 750ns on my I7, 4GHz Windows development PC, but with lots of simultaneous users you would be loading up the database. But the good news is that using a database means it automatically works with multiple instances of a web application (known as scale-out).

    NOTE: I did try adding an ASP.NET Core Distributed Memory Cache to improve local performance, but because the OnValidatePrincipal event lives outside the dependency injection you end up with difference instances of the memory cache (took me a while to work that out!). You could add a cache like Redis because it relies on configuration rather than the same instance, but it does add another level of complexity.

    The other performance issue is it has to refresh EVERY logged in user, as it doesn’t have enough information to target the specific users that need an update. If you have thousands of concurrent users that will bring a higher-than-normal load on the application and the database. Overall recalculating the Permissions aren’t that onerous, but it may be worth changing any roles and permissions outside the site’s peak usage times.

    Overall, I would suggest you think hard as to whether you need the “refresh claims” feature. Most authentication systems don’t have “refresh claims” feature as standard, so remember the Yagni (“You Aren’t Gonna Need It”) rule. But if you do need it, then now you know one way to implement it!

    Conclusion

    This article has focused on one specific feature that readers of my first article felt was needed. I believe my solution to the “refresh claims” feature is robust, but there are some (small?) downsides which I have listed. You can find all the code in this article, and a runnable example application, in the GitHub repo PermissionAccessControl2.

    When I first developed the whole feature/data authorization approach for one of my clients we discussed whether we needed the “refresh claims” feature. They decided it wasn’t worth the effort and I think that was right decision for their application.

    But if your application/users need the refresh claims feature then you now have a fully worked out approach which will still work even on web apps that scale out, i.e. run multiple instances of the web app to give better scalability.

    Happy coding!

    PS. Have a look at Andrew Lock’s excellent series “Adding feature flags to an ASP.NET Core app” for another useful feature to add to your web app.

     

    If you have a ASP.NET Core or Entity Framework Core problem that you want help on then I am available as a freelance contractor. Please send me a contact request via my Contact page and we can talk some more on Skype.


Viewing all articles
Browse latest Browse all 108

Trending Articles