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 Cookie Authentication without ASP.NET Core Identity. I will show how to setup, login and logout using local logins, a custom implementation of a membership system. In the coming examples I will show how to simply secure your ASP.NET Core 2.0 MVC application using cookies and in-memory stored users, though they could be stored anywhere, database, flat files, etc.
In coming blog posts I am going to show a more appropriate way to do the local login authentication, using a database, signing-up users, transforming their claims, etc., but first let's see something quick and simple.
Application & Cookie authentication
A very common security authentication technique would be the cookies authentication without doubt. It is used to secure web applications long enough to trust and use it in enterprise.
ASP.NET Core 2.0 provides various ways of implementing cookie-based authentication in our applications, with or without ASP.NET Core Identity. Here, we are going to discuss on using cookie authentication as standalone authentication provider in an ASP.NET Core MVC application.
What are we going to build here exactly and which are the resources that need to be secured?
Well, in this example, we have an MVC application, which has two pages. One is Home and the other is Profile. The latter is the protected resource, whereas Home can be accessed by anonymous users.
Profile page will enumerate throughout authenticated user's claims.
If a user is anonymous and tries to access this page, he/she will be challenged to enter their credentials in a login screen which they are going to be redirected to.
Of course, users can logout from the application and enter in anonymous state again.
We'll need some sort of a user store. In this example, I am going to create an in-memory, pre-populated user store and a service to validate user's credentials.
Code repository can be found on Github.
Let's start with that, but before we start, make sure to create a new empty Web Application, targeting ASP.NET Core 2.0.
Then install the Microsoft.AspNetCore.All
nuget package.
Local logins
Now that the environment is set, let's go ahead and create a User model and a service to validate user's credentials.
Under the root of the application, I've added a folder called Models and there, I've created a new class named User.
Pretty much standard here, our User has an integer as a unique identifier, a user name which is registered with and a password and also some personal details like his first, last name and date of birth. Let's continue with the service now,
First, I create a new IUserService
interface with only one method, ValidateUserCredentialsAsync
, to validate user credentials.
And here is the implementation
So, this service here is expecting a dictionary of string and user. Key is the user name of the user, whereas value is the User object itself. Pretty simple implementation for a simple example.
Now, ValidateUserCredentialsAsync
job is to validate if that user is validated based on the username and password input.
Note that this is not a recommended way to validate users credentials, with plain string passwords moving around, this is just an example and it shouldn't be implemented in production no matter what.
So, first we check if that user exists in our store, that is _users.ContainsKey(username)
and if that user exists, then we compare the password input with User's password. If they match, isValid
is true else it's false. Then I create a new Tuple
and I assign it to the result
variable, containing the isValid
value and the user, if isValid
is true, else a null value.
Lastly, I return a Task
, which is required from the method's signature.
Now that we have the models and services ready, it's time to configure the application, but first, let me go and create the in-memory store and register it on the DI along with the IUserService
.
Next step, is to open the Startup
class and add the in-memory store, so I create a new dictionary of string
and User
, populating it with a new User
and then registering it on the DI
And the registration in ConfigureServices
method
Done, let's move now on configuring MVC and cookies authentication.
Configuring ASP.NET Core 2.0 application to use cookie authentication
In order to add cookie authentication in the pipeline, first you need to register the authentication middleware in ConfigureServices
method, using the AddAuthentication
extension on services.
As you see, first I add the authentication middleware in services and then I use the AddCookie
method to declare that I would like cookie authentication to be enabled.
This extension comes with the Microsoft.AspNetCore.Authentication
nuget package.
What do the options passed mean?
For AddAuthentication
, I use the following options in order to configure the authentication scheme.
DefaultAuthenticateScheme
is the default scheme to use when a user authenticates. So I set the default scheme to cookie authentication (CookieAuthenticationDefaults.AuthenticationScheme
value is "cookie")DefaultSignInScheme
is the default scheme to use when a user signs in. In this case we use cookies to sign in a user.- The
DefaultChallengeScheme
is the default challenge to pose to an unauthenticated user, a401
response. In this case we use cookie authentication scheme, so when aChallenge
is send back to a user, he/she will be redirected to the login path where the cookie configuration has been set up. Check out theoptions.LoginPath
inAddCookie
For AddCookie
, I have defined two options
LoginPath
is the path where the user is redirected to on the cookie authentication challenge. Default value is/Account/Login
. In this case I've set it up to/auth/login
LogoutPath
is set the same way, providing the path where the logout action lives in this application
Of course, by setting all these up does not mean that I'm finished yet.
I also have to register the Mvc service into the services and then use those in the pipeline, something that is done in the Configure
method.
So, in order for this authentication to work, I need to call the UseAuthentication
method in Configure
, before the UseMvc
or UseMvcWithDefaultRoute
.
ConfigureServices
method
Configure
method.
Note that I call the UseAuthentication
after the UseStaticFiles
, because I am not interested into authenticating static files, but before the UseMvc
method which registers Mvc into the pipeline.
Setting up the protected Profile page
Next, let's set up the secured resource. I am going to create a view model for this page, which is going to show the user's name and his associated claims.
Create a new folder named Controllers and add a new subfolder called Home. Add a new controller called HomeController
.
I've added the ProfileViewModel
class under the Home folder, next to HomeController
And now add the Index
and Profile
actions in the controller.
Index is going to be allowed anonymous access, whereas Profile
is going to be decorated with an Authorize
attribute to prevent unauthorized access.
In Profile, I pass the ProfileViewModel
to the View, populating it with the username, which can be found in HttpContext.User.Identity.Name
and his claims, which can be found at HttpContext.User.Claims
. This code is executed only when a user is authenticated. Also note the [Authorize]
attribute decorating the method. This is used to secure this action from anonymous users trying to access it.
Note: Skip the following step if you have scaffolded the application with the Mvc template.
Go ahead and add the views now, create a new folder named Views and add two subfolders, Home and Shared.
Note: Copy the _ViewImports.cshtml
, _ViewStart.cshtml
and paste them under Views folder and for Shared folder, copy and paste the _Layout.cshtml
found in the Github repository, here.
Now, let's add the Index.cshtml
under the Home folder.
Very simple implementation of the home page, if the user is authenticated, then a greeting will show on screen.
Now create the Profile.cshtml
as well.
In this page, I show the user's name and enumerate through his claims creating a new bootstrap panel for each of them.
If you have been following along, running the application now, you will see the following screen
If I try to access the Profile page I will be redirected to the /auth/login
path, but this does not exist yet, so that's exactly what we are going to build now.
User login
Now it's time to build the login page.
Go to Controllers folder and create a new sub-folder named Authorization. Create a new controller there, called AuthController
. Let's give it a route path "auth"
and create a new login action.
The login action has a route path "login"
and it receives a returnUrl
as an optional parameter and for the View, it passes a LoginViewModel
through.
Create a new LoginViewModel
in the same folder with AuthController
:
Now it's time to create the Login action
In the code above, we are just rendering a view with a view model, populating the ReturnUrl
property of that model, which is not going to be shown in the actual page, rather be a hidden field.
That's all good, but we also need a view in order to show the login form to the user. Go to Views and create a new folder named Auth
and there, create a new View, named Login.cshtml
, having the following source code.
So, if the ModelState
is invalid, we show all validation errors in a bootstrap alert.
Below is the form, which makes a POST
request to the HTTP POST Login action. All we have is three inputs, one for the username, the other for the password and the last for the ReturnUrl
, which is a hidden field. Note that the asp-for
tag helper is binding the model's specific property to the view's input.
Last piece in the jigsaw is to add the HTTP POST login action. It is going to receive a LoginViewModel
, check its state and if it is valid, then it will go ahead and try to sign the user in.
First we check if the model is valid. If it is not, then we return the model back to the View to display the error messages.
Otherwise, we continue, by first validating the user, using the injected IUserService
. If you haven't already, inject the IUserService
in the AuthController
's constructor (see below). If the validation was not successful, then we add an error in the current ModelState
, indicating that the submitted credentials are invalid.
Otherwise, we go ahead and login the user in. This is done in the LoginAsync
method (see below). After it finishes signing, the user in, we finally check if the return URL is a valid (see below), relative URL and redirect there if that's the case, else we redirect the user back to the home page.
This method is responsible of signing a user in. First, we create the user's claims (in a real world scenario we would fetch the user's claims from a database). We create a NameIdentifier
claim, which is used to declare a unique identifier for the user. Then we create claims like Name
, which is the user's name, GivenName
, which I provide the value of FirstName
, Surname
whereas I provide the LastName
value and finally, DateOfBirth
claim which is a DateTime
claim with the date of birth value from the user.
Next, I construct the user's identity, which contains all the user's claims and I declare the authentication scheme this identity is for, which is the cookie authentication scheme. This is there to match the authentication scheme we provided when registered the Cookie Authentication through services. Please note that we didn't need to do that, as if no value is provided, the default will be picked.
After that, I create a new ClaimsPrincipal
, passing the newly constructed ClaimsIdentity
.
The magic happens in HttpContext.SignInAsync
method, which is signing-in a user. This method creates an encrypted cookie and adds it to the current response. This method takes an IPrincipal
, which is the ClaimsPrincipal
I created earlier. That way, the encrypted cookie will contain all the user's claims. Under the covers, this method populates the HttpContext.User
with the ClaimsPrincipal
we signed in, marking its identity as authenticated.
Also, some words on HttpContext.SignInAsync
from the official documentation
Under the covers, the encryption used is ASP.NET Core's Data Protection system. If you are hosting the app on multiple machines, load balancing across apps or using a web farm, then you must configure data protection to use the same key ring and app identifier.
Okay, now let's try to login. I will open the home page and click on the "Login"
link. This will bring me to the login page, whereas I will provide the user's credentials, which are "George"
for username and "1234"
for password (not a very secure password but anyways).
Clicking on submit redirects me back the home page, whereas I see a greeting message. That means I am logged in! Cool!
If I navigate to the Profile page, I will be able to access it now. I can see all of the user's claims, just like I set them up in code.
If you open Chrome's developer tools (F12) and go to Application tab and Cookies, you will see an .AspNetCore.Cookies
entry with an encrypted value. This is our cookie.
That's all good, now I am able to sign-in as a user. But how do I logout? I need to build a logout mechanism to do so.
User logout
As you have noticed already, in the navigation bar, there is a link to Login page and when a user is authenticated it changes to Logout.
This logout link is pointing to the Auth controller's Logout
action, which is exactly what we are going to build.
So, go to AuthController
and create a new action called Logout, giving it a route path of "logout"
.
I've also added an optional string for a returnUrl
which is going to be used only in the logout consent screen, if the user cancels logout.
Line 6 is checking for a configuration value. I've gone ahead and injected the ASP.NET Core's IConfiguration
interface into the controller in order to have access to the appsettings.json
. If the ShowLogoutPrompt
value is true, then we go ahead and show the logout consent screen. Otherwise, I just go ahead and logout the user without asking for his consent, redirecting him back to the home page.
Let's take a look on the configuration and the injected value into the controller's constructor
Now that we have the logout page ready, let's go ahead and create its view. Go to Auth subfolder of Views and create a new view with name Logout.cshtml
.
So here we are asking for the user's consent, if he/she is really sure that wants to logout. If yes, then a POST request will be issued to the logout endpoint (more on this below) which destroys the current session cookie.
Otherwise, the cancel button redirects the user back to where he came from, by using the returnUrl
path.
HTTP POST Logout action is very straightforward, we just sign the user out and then we redirect back to home page.
So, if the user is authenticated, then we go ahead and we sign him/she out by calling the HttpContext.SignOutAsync
method. Please note that we need to pass the target authentication scheme, the one that we used to sign the user in and we configured the authentication provider. Then we redirect back to home page. Let's see that in action.
We are still logged in as "George"
. Now it's time to logout. Click on the Logout link and see getting redirected back to home. The Logout link has changed to Login and if you try to access the Profile page, you will be redirected to the login page to provide credentials.
If you open the Chrome's developer tools (F12) and go to Application tab and Cookies, you will not see the .AspNetCore.Cookies
cookie any more.
Bonus
Cookie can be configured in many ways and it has many configuration properties for you to fiddle around. You can set configuration like the cookie domain, the expiration timespan, observe events and many more. These configuration options can be set on the AddCookie
method in Startup
, whereas a CookieAuthenticationOptions class is instantiated and passed in the method's delegate. Check that link for more information on how to configure the authentication cookie.
But also there are some configuration properties that can exist on the ClaimsPrincipal level, when you create the user's principal. You can instantiate a new AuthenticationProperties
class and pass some configuration there. This class is part of the Microsoft.AspNetCore.Authentication
namespace.
Let's take a peak into its internals
You can setup properties like a RedirectUri
, which is an absolute URI where the application will redirect the user when authenticated. Or you can setup properties like IsPersistent
, which makes a cookie persistent across browser sessions, meaning that you can close the browser and when reopen it, the cookie will still be there (unless it has expired). Other options are IssuedUtc
, which is a DateTimeOffset
to indicate the time the cookie was created, ExpiresUtc
sets the absolute time when the cookie expires. You must set the IsPersistent
to true in order for this to be honored. The AllowRefresh
configures the authentication session refreshing.
For example, if you wanted your cookie to be present when you reopen the browser you should set the IsPersistent
to true.
If you want the cookie to expire on a specific time, set the IsPersistent
to true and the ExpiresUtc
to a DateTimeOffset
. Then pass the AuthenticationProperties
instance to the HttpContext.SignInAsync
method.
Let's see an example of setting an absolute cookie expiration
In this example, I reuse the LoginAsync
code that was used before to sign a user in and I provide some additional authentication properties. What I want here is an absolute expiration of the cookie, the cookie expires after 10 seconds of its creation. Then I casually create the claims, the Identity and the Principal. Please note that in order for these authentication properties to be taken into account, I pass them into the HttpContext.SignInAsync
.
Now, if you login, wait for 11 seconds and refresh your page, you will see that you are automatically logged out.
Summary
Code repository can be found on Github.
In this post we saw how to secure an MVC application using cookies and local logins, implementing a custom membership backend. We saw that what you need is the HttpContext.User
property to be populated with the appropriate Identity of the user signed-in and the role that HttpContext.SignInAsync
method plays into that.
We also saw how to sign-out users from a specific authentication scheme, using the HttpContext.SignOutAsync
method.
Additionally, we saw how to configure a cookie authentication mechanism and how to create the authentication ticket for the user.
In the coming posts, I am going to show to manipulate user's claims, how to perform social logins, with various social providers like Facebook, Twitter, Github, etc. I am also going to show how to create an SPA and secure it, using OpenId Connect and OAuth 2.0.
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.