This article covers authentication in ASP .NET Core. It tries to explain the concepts and how they relate and also shows some code so you can hopefully add authentication to your own .NET app.
Authenticating a user means determining a user's identity. We do this to ensure they are who they say they are. Once we ensure we trust them, we can log them into our app and show them resources that only logged in users should have access to.
Main scenarios
Here's the main scenarios we're looking to address:
- Authenticating a user.
- Responding when an unauthenticated user tries to access a restricted resource.
Handlers, services to handle the authentication flows
To carry out an authentication flow, you need a handler. You can have more than one handler, but it must implement the IAuthenticationService
interface. The handler/s is used by the authentication middleware. The registered authentication handlers and their configuration options are called "schemes".
A scheme
To use a scheme, you need to register it here Startup.ConfigureServices
, i.e. like so:
void ConfigureServices()
{
// register your scheme
}
What you need to do is to first call AddAuthentication()
followed by a call to a specific scheme, like the below example:
void ConfigureServices()
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => Configuration.Bind("JwtSettings", options))
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options => Configuration.Bind("CookieSettings", options));
}
Here we call AddAuthentication()
followed by a call to both the schemes AddJwtBearer()
and AddCookie()
. By calling these schemes, you register them and their config settings.
You don't always need to explicitly call
AddAuthentication()
, if you use ASP .NET Identity, that call is done internally for you.
Set up middleware
To understand why we do this step, lets first talk about the request pipeline. Imagine the following scenario:
The calling client makes a request towards a resource on your system. To allow for that to happen, they need to be logged in to your system. To log in, the user needs to provide credentials (typically username and password) that convinces us they are who they say they are. We call this to authenticate. However, we need to write code to ensure the request is intercepted and we have a chance to validate the user. This is why we now need to talk to middleware and configure it to use the handlers/schemes we registered thus far with ASP .NET.
Now we go to another method Configure()
in our Startup
class and call UseAuthentication()
like so:
void Configure()
{
UseAuthentication();
}
You need to call
UseAuthentication()
in the right time so anything dependent on the auth can use it. Here's som guidelines:
- After
UseRouting()
, so that route information is available for authentication decisions.- Before
UseEndpoints()
, so that users are authenticated before accessing the endpoints.
Authenticating the user
Ok, so you've seen two parts of three so far:
- Register scheme/handler
- Instructing the pipeline to use it
- Authenticating the user <- to be explained
Let's discuss how to authenticate the user. We will do that by borrowing some code from a sample project that does Cookie authentication Cookie authentication
Register scheme/handler
First, let's check 1) the registering of the scheme, in ConfigureServices()
method in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthentication(CookieScheme) // Sets the default scheme to cookies
.AddCookie(CookieScheme, options =>
{
options.AccessDeniedPath = "/account/denied";
options.LoginPath = "/account/login";
});
// Example of how to customize a particular instance of cookie options and
// is able to also use other services.
services.AddSingleton<IConfigureOptions<CookieAuthenticationOptions>, ConfigureMyCookie>();
}
Note the call to AddCookie()
for registering the scheme and the config AccessDeniedPath
and LoginPath
. By specifying those config values, we know what route to send the user to if they are:
- unable to provide credentials
/account/denied
- a place to provide credentials and be logged in
/account/login
Configure middleware
We've registered the scheme and is ready to use it. Our next step is explicitly telling our app to use it by a call to UseAuthentication()
.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
});
}
Note the call to app.UseAuthentication()
and how it's after the call to UseRouting()
, so we have the routing information, and before the call to UseEndpoints()
so the latter can leverage the authentication capability.
Doing the actual authentication
When we registered the scheme, we told it where to go for logins, i.e /account/login
, that's a controller class and method we need to write. So, create a AccountController.cs
with the following content:
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
namespace AuthSamples.Cookies.Controllers;
public class AccountController : Controller
{
}
What we need to fill this class with is the following:
- a way to render a login form
- a method that handles a user sending their login credentials and have those validated
- a method for handling logging out
Rendering a login form
Here we our controller code becomes simple, like so:
[HttpGet]
public IActionResult Login(string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
return View();
}
It becomes simple as there's a template that we render Login.cshtml:
<h2>Login</h2>
<div class="row">
<div class="col-md-8">
<section>
<form asp-controller="Account" asp-action="Login" asp-route-returnurl="@ViewData["ReturnUrl"]" method="post" class="form-horizontal" role="form">
<div class="form-group">
<label class="col-md-2 control-label">User</label>
<div class="col-md-10">
<input type="text" name="username" />
</div>
</div>
<div class="form-group">
<label class="col-md-2 control-label">Password</label>
<div class="col-md-10">
<input type="password" name="password" />
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<button type="submit" class="btn btn-default">Log in</button>
</div>
</div>
</form>
</section>
</div>
</div>
The above template consists of a username field, a password field and submit button.
Handling login request
Imagine now the user enters their credentials in the above form, then we need controller code to handle that:
private bool ValidateLogin(string userName, string password)
{
// For this sample, all logins are successful.
return true;
}
[HttpPost]
public async Task<IActionResult> Login(string userName, string password, string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
// Normally Identity handles sign in, but you can do it directly
if (ValidateLogin(userName, password))
{
var claims = new List<Claim>
{
new Claim("user", userName),
new Claim("role", "Member")
};
await HttpContext.SignInAsync(new ClaimsPrincipal(new ClaimsIdentity(claims, "Cookies", "user", "role")));
if (Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
else
{
return Redirect("/");
}
}
return View();
}
- the
Login()
method being called via a POST request when the user hits the login button. We are provided withusername
andpassword
and we callValidateLogin()
(here you typically want a call to a database to validate the user, ensure they exist, the password is correct etc). - Then we sign in the user, if
ValidateLogin()
responds with true. - The sign in happens when we call
HttpContext.SignInAsync()
. Said method needs aClaimsPrincipal
instance to be created. That object needs aClaimsIdentity
object as an instance. High-level we say who the user is and can later determine what parts of the system the user shall have access to, in an authorization process.
Handle logout request
The user might want to logout, to handle that, you can add another method to your AccountController
class like so:
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync();
return Redirect("/");
}
Calling HttpContext.SignOutAsync()
will ensure that the app forgets all about you and your login session.
What goes on here is that:
Routing the user correctly
At this point you are wondering, how does the system know how to send the users to this login form, like how do I check who's logged in or not?
This is when we have a look at the HomeController.cs file. It uses an attribute class Authorize
to force the user to the login page if they are not authenticated. Here's how that would work:
- User tries to go to route
/Home/MyClaims
[Authorize]
public IActionResult MyClaims()
{
return View();
}
- If the user is logged in, then they are presented with the view represented by
MyClaims
function. If not logged in, then they are taken toaccount/login
.
Summary
Hopefully, you now have a good starting point for understanding how to authenticate and login a user to an ASP .NET application. In the next part we will look at how you can authorize a user, i.e determine what user should have access to what resources.