ASP.NET vNext Identity Part 2 – The Login Process

In the first article in this series I went through the process of setting up a database for identity storage and storing a single admin user in the database. However I still have not enabled logins to my application yet. I need to enable identity within the application, set up a view that needs to be authenticated and write some views to handle logging in. I am not going to be adding registration in this article – that’s the subject for another day. You can retrieve the starting point for this article in the AspNetIdentity project on my GitHub repository.

The first stop is to my Startup.cs file. I need to actually enable Identity in the same way as I enable static files, detailed error pages, browser link, MVC and just about anything else. I need to tell the pipeline that I am using identity. Like this:

        
        public void Configure(IApplicationBuilder app)
        {
            // A 404 Error Page with useful information
            app.UseErrorPage(ErrorPageOptions.ShowAll);

            // Serve up the stuff in wwwroot
            app.UseStaticFiles();

            // Authentication and Authorization
            app.UseIdentity();

            // Configure ASP.NET MVC6
            app.UseMvc(routes =>
                {
                    routes.MapRoute(
                        name: "default",
                        template: "{controller=Home}/{action=Index}/{id?}"
                    );
                });

            // Create a default user
            IdentityDbOperations.InitializeIdentityDbAsync(app.ApplicationServices).Wait();
        }

Just one line was added to this method. Now I need to get some authentication going. I want my whole application to require authentication except where it isn’t possible – like the login screen. I have a HomeController right now so let’s add authentication to that. Here is the whole file:

using Microsoft.AspNet.Mvc;

namespace AspNetIdentity.Controllers
{
    [Authorize]
    public class HomeController : Controller
    {
        [HttpGet]
        public IActionResult Index()
        {
            return View();
        }
    }
}

That [Authorize] tag tells the ASP.NET application that we want authentication. Let’s run our application and see what happens:

blog-code-0403-1

This is just part of the detailed error report I get when I get the 404 result. The system is redirecting me to a login page because I’m not authorized to be on the home page. However the login page doesn’t exist. To create one I’m going to need an Account controller and a Login view. Let’s start with the controller. Add a new MVC Controller Class called AccountController. I’ve broken the class down into manageable chunks below:

namespace AspNetIdentity.Controllers
{
    public class AccountController : Controller
    {
        public AccountController(
            UserManager<ApplicationUser> userManager,
            SignInManager<ApplicationUser> signInManager)
        {
            UserManager = userManager;
            SignInManager = signInManager;
        }

        public UserManager<ApplicationUser> UserManager
        {
            get;
            private set;
        }

        public SignInManager<ApplicationUser> SignInManager
        {
            get;
            private set;
        }

When my AccountController first gets created, it is passed the user manager and sign-in manager objects that are part of the Identity Framework (using Dependency Injection). I need to store those for later on. Those objects are stored in two properties that I’ve set to be read-only to the outside world.

        /**
         * GET /Account/Login
         */
        [HttpGet]
        [AllowAnonymous]
        public IActionResult Login(string returnUrl = null)
        {
            ViewBag.ReturnUrl = returnUrl;
            return View();
        }

When first entering the login process, we saw that it does a GET of /Account/Login with a returnUrl field. This method processes that request and returns the login page. More on that later.

        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
        {
            ViewBag.ReturnUrl = returnUrl;
            if (ModelState.IsValid)
            {
                var result = await SignInManager.PasswordSignInAsync(model.UserName, model.Password, model.RememberMe, shouldLockout: false);
                if (result.Succeeded)
                {
                    return RedirectToLocal(returnUrl);
                }
                if (result.IsLockedOut)
                {
                    return View("Lockout");
                }
                else
                {
                    ModelState.AddModelError("", "Invalid username or password.");
                    return View(model);
                }
            }

            // If we got this far, something failed - redisplay the form
            return View(model);
        }

This is a little more complex, but should be fairly readable. We first of all check that our parameters (which are contained in the LoginViewModel – more on that later) are good. If they are, then call the password checker in the sign-in manager. There are three possible outcomes – the login succeeded, the user is locked out or “something else”. I’m going to assume “something else” is a bad username or password.

If the login is successful then the user is authenticated and gets redirected back to where they were. If not, then display the login page with appropriate information about the failure.

To wrap this up I need to write the RedirectToLocal helper method:

        private IActionResult RedirectToLocal(string returnUrl)
        {
            if (Url.IsLocalUrl(returnUrl))
            {
                return Redirect(returnUrl);
            } else
            {
                return RedirectToAction("Index", "Home");
            }
        }

This basically says “if you specified a valid local URL, then go there. Otherwise go to the home page.”

There are two additional pieces I need to complete the sequence. The first is a basic ViewModel for the Login action. I’ve created a new directory called ViewModels and placed the LoginViewModel.cs in there:

using System.ComponentModel.DataAnnotations;

namespace AspNetIdentity.ViewModels
{
    public class LoginViewModel
    {
        [Required]
        [Display(Name = "User name")]
        public string UserName { get; set; }

        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; }

        [Display(Name = "Remember me?")]
        public bool RememberMe { get; set; }

    }
}

ViewModels are just like models, except they model the data coming in from the client rather than the data going out to the client. You can add decorators to the view model properties to give the view some information on how to render it. In this case, I’m telling the view that the username and password are required fields in the form and have specific default display attributes.

The second piece I need to complete the login sequence is a login view. I’m going to create a new layout file for the login view. I’ve placed this in Views/Layout/LoginPage.cshtml with the following contents:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css">
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap-theme.min.css">
    <link rel="stylesheet" href="~/Style/Login.css">
    <title>@ViewBag.Title</title>
</head>
<body>
      @RenderBody()
</body>
</html>

I’ve also got a Views/Account/Login.cshtml view file which I will build. My initial contents (it will grow) are as follows:

@model AspNetIdentity.ViewModels.LoginViewModel

@{ 
    Layout = "~/Views/Layout/LoginPage.cshtml";
    ViewBag.Title = "Log In";
}

<div class="login-form-outer"></div>

I’m stopping there temporarily because I want to show this off in order. Right now, my LESS file is as follows:

@import (less) "mixins/reset.less";

@loginbox-height: 200px;
@loginbox-width: 200px;

body {
    background-color: #666666;
}

.login-form-outer {
    border: 1px solid #333333;
    border-radius: 16px;
    background: white;

    width: @loginbox-width;
    height: @loginbox-height;
    position: absolute;
    left: 50%;
    top: 50%;
    margin-left: -(@loginbox-width / 2);
    margin-top: -(@loginbox-height / 2)
}

This uses a technique that I found on css-tricks.com for centering the box on the page. After this, everything else is in the form, so I won’t be worrying about the CSS too much. If you want to see the whole CSS, check out the project on GitHub at the end.

Now, back to our Login view. The next thing we have to do is to bring in the form. This is the bulk of the view:

@model AspNetIdentity.ViewModels.LoginViewModel

@{
    Layout = "~/Views/Layout/LoginPage.cshtml";
    ViewBag.Title = "Log In";
}

<div class="login-form-outer">
    @using (Html.BeginForm("Login", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
    {
        @Html.AntiForgeryToken()
        <h4>Use a Local Account to log in.</h4>
        <hr>
        @Html.ValidationSummary(true)
        <div class="form-group">
            @Html.LabelFor(m => m.UserName, new { @class = "col-md-2 control-label" })
            <div class="col-md-10">
                @Html.TextBoxFor(m => m.UserName, new { @class = "form-control" })
                @Html.ValidationMessageFor(m => m.UserName)
            </div>
        </div>
        <div class="form-group">
            @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" })
            <div class="col-md-10">
                @Html.PasswordFor(m => m.Password, new { @class = "form-control" })
                @Html.ValidationMessageFor(m => m.Password)
            </div>
        </div>
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <div class="checkbox">
                    @Html.CheckBoxFor(m => m.RememberMe)
                    @Html.LabelFor(m => m.RememberMe)
                </div>
            </div>
        </div>
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Log in" class="btn btn-default" />
            </div>
        </div>
    }
</div>

Run this and see what you get. You should be able to enter any username and password – wrong ones will get you an Invalid username or password message. The correct admin password will get you to your home screen. We have not integrated our identity information into the existing views yet – we’ve just authenticated. My simple home page looks like this now:

@{
    ViewBag.Title = "Home Page";
}

<h1>Hooray!  Let's take a look at the user</h1>

<p>Everything is in User.Identity</p>
<ul>
    <li>AuthenticationType: @User.Identity.AuthenticationType</li>
    <li>IsAuthenticated: @User.Identity.IsAuthenticated</li>
    <li>Name: @User.Identity.Name</li>
    <li>IsInRole("admin"): @User.IsInRole("admin")</li>
</ul>

As you can see when you run this I can get the username, the admin status and the authentication type.

In my next post I will take a look at what is required to register a new account for my application. Until then, check out my GitHub Repository for the latest code bundle.