CHAPTER 14

image

Applying ASP.NET Identity

In this chapter, I show you how to apply ASP.NET Identity to authenticate and authorize the user accounts created in the previous chapter. I explain how the ASP.NET platform provides a foundation for authenticating requests and how ASP.NET Identity fits into that foundation to authenticate users and enforce authorization through roles. Table 14-1 summarizes this chapter.

Table 14-1. Chapter Summary

Problem

Solution

Listing

Prepare an application for user authentication.

Apply the Authorize attribute to restrict access to action methods and define a controller to which users will be redirected to provide credentials.

14

Authenticate a user.

Check the name and password using the FindAsync method defined by the user manager class and create an implementation of the IIdentity interface using the CreateIdentityMethod. Set an authentication cookie for subsequent requests by calling the SignIn method defined by the authentication manager class.

5

Prepare an application for role-based authorization.

Create a role manager class and register it for instantiation in the OWIN startup class.

68

Create and delete roles.

Use the CreateAsync and DeleteAsync methods defined by the role manager class.

912

Manage role membership.

Use the AddToRoleAsync and RemoveFromRoleAsync methods defined by the user manager class.

1315

Use roles for authorization.

Set the Roles property of the Authorize attribute.

1619

Seed the database with initial content.

Use the database context initialization class.

20, 21

Preparing the Example Project

In this chapter, I am going to continue working on the Users project I created in Chapter 13. No changes to the application components are required.

Authenticating Users

The most fundamental activity for ASP.NET Identity is to authenticate users, and in this section, I explain and demonstrate how this is done. Table 14-2 puts authentication into context.

Table 14-2. Putting Authentication in Context

Question

Answer

What is it?

Authentication validates credentials provided by users. Once the user is authenticated, requests that originate from the browser contain a cookie that represents the user identity.

Why should I care?

Authentication is how you check the identity of your users and is the first step toward restricting access to sensitive parts of the application.

How is it used by the MVC framework?

Authentication features are accessed through the Authorize attribute, which is applied to controllers and action methods in order to restrict access to authenticated users.

image Tip  I use names and passwords stored in the ASP.NET Identity database in this chapter. In Chapter 15, I demonstrate how ASP.NET Identity can be used to authenticate users with a service from Google (Identity also supports authentication for Microsoft, Facebook, and Twitter accounts).

Understanding the Authentication/Authorization Process

The ASP.NET Identity system integrates into the ASP.NET platform, which means you use the standard MVC framework techniques to control access to action methods, such as the Authorize attribute. In this section, I am going to apply basic restrictions to the Index action method in the Home controller and then implement the features that allow users to identify themselves so they can gain access to it. Listing 14-1 shows how I have applied the Authorize attribute to the Home controller.

Listing 14-1.  Securing the Home Controller

using System.Web.Mvc;
using System.Collections.Generic;
 
namespace Users.Controllers {
 
    public class HomeController : Controller {
 
        [Authorize]
        public ActionResult Index() {
            Dictionary<string, object> data
                = new Dictionary<string, object>();
            data.Add("Placeholder", "Placeholder");
            return View(data);
        }
    }
}

Using the Authorize attribute in this way is the most general form of authorization and restricts access to the Index action methods to requests that are made by users who have been authenticated by the application.

If you start the application and request a URL that targets the Index action on the Home controller (/Home/Index, /Home, or just /), you will see the error shown by Figure 14-1.

9781430265412_Fig14-01.jpg

Figure 14-1. Requesting a protected URL

The ASP.NET platform provides some useful information about the user through the HttpContext object, which is used by the Authorize attribute to check the status of the current request and see whether the user has been authenticated. The HttpContext.User property returns an implementation of the IPrincipal interface, which is defined in the System.Security.Principal namespace. The IPrincipal interface defines the property and method shown in Table 14-3.

Table 14-3. The Members Defined by the IPrincipal Interface

Name

Description

Identity

Returns an implementation of the IIdentity interface that describes the user associated with the request.

IsInRole(role)

Returns true if the user is a member of the specified role. See the “Authorizing Users with Roles” section for details of managing authorizations with roles.

The implementation of IIdentity interface returned by the IPrincipal.Identity property provides some basic, but useful, information about the current user through the properties I have described in Table 14-4.

Table 14-4. The Properties Defined by the IIdentity Interface

Name

Description

AuthenticationType

Returns a string that describes the mechanism used to authenticate the user

IsAuthenticated

Returns true if the user has been authenticated

Name

Returns the name of the current user

image Tip  In Chapter 15 I describe the implementation class that ASP.NET Identity uses for the IIdentity interface, which is called ClaimsIdentity.

ASP.NET Identity contains a module that handles the AuthenticateRequest life-cycle event, which I described in Chapter 3, and uses the cookies sent by the browser to establish whether the user has been authenticated. I’ll show you how these cookies are created shortly. If the user is authenticated, the ASP.NET framework module sets the value of the IIdentity.IsAuthenticated property to true and otherwise sets it to false. (I have yet to implement the feature that will allow users to authenticate, which means that the value of the IsAuthenticated property is always false in the example application.)

The Authorize module checks the value of the IsAuthenticated property and, finding that the user isn’t authenticated, sets the result status code to 401 and terminates the request. At this point, the ASP.NET Identity module intercepts the request and redirects the user to the /Account/Login URL. This is the URL that I defined in the IdentityConfig class, which I specified in Chapter 13 as the OWIN startup class, like this:

using Microsoft.AspNet.Identity;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Owin;
using Users.Infrastructure;
 
namespace Users {
    public class IdentityConfig {
        public void Configuration(IAppBuilder app) {
 
            app.CreatePerOwinContext<AppUserManager>(AppUserManager.Create);
 
            app.UseCookieAuthentication(new CookieAuthenticationOptions {
                AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
                LoginPath = new PathString("/Account/Login"),
            });
        }
    }
}

The browser requests the /Account/Login URL, but since it doesn’t correspond to any controller or action in the example project, the server returns a 404 – Not Found response, leading to the error message shown in Figure 14-1.

Preparing to Implement Authentication

Even though the request ends in an error message, the request in the previous section illustrates how the ASP.NET Identity system fits into the standard ASP.NET request life cycle. The next step is to implement a controller that will receive requests for the /Account/Login URL and authenticate the user. I started by adding a new model class to the UserViewModels.cs file, as shown in Listing 14-2.

Listing 14-2.  Adding a New Model Class to the UserViewModels.cs File

using System.ComponentModel.DataAnnotations;
 
namespace Users.Models {
 
    public class CreateModel {
        [Required]
        public string Name { get; set; }
        [Required]
        public string Email { get; set; }
        [Required]
        public string Password { get; set; }
    }
 
    public class LoginModel {
        [Required]
        public string Name { get; set; }
        [Required]
        public string Password { get; set; }
    }
}

The new model has Name and Password properties, both of which are decorated with the Required attribute so that I can use model validation to check that the user has provided values.

image Tip  In a real project, I would use client-side validation to check that the user has provided name and password values before submitting the form to the server, but I am going to keep things focused on Identity and the server-side functionality in this chapter. See Pro ASP.NET MVC 5 for details of client-side form validation.

I added an Account controller to the project, as shown in Listing 14-3, with Login action methods to collect and process the user’s credentials. I have not implemented the authentication logic in the listing because I am going to define the view and then walk through the process of validating user credentials and signing users into the application.

Listing 14-3.  The Contents of the AccountController.cs File

using System.Threading.Tasks;
using System.Web.Mvc;
using Users.Models;
 
namespace Users.Controllers {
 
    [Authorize]
    public class AccountController : Controller {
 
        [AllowAnonymous]
        public ActionResult Login(string returnUrl) {
            if (ModelState.IsValid) {
            }
            ViewBag.returnUrl = returnUrl;
            return View();
        }
 
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> Login(LoginModel details, string returnUrl) {
            return View(details);
        }
    }
}

Even though it doesn’t authenticate users yet, the Account controller contains some useful infrastructure that I want to explain separately from the ASP.NET Identity code that I’ll add to the Login action method shortly.

First, notice that both versions of the Login action method take an argument called returnUrl. When a user requests a restricted URL, they are redirected to the /Account/Login URL with a query string that specifies the URL that the user should be sent back to once they have been authenticated. You can see this if you start the application and request the /Home/Index URL. Your browser will be redirected, like this:

/Account/Login?ReturnUrl=%2FHome%2FIndex

The value of the ReturnUrl query string parameter allows me to redirect the user so that navigating between open and secured parts of the application is a smooth and seamless process.

Next, notice the attributes that I have applied to the Account controller. Controllers that manage user accounts contain functionality that should be available only to authenticated users, such as password reset, for example. To that end, I have applied the Authorize attribute to the controller class and then used the AllowAnonymous attribute on the individual action methods. This restricts action methods to authenticated users by default but allows unauthenticated users to log in to the application.

Finally, I have applied the ValidateAntiForgeryToken attribute, which works in conjunction with the Html.AntiForgeryToken helper method in the view and guards against cross-site request forgery. Cross-site forgery exploits the trust that your user has for your application and it is especially important to use the helper and attribute for authentication requests.

image Tip  You can learn more about cross-site request forgery at http://en.wikipedia.org/wiki/Cross-site_request_forgery.

My last preparatory step is to create the view that will be rendered to gather credentials from the user. Listing 14-4 shows the contents of the Views/Account/Login.cshtml file, which I created by right-clicking the Index action method and selecting Add View from the pop-up menu.

Listing 14-4.  The Contents of the Login.cshtml File

@model Users.Models.LoginModel
@{ ViewBag.Title = "Login";}
<h2>Log In</h2>
 
@Html.ValidationSummary()
 
@using (Html.BeginForm()) {
    @Html.AntiForgeryToken();
    <input type="hidden" name="returnUrl" value="@ViewBag.returnUrl" />
    <div class="form-group">
        <label>Name</label>
        @Html.TextBoxFor(x => x.Name, new { @class = "form-control" })
    </div>
    <div class="form-group">
        <label>Password</label>
        @Html.PasswordFor(x => x.Password, new { @class = "form-control" })
    </div>
    <button class="btn btn-primary" type="submit">Log In</button>
}

The only notable aspects of this view are using the Html.AntiForgeryToken helper and creating a hidden input element to preserve the returnUrl argument. In all other respects, this is a standard Razor view, but it completes the preparations for authentication and demonstrates the way that unauthenticated requests are intercepted and redirected. To test the new controller, start the application and request the /Home/Index URL. You will be redirected to the /Account/Login URL, as shown in Figure 14-2.

9781430265412_Fig14-02.jpg

Figure 14-2. Prompting the user for authentication credentials

Adding User Authentication

Requests for protected URLs are being correctly redirected to the Account controller, but the credentials provided by the user are not yet used for authentication. In Listing 14-5, you can see how I have completed the implementation of the Login action.

Listing 14-5.  Adding Authentication to the AccountController.cs File

using System.Threading.Tasks;
using System.Web.Mvc;
using Users.Models;
using Microsoft.Owin.Security;
using System.Security.Claims;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Users.Infrastructure;
using System.Web;
 
namespace Users.Controllers {
 
    [Authorize]
    public class AccountController : Controller {
 
        [AllowAnonymous]
        public ActionResult Login(string returnUrl) {
            ViewBag.returnUrl = returnUrl;
            return View();
        }
 
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> Login(LoginModel details, string returnUrl) {
            if (ModelState.IsValid) {
                AppUser user = await UserManager.FindAsync(details.Name,
                    details.Password);
                if (user == null) {
                    ModelState.AddModelError("", "Invalid name or password.");
                } else {
                    ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user,
                        DefaultAuthenticationTypes.ApplicationCookie);
                    AuthManager.SignOut();
                    AuthManager.SignIn(new AuthenticationProperties {
                        IsPersistent = false}, ident);
                    return Redirect(returnUrl);
                }
            }
            ViewBag.returnUrl = returnUrl;
            return View(details);
        }
 
        private IAuthenticationManager AuthManager {
            get {
                return HttpContext.GetOwinContext().Authentication;
            }
        }
 
        private AppUserManager UserManager {
            get {
                return HttpContext.GetOwinContext().GetUserManager<AppUserManager>();
            }
        }
    }
}

The simplest part is checking the credentials, which I do through the FindAsync method of the AppUserManager class, which you will remember as the user manager class from Chapter 13:

...
AppUser user = await UserManager.FindAsync(details.Name, details.Password);
...

I will be using the AppUserManager class repeatedly in the Account controller, so I defined a property called UserManager that returns the instance of the class using the GetOwinContext extension method for the HttpContext class, just as I did for the Admin controller in Chapter 13.

The FindAsync method takes the account name and password supplied by the user and returns an instance of the user class (AppUser in the example application) if the user account exists and if the password is correct. If there is no such account or the password doesn’t match the one stored in the database, then the FindAsync method returns null, in which case I add an error to the model state that tells the user that something went wrong.

If the FindAsync method does return an AppUser object, then I need to create the cookie that the browser will send in subsequent requests to show they are authenticated. Here are the relevant statements:

...
ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user,
    DefaultAuthenticationTypes.ApplicationCookie);
AuthManager.SignOut();
AuthManager.SignIn(new AuthenticationProperties {IsPersistent = false}, ident);
return Redirect(returnUrl);
...

The first step is to create a ClaimsIdentity object that identifies the user. The ClaimsIdentity class is the ASP.NET Identity implementation of the IIdentity interface that I described in Table 14-4 and that you can see used in the “Using Roles for Authorization” section later in this chapter.

image Tip  Don’t worry about why the class is called ClaimsIdentity at the moment. I explain what claims are and how they can be used in Chapter 15.

Instances of ClaimsIdentity are created by calling the user manager CreateIdentityAsync method, passing in a user object and a value from the DefaultAuthenticationTypes enumeration. The ApplicationCookie value is used when working with individual user accounts.

The next step is to invalidate any existing authentication cookies and create the new one. I defined the AuthManager property in the controller because I’ll need access to the object it provides repeatedly as I build the functionality in this chapter. The property returns an implementation of the IAuthenticationManager interface that is responsible for performing common authentication options. I have described the most useful methods provided by the IAuthenticationManager interface in Table 14-5.

Table 14-5. The Most Useful Methods Defined by the IAuthenticationManager Interface

Name

Description

SignIn(options, identity)

Signs the user in, which generally means creating the cookie that identifies authenticated requests

SignOut()

Signs the user out, which generally means invalidating the cookie that identifies authenticated requests

The arguments to the SignIn method are an AuthenticationProperties object that configures the authentication process and the ClaimsIdentity object. I set the IsPersistent property defined by the AuthenticationProperties object to true to make the authentication cookie persistent at the browser, meaning that the user doesn’t have to authenticate again when starting a new session. (There are other properties defined by the AuthenticationProperties class, but the IsPersistent property is the only one that is widely used at the moment.)

The final step is to redirect the user to the URL they requested before the authentication process started, which I do by calling the Redirect method.

CONSIDERING TWO-FACTOR AUTHENTICATION

I have performed single-factor authentication in this chapter, which is where the user is able to authenticate using a single piece of information known to them in advance: the password.

ASP.NET Identity also supports two-factor authentication, where the user needs something extra, usually something that is given to the user at the moment they want to authenticate. The most common examples are a value from a SecureID token or an authentication code that is sent as an e-mail or text message (strictly speaking, the two factors can be anything, including fingerprints, iris scans, and voice recognition, although these are options that are rarely required for most web applications).

Security is increased because an attacker needs to know the user’s password and have access to whatever provides the second factor, such an e-mail account or cell phone.

I don’t show two-factor authentication in the book for two reasons. The first is that it requires a lot of preparatory work, such as setting up the infrastructure that distributes the second-factor e-mails and texts and implementing the validation logic, all of which is beyond the scope of this book.

The second reason is that two-factor authentication forces the user to remember to jump through an additional hoop to authenticate, such as remembering their phone or keeping a security token nearby, something that isn’t always appropriate for web applications. I carried a SecureID token of one sort or another for more than a decade in various jobs, and I lost count of the number of times that I couldn’t log in to an employer’s system because I left the token at home.

If you are interested in two-factor security, then I recommend relying on a third-party provider such as Google for authentication, which allows the user to choose whether they want the additional security (and inconvenience) that two-factor authentication provides. I demonstrate third-party authentication in Chapter 15.

Testing Authentication

To test user authentication, start the application and request the /Home/Index URL. When redirected to the /Account/Login URL, enter the details of one of the users I listed at the start of the chapter (for instance, the name joe and the password MySecret). Click the Log In button, and your browser will be redirected back to the /Home/Index URL, but this time it will submit the authentication cookie that grants it access to the action method, as shown in Figure 14-3.

9781430265412_Fig14-03.jpg

Figure 14-3. Authenticating a user

Authorizing Users with Roles

image Tip  You can use the browser F12 tools to see the cookies that are used to identify authenticated requests.

In the previous section, I applied the Authorize attribute in its most basic form, which allows any authenticated user to execute the action method. In this section, I will show you how to refine authorization to give finer-grained control over which users can perform which actions. Table 14-6 puts authorization in context.

Table 14-6. Putting Authorization in Context

Question

Answer

What is it?

Authorization is the process of granting access to controllers and action methods to certain users, generally based on role membership.

Why should I care?

Without roles, you can differentiate only between users who are authenticated and those who are not. Most applications will have different types of users, such as customers and administrators.

How is it used by the MVC framework?

Roles are used to enforce authorization through the Authorize attribute, which is applied to controllers and action methods.

image Tip  In Chapter 15, I show you a different approach to authorization using claims, which are an advanced ASP.NET Identity feature.

Adding Support for Roles

ASP.NET Identity provides a strongly typed base class for accessing and managing roles called RoleManager<T>, where T is the implementation of the IRole interface supported by the storage mechanism used to represent roles. The Entity Framework uses a class called IdentityRole to implement the IRole interface, which defines the properties shown in Table 14-7.

Table 14-7. The Properties Defined by the IdentityRole Class

Name

Description

Id

Defines the unique identifier for the role

Name

Defines the name of the role

Users

Returns a collection of IdentityUserRole objects that represents the members of the role

I don’t want to leak references to the IdentityRole class throughout my application because it ties me to the Entity Framework for storing role data, so I start by creating an application-specific role class that is derived from IdentityRole. I added a class file called AppRole.cs to the Models folder and used it to define the class shown in Listing 14-6.

Listing 14-6.  The Contents of the AppRole.cs File

using Microsoft.AspNet.Identity.EntityFramework;
 
namespace Users.Models {
    public class AppRole : IdentityRole {
 
        public AppRole() : base() {}
 
        public AppRole(string name) : base(name) { }
    }
}

The RoleManager<T> class operates on instances of the IRole implementation class through the methods and properties shown in Table 14-8.

Table 14-8. The Members Defined by the RoleManager<T> Class

Name

Description

CreateAsync(role)

Creates a new role

DeleteAsync(role)

Deletes the specified role

FindByIdAsync(id)

Finds a role by its ID

FindByNameAsync(name)

Finds a role by its name

RoleExistsAsync(name)

Returns true if a role with the specified name exists

UpdateAsync(role)

Stores changes to the specified role

Roles

Returns an enumeration of the roles that have been defined

These methods follow the same basic pattern of the UserManager<T> class that I described in Chapter 13. Following the pattern I used for managing users, I added a class file called AppRoleManager.cs to the Infrastructure folder and used it to define the class shown in Listing 14-7.

Listing 14-7.  The Contents of the AppRoleManager.cs File

using System;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using Users.Models;
 
namespace Users.Infrastructure {
 
    public class AppRoleManager : RoleManager<AppRole>, IDisposable {
 
        public AppRoleManager(RoleStore<AppRole> store)
            : base(store) {
        }
 
        public static AppRoleManager Create(
                IdentityFactoryOptions<AppRoleManager> options,
                IOwinContext context) {
            return new AppRoleManager(new
                RoleStore<AppRole>(context.Get<AppIdentityDbContext>()));
        }
    }
}

This class defines a Create method that will allow the OWIN start class to create instances for each request where Identity data is accessed, which means I don’t have to disseminate details of how role data is stored throughout the application. I can just obtain and operate on instances of the AppRoleManager class. You can see how I have registered the role manager class with the OWIN start class, IdentityConfig, in Listing 14-8. This ensures that instances of the AppRoleManager class are created using the same Entity Framework database context that is used for the AppUserManager class.

Listing 14-8.  Creating Instances of the AppRoleManager Class in the IdentityConfig.cs File

using Microsoft.AspNet.Identity;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Owin;
using Users.Infrastructure;
 
namespace Users {
    public class IdentityConfig {
        public void Configuration(IAppBuilder app) {
 
            app.CreatePerOwinContext<AppIdentityDbContext>(AppIdentityDbContext.Create);
            app.CreatePerOwinContext<AppUserManager>(AppUserManager.Create);
            app.CreatePerOwinContext<AppRoleManager>(AppRoleManager.Create);
 
            app.UseCookieAuthentication(new CookieAuthenticationOptions {
                AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
                LoginPath = new PathString("/Account/Login"),
            });
        }
    }
}

Creating and Deleting Roles

Having prepared the application for working with roles, I am going to create an administration tool for managing them. I will start the basics and define action methods and views that allow roles to be created and deleted. I added a controller called RoleAdmin to the project, which you can see in Listing 14-9.

Listing 14-9.  The Contents of the RoleAdminController.cs File

using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Users.Infrastructure;
using Users.Models;
 
namespace Users.Controllers {
    public class RoleAdminController : Controller {
 
        public ActionResult Index() {
            return View(RoleManager.Roles);
        }
 
        public ActionResult Create() {
            return View();
        }
 
        [HttpPost]
        public async Task<ActionResult> Create([Required]string name) {
            if (ModelState.IsValid) {
                IdentityResult result
                    = await RoleManager.CreateAsync(new AppRole(name));
                if (result.Succeeded) {
                    return RedirectToAction("Index");
                } else {
                    AddErrorsFromResult(result);
                }
            }
            return View(name);
        }
 
        [HttpPost]
        public async Task<ActionResult> Delete(string id) {
            AppRole role = await RoleManager.FindByIdAsync(id);
            if (role != null) {
                IdentityResult result = await RoleManager.DeleteAsync(role);
                if (result.Succeeded) {
                    return RedirectToAction("Index");
                } else {
                    return View("Error", result.Errors);
                }
            } else {
                return View("Error", new string[] { "Role Not Found" });
            }
        }
 
        private void AddErrorsFromResult(IdentityResult result) {
            foreach (string error in result.Errors) {
                ModelState.AddModelError("", error);
            }
        }
 
        private AppUserManager UserManager {
            get {
                return HttpContext.GetOwinContext().GetUserManager<AppUserManager>();
            }
        }
 
        private AppRoleManager RoleManager {
            get {
                return HttpContext.GetOwinContext().GetUserManager<AppRoleManager>();
            }
        }
    }
}

I have applied many of the same techniques that I used in the Admin controller in Chapter 13, including a UserManager property that obtains an instance of the AppUserManager class and an AddErrorsFromResult method that processes the errors reported in an IdentityResult object and adds them to the model state.

I have also defined a RoleManager property that obtains an instance of the AppRoleManager class, which I used in the action methods to obtain and manipulate the roles in the application. I am not going to describe the action methods in detail because they follow the same pattern I used in Chapter 13, using the AppRoleManager class in place of AppUserManager and calling the methods I described in Table 14-8.

Creating the Views

The views for the RoleAdmin controller are standard HTML and Razor markup, but I have included them in this chapter so that you can re-create the example. I want to display the names of the users who are members of each role. The Entity Framework IdentityRole class defines a Users property that returns a collection of IdentityUserRole user objects representing the members of the role. Each IdentityUserRole object has a UserId property that returns the unique ID of a user, and I want to get the username for each ID. I added a class file called IdentityHelpers.cs to the Infrastructure folder and used it to define the class shown in Listing 14-10.

Listing 14-10.  The Contents of the IdentityHelpers.cs File

using System.Web;
using System.Web.Mvc;
using Microsoft.AspNet.Identity.Owin;
 
namespace Users.Infrastructure {
    public static class IdentityHelpers {
        public static MvcHtmlString GetUserName(this HtmlHelper html, string id) {
            AppUserManager mgr
                =  HttpContext.Current.GetOwinContext().GetUserManager<AppUserManager>();
            return new MvcHtmlString(mgr.FindByIdAsync(id).Result.UserName);
        }
    }
}

Custom HTML helper methods are defined as extensions on the HtmlHelper class. My helper, which is called GetUsername, takes a string argument containing a user ID, obtains an instance of the AppUserManager through the GetOwinContext.GetUserManager method (where GetOwinContext is an extension method on the HttpContext class), and uses the FindByIdAsync method to locate the AppUser instance associated with the ID and to return the value of the UserName property.

Listing 14-11 shows the contents of the Index.cshtml file from the Views/RoleAdmin folder, which I created by right-clicking the Index action method in the code editor and selecting Add View from the pop-up menu.

Listing 14-11.  The Contents of the Index.cshtml File in the Views/RoleAdmin Folder

@using Users.Models
@using Users.Infrastructure
@model IEnumerable<AppRole>
@{ ViewBag.Title = "Roles"; }
<div class="panel panel-primary">
    <div class="panel-heading">Roles</div>
    <table class="table table-striped">
        <tr><th>ID</th><th>Name</th><th>Users</th><th></th></tr>
        @if (Model.Count() == 0) {
            <tr><td colspan="4" class="text-center">No Roles</td></tr>
        } else {
            foreach (AppRole role in Model) {
                <tr>
                    <td>@role.Id</td>
                    <td>@role.Name</td>
                    <td>
                        @if (role.Users == null || role.Users.Count == 0) {
                            @: No Users in Role
                        } else {
                            <p>@string.Join(", ", role.Users.Select(x =>
                                Html.GetUserName(x.UserId)))</p>
                        }
                    </td>
                    <td>
                        @using (Html.BeginForm("Delete", "RoleAdmin",
                            new { id = role.Id })) {
                            @Html.ActionLink("Edit", "Edit", new { id = role.Id },
                                    new { @class = "btn btn-primary btn-xs" })
                            <button class="btn btn-danger btn-xs"
                                    type="submit">
                                Delete
                            </button>
                        }
                    </td>
                </tr>
            }
        }
    </table>
</div>
@Html.ActionLink("Create", "Create", null, new { @class = "btn btn-primary" })

This view displays a list of the roles defined by the application, along with the users who are members, and I use the GetUserName helper method to get the name for each user.

Listing 14-12 shows the Views/RoleAdmin/Create.cshtml file, which I created to allow new roles to be created.

Listing 14-12.  The Contents of the Create.cshtml File in the Views/RoleAdmin Folder

@model string
@{ ViewBag.Title = "Create Role";}
<h2>Create Role</h2>
@Html.ValidationSummary(false)
@using (Html.BeginForm()) {
    <div class="form-group">
        <label>Name</label>
        <input name="name" value="@Model" class="form-control" />
    </div>
    <button type="submit" class="btn btn-primary">Create</button>
    @Html.ActionLink("Cancel", "Index", null, new { @class = "btn btn-default" })
}

The only information required to create a new view is a name, which I gather using a standard input element and submit the value to the Create action method.

Testing Creating and Deleting Roles

To test the new controller, start the application and navigate to the /RoleAdmin/Index URL. To create a new role, click the Create button, enter a name in the input element, and click the second Create button. The new view will be saved to the database and displayed when the browser is redirected to the Index action, as shown in Figure 14-4. You can remove the role from the application by clicking the Delete button.

9781430265412_Fig14-04.jpg

Figure 14-4. Creating a new role

Managing Role Memberships

To authorize users, it isn’t enough to just create and delete roles; I also have to be able to manage role memberships, assigning and removing users from the roles that the application defines. This isn’t a complicated process, but it invokes taking the role data from the AppRoleManager class and then calling the methods defined by the AppUserMangager class that associate users with roles.

I started by defining view models that will let me represent the membership of a role and receive a new set of membership instructions from the user. Listing 14-13 shows the additions I made to the UserViewModels.cs file.

Listing 14-13.  Adding View Models to the UserViewModels.cs File

using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
 
namespace Users.Models {
 
    public class CreateModel {
        [Required]
        public string Name { get; set; }
        [Required]
        public string Email { get; set; }
        [Required]
        public string Password { get; set; }
    }
 
    public class LoginModel {
        [Required]
        public string Name { get; set; }
        [Required]
        public string Password { get; set; }
    }
 
    public class RoleEditModel {
        public AppRole Role { get; set; }
        public IEnumerable<AppUser> Members { get; set; }
        public IEnumerable<AppUser> NonMembers { get; set; }
    }
 
    public class RoleModificationModel {
        [Required]
        public string RoleName { get; set; }
        public string[] IdsToAdd { get; set; }
        public string[] IdsToDelete { get; set; }
    }
}

The RoleEditModel class will let me pass details of a role and details of the users in the system, categorized by membership. I use AppUser objects in the view model so that I can extract the name and ID for each user in the view that will allow memberships to be edited. The RoleModificationModel class is the one that I will receive from the model binding system when the user submits their changes. It contains arrays of user IDs rather than AppUser objects, which is what I need to change role memberships.

Having defined the view models, I can add the action methods to the controller that will allow role memberships to be defined. Listing 14-14 shows the changes I made to the RoleAdmin controller.

Listing 14-14.  Adding Action Methods in the RoleAdminController.cs File

using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Users.Infrastructure;
using Users.Models;
using System.Collections.Generic;
 
namespace Users.Controllers {
    public class RoleAdminController : Controller {
 
        //...other action methods omitted for brevity...
 
        public async Task<ActionResult> Edit(string id) {
            AppRole role = await RoleManager.FindByIdAsync(id);
            string[] memberIDs = role.Users.Select(x => x.UserId).ToArray();
            IEnumerable<AppUser> members
                = UserManager.Users.Where(x => memberIDs.Any(y => y == x.Id));
            IEnumerable<AppUser> nonMembers = UserManager.Users.Except(members);
            return View(new RoleEditModel {
                Role = role,
                Members = members,
                NonMembers = nonMembers
            });
        }
 
        [HttpPost]
        public async Task<ActionResult> Edit(RoleModificationModel model) {
            IdentityResult result;
            if (ModelState.IsValid) {
                foreach (string userId in model.IdsToAdd ?? new string[] { }) {
                    result = await UserManager.AddToRoleAsync(userId, model.RoleName);
                    if (!result.Succeeded) {
                        return View("Error", result.Errors);
                    }
                }
                foreach (string userId in model.IdsToDelete ?? new string[] { }) {
                    result = await UserManager.RemoveFromRoleAsync(userId,
                        model.RoleName);
                    if (!result.Succeeded) {
                        return View("Error", result.Errors);
                    }
                }
                return RedirectToAction("Index");
            }
            return View("Error", new string[] { "Role Not Found" });
        }
 
        private void AddErrorsFromResult(IdentityResult result) {
            foreach (string error in result.Errors) {
                ModelState.AddModelError("", error);
            }
        }
 
        private AppUserManager UserManager {
            get {
                return HttpContext.GetOwinContext().GetUserManager<AppUserManager>();
            }
        }
 
        private AppRoleManager RoleManager {
            get {
                return HttpContext.GetOwinContext().GetUserManager<AppRoleManager>();
            }
        }
    }
}

The majority of the code in the GET version of the Edit action method is responsible for generating the sets of members and nonmembers of the selected role, which is done using LINQ. Once I have grouped the users, I call the View method, passing a new instance of the RoleEditModel class I defined in Listing 14-13.

The POST version of the Edit method is responsible for adding and removing users to and from roles. The AppUserManager class inherits a number of role-related methods from its base class, which I have described in Table 14-9.

Table 14-9. The Role-Related Methods Defined by the UserManager<T> Class

Name

Description

AddToRoleAsync(id, name)

Adds the user with the specified ID to the role with the specified name

GetRolesAsync(id)

Returns a list of the names of the roles of which the user with the specified ID is a member

IsInRoleAsync(id, name)

Returns true if the user with the specified ID is a member of the role with the specified name

RemoveFromRoleAsync(id, name)

Removes the user with the specified ID as a member from the role with the specified name

An oddity of these methods is that the role-related methods operate on user IDs and role names, even though roles also have unique identifiers. It is for this reason that my RoleModificationModel view model class has a RoleName property.

Listing 14-15 shows the view for the Edit.cshtml file, which I added to the Views/RoleAdmin folder and used to define the markup that allows the user to edit role memberships.

Listing 14-15.  The Contents of the Edit.cshtml File in the Views/RoleAdmin Folder

@using Users.Models
@model RoleEditModel
@{ ViewBag.Title = "Edit Role";}
@Html.ValidationSummary()
@using (Html.BeginForm()) {
    <input type="hidden" name="roleName" value="@Model.Role.Name" />
    <div class="panel panel-primary">
        <div class="panel-heading">Add To @Model.Role.Name</div>
        <table class="table table-striped">
            @if (Model.NonMembers.Count() == 0) {
                <tr><td colspan="2">All Users Are Members</td></tr>
            } else {
                <tr><td>User ID</td><td>Add To Role</td></tr>
                foreach (AppUser user in Model.NonMembers) {
                    <tr>
                        <td>@user.UserName</td>
                        <td>
                            <input type="checkbox" name="IdsToAdd" value="@user.Id">
                        </td>
                    </tr>
                }
            }
        </table>
    </div>
    <div class="panel panel-primary">
        <div class="panel-heading">Remove from @Model.Role.Name</div>
        <table class="table table-striped">
            @if (Model.Members.Count() == 0) {
                <tr><td colspan="2">No Users Are Members</td></tr>
            } else {
                <tr><td>User ID</td><td>Remove From Role</td></tr>
                foreach (AppUser user in Model.Members) {
                    <tr>
                        <td>@user.UserName</td>
                        <td>
                            <input type="checkbox" name="IdsToDelete" value="@user.Id">
                        </td>
                    </tr>
                }
            }
        </table>
    </div>
    <button type="submit" class="btn btn-primary">Save</button>
    @Html.ActionLink("Cancel", "Index", null, new { @class = "btn btn-default" })
}

The view contains two tables: one for users who are not members of the selected role and one for those who are members. Each user’s name is displayed along with a check box that allows the membership to be changed.

Testing Editing Role Membership

Adding the AppRoleManager class to the application causes the Entity Framework to delete the contents of the database and rebuild the schema, which means that any users you created in the previous chapter have been removed. So that there are users to assign to roles, start the application and navigate to the /Admin/Index URL and create users with the details in Table 14-10.

Table 14-10. The Values for Creating Example User

Name

Email

Password

Alice

[email protected]

MySecret

Bob

[email protected]

MySecret

Joe

[email protected]

MySecret

image Tip  Deleting the user database is fine for an example application but tends to be a problem in real applications. I show you how to gracefully manage changes to the database schema in Chapter 15.

To test managing role memberships, navigate to the /RoleAdmin/Index URL and create a role called Users, following the instructions from the “Testing, Creating, and Deleting Roles” section. Click the Edit button and check the boxes so that Alice and Joe are members of the role but Bob is not, as shown in Figure 14-5.

9781430265412_Fig14-05.jpg

Figure 14-5. Editing role membership

image Tip  If you get an error that tells you there is already an open a data reader, then you didn’t set the MultipleActiveResultSets setting to true in the connection string in Chapter 13.

Click the Save button, and the controller will update the role memberships and redirect the browser to the Index action. The summary of the Users role will show that Alice and Joe are now members, as illustrated by Figure 14-6.

9781430265412_Fig14-06.jpg

Figure 14-6. The effect of adding users to a role

Using Roles for Authorization

Now that I have the ability to manage roles, I can use them as the basis for authorization through the Authorize attribute. To make it easier to test role-based authorization, I have added a Logout method to the Account controller, as shown in Listing 14-16, which will make it easier to log out and log in again as a different user to see the effect of role membership.

Listing 14-16.  Adding a Logout Method to the AccountController.cs File

using System.Threading.Tasks;
using System.Web.Mvc;
using Users.Models;
using Microsoft.Owin.Security;
using System.Security.Claims;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Users.Infrastructure;
using System.Web;
 
namespace Users.Controllers {
 
    [Authorize]
    public class AccountController : Controller {
 
        [AllowAnonymous]
        public ActionResult Login(string returnUrl) {
            ViewBag.returnUrl = returnUrl;
            return View();
        }
 
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> Login(LoginModel details, string returnUrl) {
            //...statements omitted for brevity...
        }
 
        [Authorize]
        public ActionResult Logout() {
            AuthManager.SignOut();
            return RedirectToAction("Index", "Home");
        }
 
        private IAuthenticationManager AuthManager {
            get {
                return HttpContext.GetOwinContext().Authentication;
            }
        }
 
        private AppUserManager UserManager {
            get {
                return HttpContext.GetOwinContext().GetUserManager<AppUserManager>();
            }
        }
    }
}

I have updated the Home controller to add a new action method and pass some information about the authenticated user to the view, as shown in Listing 14-17.

Listing 14-17.  Adding an Action Method and Account Information to the HomeController.cs File

using System.Web.Mvc;
using System.Collections.Generic;
using System.Web;
using System.Security.Principal;
 
namespace Users.Controllers {
 
    public class HomeController : Controller {
 
        [Authorize]
        public ActionResult Index() {
            return View(GetData("Index"));
        }
 
        [Authorize(Roles="Users")]
        public ActionResult OtherAction() {
            return View("Index", GetData("OtherAction"));
        }
 
        private Dictionary<string, object> GetData(string actionName) {
            Dictionary<string, object> dict
                = new Dictionary<string, object>();
            dict.Add("Action", actionName);
            dict.Add("User", HttpContext.User.Identity.Name);
            dict.Add("Authenticated", HttpContext.User.Identity.IsAuthenticated);
            dict.Add("Auth Type", HttpContext.User.Identity.AuthenticationType);
            dict.Add("In Users Role", HttpContext.User.IsInRole("Users"));
            return dict;
        }
    }
}

I have left the Authorize attribute unchanged for the Index action method, but I have set the Roles property when applying the attribute to the OtherAction method, specifying that only members of the Users role should be able to access it. I also defined a GetData method, which adds some basic information about the user identity, using the properties available through the HttpContext object. The final change I made was to the Index.cshtml file in the Views/Home folder, which is used by both actions in the Home controller, to add a link that targets the Logout method in the Account controller, as shown in Listing 14-18.

Listing 14-18.  Adding a Sign-Out Link to the Index.cshtml File in the Views/Home Folder

@{ ViewBag.Title = "Index"; }
 
<div class="panel panel-primary">
    <div class="panel-heading">User Details</div>
    <table class="table table-striped">
        @foreach (string key in Model.Keys) {
            <tr>
                <th>@key</th>
                <td>@Model[key]</td>
            </tr>
        }
    </table>
</div>
 
@Html.ActionLink("Sign Out", "Logout", "Account", null, new {@class = "btn btn-primary"})

image Tip  The Authorize attribute can also be used to authorize access based on a list of individual usernames. This is an appealing feature for small projects, but it means you have to change the code in your controllers each time the set of users you are authorizing changes, and that usually means having to go through the test-and-deploy cycle again. Using roles for authorization isolates the application from changes in individual user accounts and allows you to control access to the application through the memberships stored by ASP.NET Identity.

To test the authentication, start the application and navigate to the /Home/Index URL. Your browser will be redirected so that you can enter user credentials. It doesn’t matter which of the user details from Table 14-10 you choose to authenticate with because the Authorize attribute applied to the Index action allows access to any authenticated user.

However, if you now request the /Home/OtherAction URL, the user details you chose from Table 14-10 will make a difference because only Alice and Joe are members of the Users role, which is required to access the OtherAction method. If you log in as Bob, then your browser will be redirected so that you can be prompted for credentials once again.

Redirecting an already authenticated user for more credentials is rarely a useful thing to do, so I have modified the Login action method in the Account controller to check to see whether the user is authenticated and, if so, redirect them to the shared Error view. Listing 14-19 shows the changes.

Listing 14-19.  Detecting Already Authenticated Users in the AccountController.cs File

using System.Threading.Tasks;
using System.Web.Mvc;
using Users.Models;
using Microsoft.Owin.Security;
using System.Security.Claims;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Users.Infrastructure;
using System.Web;
 
namespace Users.Controllers {
 
    [Authorize]
    public class AccountController : Controller {
 
        [AllowAnonymous]
        public ActionResult Login(string returnUrl) {
            if (HttpContext.User.Identity.IsAuthenticated) {
                return View("Error", new string[] { "Access Denied" });
            }
            ViewBag.returnUrl = returnUrl;
            return View();
        }
 
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> Login(LoginModel details, string returnUrl) {
            //...code omitted for brevity...
        }
 
        [Authorize]
        public ActionResult Logout() {
            AuthManager.SignOut();
            return RedirectToAction("Index", "Home");
        }
 
        private IAuthenticationManager AuthManager {
            get {
                return HttpContext.GetOwinContext().Authentication;
            }
        }
 
        private AppUserManager UserManager {
            get {
                return HttpContext.GetOwinContext().GetUserManager<AppUserManager>();
            }
        }
    }
}

Figure 14-7 shows the responses generated for the user Bob when requesting the /Home/Index and /Home/OtherAction URLs.

9781430265412_Fig14-07.jpg

Figure 14-7. Using roles to control access to action methods

image Tip  Roles are loaded when the user logs in, which means if you change the roles for the user you are currently authenticated as, the changes won’t take effect until you log out and authenticate.

Seeding the Database

One lingering problem in my example project is that access to my Admin and RoleAdmin controllers is not restricted. This is a classic chicken-and-egg problem because in order to restrict access, I need to create users and roles, but the Admin and RoleAdmin controllers are the user management tools, and if I protect them with the Authorize attribute, there won’t be any credentials that will grant me access to them, especially when I first deploy the application.

The solution to this problem is to seed the database with some initial data when the Entity Framework Code First feature creates the schema. This allows me to automatically create users and assign them to roles so that there is a base level of content available in the database.

The database is seeded by adding statements to the PerformInitialSetup method of the IdentityDbInit class, which is the application-specific Entity Framework database setup class. Listing 14-20 shows the changes I made to create an administration user.

Listing 14-20.  Seeding the Database in the AppIdentityDbContext.cs File

using System.Data.Entity;
using Microsoft.AspNet.Identity.EntityFramework;
using Users.Models;
using Microsoft.AspNet.Identity;
 
namespace Users.Infrastructure {
    public class AppIdentityDbContext : IdentityDbContext<AppUser> {
 
        public AppIdentityDbContext() : base("IdentityDb") { }
 
        static AppIdentityDbContext() {
            Database.SetInitializer<AppIdentityDbContext>(new IdentityDbInit());
        }
 
        public static AppIdentityDbContext Create() {
            return new AppIdentityDbContext();
        }
    }
 
    public class IdentityDbInit
            : DropCreateDatabaseIfModelChanges<AppIdentityDbContext> {
        protected override void Seed(AppIdentityDbContext context) {
            PerformInitialSetup(context);
            base.Seed(context);
        }
 
        public void PerformInitialSetup(AppIdentityDbContext context) {
            AppUserManager userMgr = new AppUserManager(new UserStore<AppUser>(context));
            AppRoleManager roleMgr = new AppRoleManager(new RoleStore<AppRole>(context));
 
            string roleName = "Administrators";
            string userName = "Admin";
            string password = "MySecret";
            string email = "[email protected]";
 
            if (!roleMgr.RoleExists(roleName)) {
                roleMgr.Create(new AppRole(roleName));
            }
 
            AppUser user = userMgr.FindByName(userName);
            if (user == null) {
                userMgr.Create(new AppUser { UserName = userName, Email = email },
                    password);
                user = userMgr.FindByName(userName);
            }
 
            if (!userMgr.IsInRole(user.Id, roleName)) {
                userMgr.AddToRole(user.Id, roleName);
            }
        }
    }
}

image Tip  For this example, I used the synchronous extension methods to locate and manage the role and user. As I explained in Chapter 13, I prefer the asynchronous methods by default, but the synchronous methods can be useful when you need to perform a sequence of related operations.

I have to create instances of AppUserManager and AppRoleManager directly because the PerformInitialSetup method is called before the OWIN configuration is complete. I use the RoleManager and AppManager objects to create a role called Administrators and a user called Admin and add the user to the role.

image Tip  Read Chapter 15 before you add database seeding to your project. I describe database migrations, which allow you to take control of schema changes in the database and which put the seeding logic in a different place.

With this change, I can use the Authorize attribute to protect the Admin and RoleAdmin controllers. Listing 14-21 shows the change I made to the Admin controller.

Listing 14-21.  Restricting Access in the AdminController.cs File

using System.Web;
using System.Web.Mvc;
using Microsoft.AspNet.Identity.Owin;
using Users.Infrastructure;
using Users.Models;
using Microsoft.AspNet.Identity;
using System.Threading.Tasks;
 
namespace Users.Controllers {
 
    [Authorize(Roles = "Administrators")]
    public class AdminController : Controller {
        //...statements omitted for brevity...
    }
}

Listing 14-22 shows the corresponding change I made to the RoleAdmin controller.

Listing 14-22.  Restricting Access in the RoleAdminController.cs File

using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Users.Infrastructure;
using Users.Models;
using System.Collections.Generic;
 
namespace Users.Controllers {
 
    [Authorize(Roles = "Administrators")]
    public class RoleAdminController : Controller {
        //...statements omitted for brevity...
    }
}

The database is seeded only when the schema is created, which means I need to reset the database to complete the process. This isn’t something you would do in a real application, of course, but I wanted to wait until I demonstrated how authentication and authorization worked before creating the administrator account.

To delete the database, open the Visual Studio SQL Server Object Explorer window and locate and right-click the IdentityDb item. Select Delete from the pop-up menu and check both of the options in the Delete Database dialog window. Click the OK button to delete the database.

Now create an empty database to which the schema will be added by right-clicking the Databases item, selecting Add New Database, and entering IdentityDb in the Database Name field. Click OK to create the empty database.

image Tip  There are step-by-step instructions with screenshots in Chapter 13 for creating the database.

Now start the application and request the /Admin/Index or /RoleAdmin/Index URL. There will be a delay while the schema is created and the database is seeded, and then you will be prompted to enter your credentials. Use Admin as the name and MySecret as the password, and you will be granted access to the controllers.

image Caution  Deleting the database removes the user accounts you created using the details in Table 14-10, which is why you would not perform this task on a live database containing user details.

Summary

In this chapter, I showed you how to use ASP.NET Identity to authenticate and authorize users. I explained how the ASP.NET life-cycle events provide a foundation for authenticating requests, how to collect and validate credentials users, and how to restrict access to action methods based on the roles that a user is a member of. In the next chapter, I demonstrate some of the advanced features that ASP.NET Identity provides.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.59.158.151