Home ASP.NET Core 2.0 Authentication with local logins - Responding to backend changes
Post
Cancel

ASP.NET Core 2.0 Authentication with local logins - Responding to backend changes

ASP.NET Core 2.0 makes it very easy and straightforward to setup a cookie authentication mechanism in your application. Framework provides numerous ways to achieve that, with or without ASP.NET Core Identity membership system.

This post is part of a series on ASP.NET Core 2.0 Authentication and I am about to talk about cookie events and how to respond to them.

Application

In this demo, I am going to talk about cookie events in ASP.NET Core 2.0. The application in question is based on previous post of this series, ASP.NET Core 2.0 Authentication with local logins - Implementing claims transformation, so if you haven't followed along, it would be better to check this out first before diving into code that's coming next.

Code for this demo can be found here.

Very little has changed, the most significant change (aside from this post focus which is cookie events) is the database schema and data, so you might want to update accordingly by running the setup.cmd batch script that is found within the application source code.

You might notice there is a new table, the [dbo].[Log], which will be used for logging purposes.

Handling back-end volatility

In the previous post, I talked about enhancing an authenticated Identity with additional claims, utilizing a user profile store and the IClaimsTransformation interface.
This service responds to changes that happen to these additional claims, as it is executed for each request, as long as the user is authenticated.

Required claims usually are set during the cookie creation, that is when the user logs in, and most of the times, these are identity values that you define as the most important for your application to function as expected. Without them you might end up with all kinds of trouble, minor or major, doesn't matter as long as it is trouble.
How your application should react when these identity values have been modified in the back-end? Or even worse, how your application should react when the user in question is deleted from the back-end store?
The latter spells a serious security hole, as the user's cookie still remains in the browser session until it expires after a specified amount time. That means this user will continue to have access to the application, even though technically he does not exist anymore!

That said, we need to find a way to respond to these back-end changes, by either updating claims whose value has changed or reject the principal (destroy the cookie) if user does not exist anymore.
Thankfully, ASP.NET Core provides a way to tackle this by theCookieAuthenticationEvents class, which is part of theMicrosoft.AspNetCore.Authentication.Cookies namespace. Let's see how it works.

Implementation

To start listening to events you should setup cookie authentication in Startup.cs.
There you can define either some inline delegate handlers or a class which derives from CookieAuthenticationEvents.

I will focus on the OOP approach rather delegates in this post, though it is really easy to setup an inline delegate, you just need to use the Events property on the CookieAuthenticationOptions class, which is passed as an input in the AddCookie method overload.

Events property, which is a type of CookieAuthenticationEvents, supports more event handlers which are the following:

  • OnRedirectToAccessDenied: Event is invoked when application is redirecting to access denied screen.
  • OnRedirectToLogin: Event is invoked when a redirect to login page occurs.
  • OnSigningOut: This event is invoked before the user signs out.
  • OnSignedIn: This event is invoked when the user has signed in.
  • OnSigningIn: This event is invoked before the user signs in.
  • OnValidatePrincipal: This event is invoked on each request for an authenticated user and can be used to overwrite the existing cookie identity.
  • OnRedirectToLogout: This event is invoked when a redirect to logout page occurs.
  • OnRedirectToReturnUrl: This event is invoked when a redirect to the return URL that is specified occurs.

And the official documentation's definition for this property is:

The handler calls methods on the provider that give the app control on certain processing points. If events aren't provided, a default instance is supplied that does nothing when the methods are called.

For more info, check out ASP.NET Security repository implementation of CookieAuthenticationEvents.

OOP approach

An OOP approach is recommended when you wish to inject and work with other dependencies, for instance certain service implementations.

To register a class that handles the cookie events mentioned earlier, you need to assign it to the EventsType property of CookieAuthenticationOptions class in cookie setup. This property as it name states, takes a type, which must be a type of CookieAuthenticationEvents. Let's see its definition by the official documentation:

Used as the service type to get the Events instance instead of the property

That said, let's take a look at the following code snippet.

I have created a custom CookieEvents class, which inherits from CookieAuthenticationEvents. First, I register this class on the container, in this scenario I added this as scoped. Registering this class as scoped makes sense, as this is going to be invoked for each request.
After some standard cookie authentication setup, I assign the result of typeof(CookieEvents) to the EventsType property. Presto, we are done with the configuration, let's implement this class.

CookieEvents

This class inherits from CookieAuthenticationEvents, which contains virtual method handlers for all the events mentioned earlier. I have defined as dependencies the IUserRepository, which contains methods to fetch a user by his username and the ILogger service to log messages to the database and more specifically to the [dbo].[Log] table.
Following snippet shows the class declaration and the constructor implementation. I will explore this class method by method until I reach to the ValidatePrincipal method, which is the main point of interest.

More info on how to setup a custom database logger in ASP.NET coming later. For now I will focus on the virtual methods I've overridden and these are SignedIn, SigningIn and ValidatePrincipal methods.

SignedIn
In this method I just log an information message to the database on the user that has signed in. This handler is invoked when a user has successfully signed in.

SigningIn

This method is called when a user attempts to sign in. Looking at its context passed, the CookieSigningInContext, it doesn't let you do much, as it is invoked early in the request lifecycle, at best its a good place to modify some cookie options before signing the user in. That's what I am doing in the following code, I change the default SameSiteMode from Lax to Strict. More info on SameSiteMode:

Indicates whether the browser should allow the cookie to be attached to same-site requests only (SameSiteMode.Strict) or cross site requests using safe HTTP methods and same-site requests (SameSiteMode.Lax). When you set the SameSiteMode.None the cookie header value isn't set. The cookie policy middleware might overwrite the value that your provide. To support OAuth authentication, the default value is SameSite.Lax.

My application doesn't include any cross site requests, or OAuth, so it safe to change the SameSiteMode to Strict.

Next up, is the ValidatePrincipal method (in ASP.NET Core 1.x it was ValidateAsync, in 2.0+ it is ValidatePrincipal). As mentioned earlier, this method intercepts each authenticated request, and judging by its context, it does let you access the current HttpContext fully, as well as the current authenticated ClaimsPrincipal. Through this method I am able to modify the user's claims and his/her principal or even reject the principal and essentially throw him/her out. Take a look at the implementation below and I will come back and discuss its bits and pieces.

Pretty much standard implementation, at first I check if the user is indeed authenticated and the Name claim is populated, because in order to fetch his additional claims, I need the unique username stored in the Name claim.

Now, the entire process of updating the ClaimsPrincipal if there are any back-end changes is wrapped within a try..catch block, whereas in the case of an exception, I log the exception thrown, into the database and then reject the principal entirely, which logs the user out. Of course, you might not want that in your case, you might still want the user logged in, though for demo purposes, I'd like to log him out in case of error.

So, I am using the IUserRepository to find a user by a username and in case I don't find him, it means that he has been deleted from the database, so I immediately reject the principal, again, signing him out. But in the case I find the user, then I run a series of tests to identify the change, update the the claims and user's cookie.
I have a convention in the [dbo].[User] table, when an update occurs, the UpdatedOn field is updated with the current date and time. I've added a custom claim in the cookie with the arbitrary ClaimType of "UpdatedOn" which is a DateTime value. Check out the LoginAsync method in AuthController, it is changed a bit to include the UpdatedOn claim.

So, if an update occurs for a particular user, the UpdatedOn field will be invalidated with a new DateTime value, which is exactly what I am testing here, I am testing the claim value stored in the cookie against the database value. If the value fetched from the database is a future DateTime, then it means something was changed, so better proceed into updating the cookie's identity values and user's current principal (his cookie).

By updating the user's claims, I then proceed into creating a new ClaimsIdentity instance, of course based on the existing user claims and authentication type for that identity and then onto creating a new ClaimsPrincipal. In order to update the current principal, use the ReplacePrincipal method of the context and pass your new ClaimsPrincipal there. Be sure to set the ShouldRenew property of the context to true if you want to update the principal. Notice that we do not need to return anything, all the heavy lifting is done by the context passed.

The snippet below shows the private helper methods that are used by the ValidatePrincipal handler.
UserIsNotAuthenticatedOrNameClaimIsEmpty method is self explanatory, so let's move to the others.
The UpdateUserClaimsIfModified is just running through all the cookie claims and if it detects a change it updates that claim, using the UpdateClaimIfChanged method.

Also, something that you should know, is that you cannot access additional claims from this method. Let's say you have a IClaimsTransformation implementation in place and you want to access the claims generated by the profile store. Well, you cannot, as these are stored in the cookie per request and you won't be able to access them from the ValidatePrincipal method, as this runs first in the request lifecycle.

Bonus - ASP.NET Core 2.0 database logging

This is a bonus section, I will briefly explain about ASP.NET Core logging and how to make a database logging provider.

First, take a glance on the log table, which I store the log events.

And this is the EventLog model that I construct for logging an event in the database.

And now follows a snippet from the Startup.cs configuration class.
In order to implement logging in ASP.NET Core, you need an ILoggerFactory, which is an object that supports various kinds of logs and can be injected/configured in the Configure method. Having a logging factory, I can add various providers to it, build-in or custom. Then as your code requests for a certain logger, the logger factory will use the logger provider to create a new logger.

In the code below, you can see the high overview implementation, I register a custom ILoggerProvider, which is the DatabaseLoggerProvider to the ILoggerFactory, passing some dependencies, which are

  1. The database commander, which communicates with the underlying database.
  2. A filter function, which declares the permitted categories for the Logger to log. In this case, the category string will be the class from which the logger was called.

So, that said our first move would be to create LoggerProvider, which is a class that implements the ILoggerProvider interface, with CreateLogger and IDisposable.Dispose methods. In CreateLogger we return an instance of a logger class that implements the ILogger interface.
As you see, the provider is fairly simple, I just instantiate a new DatabaseLogger in the CreateLogger method, passing the categoryName and the filter that I declared earlier in the Configure method.

The ILogger interface contains three methods to implement

  • IDisposable BeginScope<TState>(TState state). This method provides the logger the ability to generate a disposable scope, essentially adding some context to the log events of that logger. Check out this post for more info on BeginScope semantics.
  • bool IsEnabled(LogLevel level). This method takes a LogLevel and tells the caller whether that LogLevel satisfies this specific logger. Of course, you can have a more fine-grained control on under which circumstances you enable this logger.
  • void Log<TState>(...). This method has a fairly lengthy signature, so I omitted it. This is where the logging actually happens.

In order for the logger to log to the database, it first needs to know if it's allowed to do so, by calling the IsEnabled method, where I filter the category based on the categories that I allow to log. Remember, category is going to be the full class name in which a call to the logger was issued.
Then, I construct the EventLog object and log it into the database, calling the Execute method of the Syrx commander.

Action!

Let's see all these in action. I run the application, which opens a new browser window.

Let's try to login. This will create a new log entry in the [dbo].[Log] table.

And indeed, after I hit Submit, it logs the sign-in event.

Let's try and modify logged-in user's email, to john.doe@email.com. First, I will visit the /profile page to verify the user's claims. Email address is the last claim in the list.

I will update the UpdatedOn field as well to the current date.

If I reload the page, I will see the email address updated.

I will try to remove that user now. I expect me to automatically sign-out after a refresh. First, I will delete the user from the database.

And now I will try to hit F5 and I should redirect to the login page (/profile is protected, that's why I am send back to login. If I try to navigate somewhere else, for instance /home, I will just see that I am no longer signed-in).

Summary

In this post we saw how it is possible to respond to back-end changes by either updating the values that have changed or by signing-out the user in question, essentially destroying the associated cookie, thus avoiding security holes, like users that shouldn't exist, still lurking around, all this thanks to CookieAuthenticationEvents class.

Github repository is here.

In coming posts, I will continue with local logins, talking about authorization policies and then move to social logins with various providers.


This post is part of the ASP.NET Core 2.0 Authentication series.

  1. ASP.NET Core 2.0 Cookie Authentication - Local logins
  2. ASP.NET Core 2.0 Authentication with local logins - Implementing claims transformation
  3. ASP.NET Core 2.0 Authentication with local logins - Responding to backend changes
  4. ASP.NET Core 2.0 Authentication with local logins - Implementing custom authorization policies
  5. ASP.NET Core 2.1 Authentication with social logins
  6. ASP.NET Core 2.0 Authentication with social logins - Implementing a profile store
  7. ASP.NET Core 2.0 Authentication with Azure Active Directory
  8. ASP.NET Core 2.0 Authentication with Azure Active Directory B2C
  9. ASP.NET Core 2.0 Authentication, IdentityServer4 and Angular
This post is licensed under CC BY 4.0 by the author.

ASP.NET Core 2.0 Authentication with local logins - Implementing claims transformation

ASP.NET Core 2.0 Authentication with local logins - Implementing custom authorization policies

Comments powered by Disqus.