DotNet Core Authentication 1 - Sites
(Updated from the original version of 8th March 2020)
Sometimes the built-in options for authentication (Individual User Accounts
, Work or School Accounts
, or Windows Authentication
) are not what you want. Most small sites will go for the first option, but as standard you usually end up with Entity Framework, EF migrations, and SQL Server (Express etc).
Whilst you can override, delete, or edit all this, it's much easier to never generate it to start with. Especially if you intend to use an alternative database such as Postgres. What follows is a set of extremely simple steps to get the basics in place without pre-generation.
You'll get [Authorize]
attributes for controller/route protection, supported by cookie-based user sessions. It doesn't go as far as creating the database, but it gets you to the point where you just need to add your own logic to a single Login
method. You will have Login
, Logout
, and a protected (stub) Dashboard
.
There is a Github repository which contains all the below steps as a sequence of commits.
Create the solution
Update: This continues to work with DotNet 7. The only difference is the lack of a
Startup.cs
file which involves a trivial change noted at the appropriate point below.
Using Visual Studio 2019 v16.5 Preview or later, create as follows:
- ASP.Net Core Web Application
- C# and DotNet Core 3 or later (tested up to DotNet 7)
- Web Application (MVC)
- No authentication
- Configure for HTTPS
Update the configuration
The first thing is to register the use of cookies for maintaining authentication state. This is done during startup.
Startup.cs
Update: In later versions of DotNet this file has gone and we edit
Program.cs
instead. The code is the same but is now placed in theMain
method, andservices
is replaced withbuilder.Services
.
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Authentication.Cookies;
public void ConfigureServices(IServiceCollection services)
{
// Add just before AddControllersWithViews()
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(o =>
{
o.Cookie.HttpOnly = true;
o.Cookie.SameSite = SameSiteMode.Strict;
o.Cookie.SecurePolicy = CookieSecurePolicy.Always;
o.ExpireTimeSpan = TimeSpan.FromMinutes(120);
o.SlidingExpiration = true;
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// Add just before UseHttpsRedirection()
app.UseAuthentication();
}
Add a sample protected route
With that in place you need a sample endpoint to test/demo it.
One simple way is to add a Dashboard
to the HomeController
(which already exists).
HomeController.cs
using Microsoft.AspNetCore.Authorization;
[Authorize()]
public IActionResult Dashboard()
{
return View();
}
- Right-click the
Dashboard()
method name andAdd View
.- If you are not using Visual Studio you may need to copy and rename
Index.cshtml
instead
- If you are not using Visual Studio you may need to copy and rename
- Choose
Razor View
(notRazor View - Empty
) and clickAdd
. - On the next screen select
Empty (without model)
and keep the defaults for the other stuff. - There is no need to add anything to the view once it is created.
Test accessing that route
Navigate to Home/Dashboard
in your site.
You should get a 404
but if you look carefully you'll see it isn't the dashboard that can't be found. It's an Account/Login
page.
This is because the Authorize
attribute has protected the Home/Dashboard
route by redirecting you away to the default login screen (which you don't yet have).
Add the ability to login
There is a default Account
controller expected by DotNet Core, that is also expected to have a Login
action. In the real world GET AccountController.Login()
would request the user's credentials. POST AccountController.Login()
would receive the answers, check them, then sign the user in. For this brief summary we will just sign them in automatically without them needing to provide their credentials.
Add a new Controller using the template for MVC Controller - Empty
, and call it AccountController
. You'll get a default Index
action, which you should rename to Login
and whose contents should be removed and replaced with what follows.
AccountController.cs
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
public async Task<IActionResult> Login()
{
var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
identity.AddClaim(new Claim(ClaimTypes.Name, "Example"));
identity.AddClaim(new Claim("ID", "1"));
identity.AddClaim(new Claim("IsAdmin", "Y"));
var principal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);
return RedirectToAction("Dashboard", "Home");
}
This code will log you in without requiring credentials. It does this by creating a set of claims in an Identity, which is then used to sign you in.
Add the ability to logout
Clearing cookies should work. Traditionally however we would expect a simple link or button that would log users out of the site. The following does that, removing the cookie as it goes.
AccountController
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync();
return RedirectToAction("Index", "Home");
}
Updating layouts and views
There is a standard object available to both the layout template and any controller actions. It's called User
, and from it we always have access to any logged in user.
First let's update the main layout file to work out the login details.
Insert the following on the line below the !DOCTYPE
declaration.
_Layout.cshtml
This creates a view-level pair of helpers.
@{
var loggedIn = (User != null && User.Identity.IsAuthenticated);
var name = (loggedIn ? User.Identity.Name : string.Empty);
}
We need to insert a new block between the LI
tags for Home and Privacy.
This adds menu options which are dynamic according to your login status.
@if (loggedIn)
{
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Dashboard">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Account" asp-action="Logout">Logout</a>
</li>
}
else
{
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Account" asp-action="Login">Login</a>
</li>
}
Now just above the @RenderBody()
(inside main
) add this.
It will show a strip below the menu with your login status.
@if (loggedIn)
{
<p>Logged in as <strong>@name</strong></p>
}
You can use the same loggedIn
flag anywhere in the view, and even place it in the AccountController
to get access during request handling.
Final and next steps
What you've got
If you run the site now you should be able to login and logout, seeing the menu change as needed. You'll also get a (stub) dashboard page and a User
instance available in both controllers and views, to which you can attach any info you're happy being transmitted in cookies to avoid database trips.
Next steps
- The automatic login should be replaced with
- a GET for user credential input
- a POST to accept, verify, and do the login
- Add a database backend, with your own custom migration
- Consider using Migratable, my own migration library
- Remember to not store passwords (either plain or encrypted)
- Use one-way repeatable hashes
- Include both salt and pepper