Now that users can successfully register for new accounts, let's add the logic necessary for them to sign in and out of their accounts.
First, let's walk through the process of logging a user in.
Our login page will contain a form asking for the user's email and password. We'll create the necessary ViewModel
in the ViewModels
folder to manage this:
using System.ComponentModel.DataAnnotations;
namespace ToDoList.ViewModels
{
public class LoginViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}
Again, notice we follow naming conventions by including the ViewModel
prefix at the end of our View Model's filename.
Next, we'll create the View
containing our login form:
@{
Layout = "_Layout";
}
@using ToDoList.ViewModels
@model LoginViewModel
<h2>Log in with your account</h2>
<hr />
@if(!ViewData.ModelState.IsValid)
{
@Html.ValidationSummary("Please fix the following error(s) and then resubmit the form:")
}
@using (Html.BeginForm())
{
@Html.LabelFor(m => m.Email)
@Html.TextBoxFor(m => m.Email)
@Html.LabelFor(m => m.Password)
@Html.EditorFor(m => m.Password)
<input type="submit" value="Log in" />
}
<p>@Html.ActionLink("Register for an account", "Register")</p>
<p>@Html.ActionLink("Back to accounts page", "Index")</p>
Now we can add the necessary actions to the controller. We'll include a GET
to retrieve and display the Login View
in addition to a POST
to go through the process of actually logging a user in once they submit the login form.
Let's take a look at the full code and then we'll explain it in detail.
...
public ActionResult Login()
{
return View();
}
[HttpPost]
public async Task<ActionResult> Login(LoginViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
else
{
Microsoft.AspNetCore.Identity.SignInResult result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, isPersistent: true, lockoutOnFailure: false);
if (result.Succeeded)
{
return RedirectToAction("Index");
}
else
{
ModelState.AddModelError("", "There is something wrong with your email or username. Please try again.");
return View(model);
}
}
}
...
We'll focus on the Login()
POST method which once again uses an asynchronous method. Note that there are several similarities with our Register()
POST method:
async
and return a Task<ActionResult>
.ViewModel
as an argument.Async
. All async Identity methods have Async
appended to them.result
that must await
the completion of an Identity method.Now let's take a closer look at the following line:
Microsoft.AspNetCore.Identity.SignInResult result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, isPersistent: true, lockoutOnFailure: false);
Remember that we've injected a SignInManager
service, which is being referenced in the signInManager
variable. The SignInManager
class includes the PasswordSignInAsync()
method, which has a self-explanatory name: it's an async method that allows users to sign in with a password.
PasswordSignInAsync()
takes four parameters: userName
, password
, isPersistent
, and lockoutOnFailure
. For now we're only handling username and password, so we set explicit boolean values for isPersistent
and lockoutOnFailure
.
However, just like in our Register()
action, we want to ensure our application doesn't freeze or break if Identity can't successfully authenticate an account. That's why we add an if
statement based on whether the result
has succeeded or not. The Microsoft.AspNetCore.Identity.SignInResult
object has a Succeeded
boolean property to help with this. If our sign in action has succeeded, then we'll be redirected to our accounts index page.
If our sign in action has failed, well, there's no SignInResult.Errors
property to use to create error messages for our user. There are individual properties with information about different signin states, which you can review in the [SignInResult
] docs.
So what should we do? This is really a trick question: we don't actually want to give specific error messages to the user about why a sign in attempt has failed. That's because specific error messages could be delivered to malicious users that improve their ability to break into an account. For example, it's better to state "there was an issue with your username or password" than to state the more specific "that password does not match our records". So this is exactly what we do in case of a signin failure. Let's look at the code once more:
Microsoft.AspNetCore.Identity.SignInResult result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, isPersistent: true, lockoutOnFailure: false);
if (result.Succeeded)
{
return RedirectToAction("Index");
}
else
{
ModelState.AddModelError("", "There is something wrong with your email or username. Please try again.");
return View(model);
}
We still create a new ModelState
error, but our error message is very generic.
Since the action will redirect to Index if the user successfully logs in, let's add some code to Index.cshtml
to display different information based on whether or not the user is logged in:
@{
Layout = "_Layout";
}
<h2>Authentication with Identity</h2>
<hr />
@if (User.Identity.IsAuthenticated)
{
<p>Hello @User.Identity.Name!</p>
}
else
{
<p>@Html.ActionLink("Register", "Register")</p>
<p>@Html.ActionLink("Log in", "Login")</p>
}
<p>@Html.ActionLink("Home", "Index", "Home")</p>
We added an if/else
statement to display a greeting to the user if they are logged in. If they aren't, we display links to register or log in. Our if statement relies on accessing the User
variable, which is a property called RazorPageBase.User, but is of the type System.Security.Claims.ClaimsPrincipal.
We can access two properties through the User.Indentity
property:
User.Indentity.Name
gives us the currently logged in user's name (for us, their email) or null if there is none.User.Indentity.IsAuthenticated
gives us true
if there's an authenticated user (meaning, a user who is logged in), or false
if there is no currently logged in user.Note that we can access both of the above properties anywhere in our views, as needed. User.Indentity.IsAuthenticated
is particularly useful for creating conditionals that display information only to authenticated users.
Next, let's give the user the ability to log out. Instead of creating a separate GET and POST controller action and view, we'll create just a POST controller action and add a form directly to our account's index page.
Add a LogOff()
action to the controller.
...
[HttpPost]
public async Task<ActionResult> LogOff()
{
await _signInManager.SignOutAsync();
return RedirectToAction("Index");
}
...
This method is straightforward. SignInManager
has the asynchronous method SignOutAsync()
that signs the user out. Everything else in this method should look familiar at this point.
Note that we don't need to add any error handling or model validation here, since a user should only be able to log out if they're already logged in.
We're going to add a form to invoke the LogOff()
POST action directly to our Account/Index.cshtml
view. Here's what our view should now look like:
@{
Layout = "_Layout";
}
<h2>Authentication with Identity</h2>
<hr />
@if (User.Identity.IsAuthenticated)
{
<p>Hello @User.Identity.Name!</p>
@using (Html.BeginForm("LogOff", "Account"))
{
<input type="submit" class="btn btn-primary" value="Log out" />
}
}
else
{
<p>@Html.ActionLink("Register", "Register")</p>
<p>@Html.ActionLink("Log in", "Login")</p>
}
<p>@Html.ActionLink("Home", "Index", "Home")</p>
We now have complete functionality for users to sign up, sign in, and sign out. Woo-hoo!
Follow the link below to view how a sample version of the project should look at this point. Note that this is a link to a specific branch in the repository.
Example GitHub Repo for To Do List with Authentication and Authorization: 2_authentication