LessThanDot

Less Than Dot is a community of passionate IT professionals and enthusiasts dedicated to sharing technical knowledge, experience, and assistance. Inside you will find reference materials, interesting technical discussions, and expert tips and commentary. Once you register for an account you will have immediate access to the forums and all past articles and commentaries.

LTD Social Sitings

Note: Watch for social icons on posts by your favorite authors to follow their postings on these and other social sites.

Performing Authentication and Authorization has changed from ASP.Net to ASP.Net Core. Rather than relying on attributes, ASP.Net Core uses middleware similar to NancyFX and Rails. This is a short, step-by-step approach to implementing custom Authentication in ASP.Net Core without the overhead (and assumptions) of the new Identity model.

The goal is to support basic necessities like a Login page with cookie-based authentication tickets that properly require HTTPS in production, but gracefully fail back to HTTP in local development.

Cookie Middleware

ASP.Net Core 2.0 has pivoted authentication in a different direction (it looks like for purposes of architecture prettiness?), see the comments below for more information. Rather than separate middleware for each authentication mechanism, there is now one middleware for all authentication that you register services for.

This middleware provides support for a number of things we want: directing unauthenticated users to a LoginPath, redirecting access denied requests, authentication tickets with sliding expirations and encryption, and hooks to tie into the process for additional custom logic.

Add the Cookie Authentication middleware in the Startup.Configure method:

Authorize

Once we have an authenticated user, we can add the standard [Authorize] attribute on any controllers or methods to ensure access will require authentication (or challenge if it’s not present). If we had multiple methods of authentication (interactive user, some API access, etc), we could further restrict these actions to just the cookie-based authenticated requests by specifying that authentication scheme in the attribute:[Authorize(ActiveAuthenticationSchemes = "NAME_OF_YOUR_COOKIE_SCHEME")]

An easy way to verify things are in working order is to decorate another Controller/Action with Authorize and attempt to visit it. We should get redirected to Unauthorized on the Account controller above. If we visit Login first, then visit our sample Action we should be allowed in. Visit Logout and then our sample Action, we’re back at Unauthorized.

Ensure Endpoints have Authorization

Now that we have the middleware in place, the Account pages, and a sample page, we should add some protection to make sure we don’t leave any pages exposed accidentally. This step is optional, but it’s something I prefer to do for every application I work on.

Using the sample code from a prior ASP.Net post, we can write a quick unit test that will inspect all Actions and require them to either have explicit [Authorize] or [AllowAnonymous] attributes. This will protect us from accidentally pushing an unprotected endpoint.

Unfortunately, APIExplorer in ASP.Net Core is even less documented so we have to rely on Reflection instead:

I am also using BCrypt for password hashing. BCrypt is a slow hashing function that makes brute force attempts computationally expensive. To verify a hashed BCrypt password, we have to pull it out of our store and ask BCrypt to verify it (instead of some methods that allow you to hash a new value and compare at the storage level).

Inside the ISessionManager.CreateSessionAsync, we create a session in storage with an id and then when the Principal is created we add that Id as a claim in the Principal. This is how we’ll look up the session on later requests to get the user information.

The Cookie middleware will take care of built-in timeouts, but we also need to add a check in case someone disables the user (or whatever criteria is necessary in your system). Add this to the Startup.cs setup.

ISessionManager.IsSessionValidAsync can now pull that Id claim we created above, get the associated user, and do any number of additional validations on the session length, user enabled state, and so on.

Accessing the User/Session

The last step is to the ability to access the User in other controllers. Controllers have a User property that grants access to the ClaimsPrincipal created during login. Assuming we had also stored a property like “UserId” in ISessionManager.CreateSessionAsync then we can access it like this:

The second time I jumped through this particular hoop, I added a method to my SessionManager that accepted a Controller and looked the user up from the database, so I could do this instead:

APIProject/Controllers/AnyOldController.cs

C#

1

var user = await _sessionManager.GetCurrentUserAsync(this);

var user = await _sessionManager.GetCurrentUserAsync(this);

The Session Id mentioned in the prior section is handled the same way.

Final Checklist

If you’re using this to follow along and implement, here are the pieces you should have:

Startup.cs: The CookieMiddleware configured with expiration, automatic challenge, 401 redirects for APIs, and re-validation of the session

InteractiveAccountController: GET/POST for Login, GET for Logout, pages for Forbidden plus the Views for these pages

UserStore and SessionManager implementations, with just a few functions needed and ability to extend the ClaimsPrincipal with more fields as you grow

You can go as light or heavy as you need to with storage, relational or non-relational, as needed. You have BCrypt, one of the small set of recommended options from OWASP (or upgrade to Argon2). You have cookies with encrypted contents (including the expiration date) that automatically default to secure for HTTPS websites.

Related Posts

One of the challenges of SPA applications is making sure a bookmark or hard refresh…

About the Author

My roles have included accidental DBA, lone developer, systems architect, team lead, VP of Engineering, and general troublemaker. On the technical front I work in web development, distributed systems, test automation, and devop-sy areas like delivery pipelines and integration of all the auditable things.

Thought it was worth mentioning that the Startup.cs portion is only valid for dotnet core 1.x. Beginning in core 2.0, the cookie authentication config is moved to ConfigureServices method, and is different from the code provided in this blog.