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.
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
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 the
CookieAuthenticationEvents class, which is part of the
Microsoft.AspNetCore.Authentication.Cookies namespace. Let's see how it works.
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
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.
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.
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
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
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.
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
Strict. More info on
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
My application doesn't include any cross site requests, or OAuth, so it safe to change the
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
"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
UserIsNotAuthenticatedOrNameClaimIsEmpty method is self explanatory, so let's move to the others.
UpdateUserClaimsIfModified is just running through all the cookie claims and if it detects a change it updates that claim, using the
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
- The database commander, which communicates with the underlying database.
- 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
IDisposable.Dispose methods. In
CreateLogger we return an instance of a logger class that implements the
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
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.
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
And indeed, after I hit Submit, it logs the sign-in event.
Let's try and modify logged-in user's email, to
firstname.lastname@example.org. 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).
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
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.
- ASP.NET Core 2.0 Cookie Authentication - Local logins
- ASP.NET Core 2.0 Authentication with local logins - Implementing claims transformation
- ASP.NET Core 2.0 Authentication with local logins - Responding to backend changes
- ASP.NET Core 2.0 Authentication with local logins - Implementing custom authorization policies
- ASP.NET Core 2.1 Authentication with social logins
- ASP.NET Core 2.0 Authentication with social logins - Implementing a profile store
- ASP.NET Core 2.0 Authentication with Azure Active Directory
- ASP.NET Core 2.0 Authentication with Azure Active Directory B2C
- ASP.NET Core 2.0 Authentication, IdentityServer4 and Angular
Comments powered by Disqus.