Authentication is the process of confirming a user’s identity. It is a set of actions, we use to verify the user’s credentials against the ones in the database. For the user to be able to provide credentials, our application requires a Login page with a set of fields for our user to interact with.
In this article, we are going to learn how to implement user authentication with ASP.NET Core Identity. So our main goal is going to be creating a login page and preparing a set of actions to validate input credentials.
To navigate through the entire series, visit the ASP.NET Core Identity series page.
Let’s start.
VIDEO: Web API Authentication With Identity And JWT.
Preparing the Authentication Environment in our Project
The first thing, we are going to do is disable unauthorized users to access the Employees
action. To do that, we have to add the [Authorize]
attribute on top of that action:
[Authorize] public async Task<IActionResult> Employees() { var employees = await _context.Employees.ToListAsync(); return View(employees); }
Additionally, we have to add authentication middleware to the ASP.NET Core’s pipeline right above the app.UseAuthorization()
expression:
app.UseAuthentication();
If we run our application now and click on the Employees
link, we are going to get a 404 not found response:
We get this because, by default, ASP.NET Core Identity tries to redirect an unauthorized user to the /Account/Login
action, which doesn’t exist at the moment. Additionally, you can see a ReturnUrl
query string that provides a path to the required action before the user was redirected to the Login page. We are going to deal with it later in this post.
Now, we are going to do a couple of things to fix this 404 error.
First, let’s create a UserLoginModel
class in the Models
folder:
public class UserLoginModel { [Required] [EmailAddress] public string Email { get; set; } [Required] [DataType(DataType.Password)] public string Password { get; set; } [Display(Name = "Remember me?")] public bool RememberMe { get; set; } }
Next, let’s add two new actions to the Account controller:
[HttpGet] public IActionResult Login() { return View(); } [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Login(UserLoginModel userModel) { return View(); }
We don’t want to navigate to the Login page only by accessing the protected action, we want to have a separate link for that as well. So, let’s modify the _LoginPartial
view:
<ul class="navbar-nav"> <li class="nav-item"> <a class="nav-link text-dark" asp-controller="Account" asp-action="Login">Login</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-controller="Account" asp-action="Register">Register</a> </li> </ul>
Finally, let’s create a Login view:
@model IdentityByExamples.Models.UserLoginModel <h1>Login</h1> <div class="row"> <div class="col-md-4"> <form asp-action="Login"> <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="Email" class="control-label"></label> <input asp-for="Email" class="form-control" /> <span asp-validation-for="Email" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Password" class="control-label"></label> <input asp-for="Password" class="form-control" /> <span asp-validation-for="Password" class="text-danger"></span> </div> <div class="form-group form-check"> <label class="form-check-label"> <input class="form-check-input" asp-for="RememberMe" /> @Html.DisplayNameFor(model => model.RememberMe) </label> </div> <div class="form-group"> <input type="submit" value="Log In" class="btn btn-primary" /> </div> </form> </div> </div> @section Scripts { @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} }
Ok, let’s test this:
Excellent, everything is prepared and we can continue.
Implementing Authentication with ASP.NET Core Identity
After we click the submit button, the UserLoginModel
will be sent to the POST Login action. So, we have to modify that action:
[HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Login(UserLoginModel userModel) { if(!ModelState.IsValid) { return View(userModel); } var user = await _userManager.FindByEmailAsync(userModel.Email); if(user != null && await _userManager.CheckPasswordAsync(user, userModel.Password)) { var identity = new ClaimsIdentity(IdentityConstants.ApplicationScheme); identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id)); identity.AddClaim(new Claim(ClaimTypes.Name, user.UserName)); await HttpContext.SignInAsync(IdentityConstants.ApplicationScheme, new ClaimsPrincipal(identity)); return RedirectToAction(nameof(HomeController.Index), "Home"); } else { ModelState.AddModelError("", "Invalid UserName or Password"); return View(); } }
First, we check if the model is invalid and if it is, we just return a view with the model. After that, we use the FindByEmailAsync
method from UserManager
to return a user by email. If the user exists and the password matches the hashed password from the database, we create the ClaimsIdentity
object with two claims inside (Id and UserName). Then, we sign in the user with the SignInAsync
method by providing the scheme parameter and the claims principal. This will create the Identity.Application
cookie in our browser. Finally, we redirect the user to the Index action.
If the user doesn’t exist in the database or the password doesn’t match, we return a view with the appropriate message.
Before we test this, let’s add a small modification in the Employees view below the table. This way we can see our claims:
<h2>Claim details</h2> <ul> @foreach (var claim in User.Claims) { <li><strong>@claim.Type</strong>: @claim.Value</li> } </ul>
Now, let’s try to login with the invalid credentials:
If we use valid credentials, we are going to be redirected to the Index page with the Identity.Application
cookie created:
Now, we can visit the Employees page:
We can access it and we can see our claims.
Implementing ReturnUrl in Authentication Process
As you could see in the first part of this article, if the user is not authorized and tries to access the protected action, they are going to be redirected to the Login page. The URL contains the ReturnUrl
query parameter as well, which shows the source page the user came from. But in our case, we just navigate the user to the Home page. So, let’s fix that.
The first thing, we are going to do is to modify the GET Login action:
[HttpGet] public IActionResult Login(string returnUrl = null) { ViewData["ReturnUrl"] = returnUrl; return View(); }
Then, we have to modify the Login.cshtml
file as well:
<form asp-action="Login" asp-route-returnUrl="@ViewData["ReturnUrl"]">
We have to modify the POST action too:
public async Task<IActionResult> Login(UserLoginModel userModel, string returnUrl = null)
Also, in the same method, instead of the ReturnToAction
method, we are going to call a custom one:
return RedirectToLocal(returnUrl);
And of course, let’s create that method in the Account controller:
private IActionResult RedirectToLocal(string returnUrl) { if (Url.IsLocalUrl(returnUrl)) return Redirect(returnUrl); else return RedirectToAction(nameof(HomeController.Index), "Home"); }
So we just check if the returnUrl is a local URL and if it is we redirect the user to that address, otherwise, we redirect the user to the Home page.
Let’s see how it works:
And we can see it works as expected.
One additional thing. Our Login action is on the /Account/Login
route, and that’s the default route ASP.NET Core Identity is going to look for. But for a different path, for example: /Authentication/Login
, we have to configure application cookie in the ConfigureServices
method:
services.ConfigureApplicationCookie(o => o.LoginPath = "/Authentication/Login");
Now, we can proceed.
Automating Authentication Process
If you want to take complete control over the authentication logic, the approach we have used is a great choice. But, we can speed up the process by using the SignInManger<TUser>
class. This class provides the API for user sign in with a lot of helper methods.
So, let’s inject it first in the Account controller:
private readonly IMapper _mapper; private readonly UserManager<User> _userManager; private readonly SignInManager<User> _signInManager; public AccountController(IMapper mapper, UserManager<User> userManager, SignInManager<User> signInManager) { _mapper = mapper; _userManager = userManager; _signInManager = signInManager; }
And, let’s use it in the Login action:
[HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Login(UserLoginModel userModel, string returnUrl = null) { if (!ModelState.IsValid) { return View(userModel); } var result = await _signInManager.PasswordSignInAsync(userModel.Email, userModel.Password, userModel.RememberMe, false); if (result.Succeeded) { return RedirectToLocal(returnUrl); } else { ModelState.AddModelError("", "Invalid UserName or Password"); return View(); } }
We use the PasswordSignInAsync
method that accepts four parameters: Username, Password, Persist Cookie, and LockOut on Failure. For now, we don’t require a lockout feature, so we set it to false. This method does all the magic we had to do by ourselves in the previous implementation. Additionally, it returns a result with four properties:
As you can see, we use the Succeeded
property to verify that our action completed successfully.
Now if we login successfully to our application, we can see the cookie created with our claims:
We can see additional claims as well, like security stamp, role, and amr(Authentication Method Reference).
Adding Custom Claims in the Authentication Process
In the previous list of claims, we can’t see our custom properties from the User class. Of course, if we want to add those, there is a way to do that.
Let’s create a new Factory folder and a new class inside:
public class CustomClaimsFactory : UserClaimsPrincipalFactory<User> { public CustomClaimsFactory(UserManager<User> userManager, IOptions<IdentityOptions> optionsAccessor) : base(userManager, optionsAccessor) { } }
Our custom class has to implement the UserClaimsPrincipalFactory<TUser>
class and to send a userManager
and optionsAccessor
objects to it. Now, we have to override the GenerateClaimsAsync
method in this class to add our additional claims:
protected override async Task<ClaimsIdentity> GenerateClaimsAsync(User user) { var identity = await base.GenerateClaimsAsync(user); identity.AddClaim(new Claim("firstname", user.FirstName)); identity.AddClaim(new Claim("lastname", user.LastName)); return identity; }
The final thing we have to do is to register this class in the service collection:
services.AddScoped<IUserClaimsPrincipalFactory<User>, CustomClaimsFactory>();
After we test this:
We can see our additional claims.
Awesome.
Update
After the CustomClaimsFactory
class implementation, we can’t see the role claim anymore on the page. If we want to add it, we have to modify the GenerateClaimsAsync
method:
protected override async Task<ClaimsIdentity> GenerateClaimsAsync(User user) { var identity = await base.GenerateClaimsAsync(user); identity.AddClaim(new Claim("firstname", user.FirstName)); identity.AddClaim(new Claim("lastname", user.LastName)); var roles = await UserManager.GetRolesAsync(user); foreach (var role in roles) { identity.AddClaim(new Claim(ClaimTypes.Role, role)); } return identity; }
With this in place, we can see the role claim once again as part of our token.
Logout Implementation
The Logout implementation is pretty simple though. But before we do that, let’s modify the _LoginPartial
view:
@using Microsoft.AspNetCore.Identity @inject SignInManager<User> SignInManager @inject UserManager<User> UserManager <ul class="navbar-nav"> @if (SignInManager.IsSignedIn(User)) { <li class="nav-item"> <a class="nav-link text-dark" asp-controller="Home" asp-action="Index" title="Welcome">Welcome @User.Identity.Name!</a> </li> <li class="nav-item"> <form class="form-inline" asp-controller="Account" asp-action="Logout"> <button type="submit" class="nav-link btn btn-link text-info">Logout</button> </form> </li> } else { <li class="nav-item"> <a class="nav-link text-dark" asp-controller="Account" asp-action="Login">Login</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-controller="Account" asp-action="Register">Register</a> </li> } </ul>
Here, we inject the SignInManager
and UserManager
classes by using the View Dependency Injection Technique and then just check if our user is signed in. If it is, we show a welcome message and the Logout button.
Now, let’s implement the Logout action:
[HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Logout() { await _signInManager.SignOutAsync(); return RedirectToAction(nameof(HomeController.Index), "Home"); }
Once we start the application, log in successfully and click the Logout link, we are going to be logged out and the cookie will be removed.
Conclusion
So, to sum it up, we have learned:
- How to execute the authentication process
- How to use different UserManager helper methods that help in a process
- The way to implement ReturnUrl logic
- And how to add additional claims to the user
In the next article, we are going to talk about Reset Password (Forgot Password) functionality with ASP.NET Core Identity.