Adding Authentication and Authorization - Code Architecture - ASP.NET MVC 5 with Bootstrap and Knockout.js (2015)

ASP.NET MVC 5 with Bootstrap and Knockout.js (2015)

Part III. Code Architecture

Chapter 10. Adding Authentication and Authorization

In this chapter, I will demonstrate how to create your own Authentication and Authorization filters. There are many tutorials available on the Internet about setting up FormsAuthentication together with ASP.NET Membership to manage users in your application — in fact, this is a built-in option when you create a new MVC application with Visual Studio. To avoid reinventing the wheel, this chapter will implement Basic Access Authentication. Basic authentication allows a web browser to provide a username and password when performing a request against the web server. The authentication is provided in the HTTP Headers as a Base64-encoded string.

Authentication Overview

Authentication filters did not exist prior to MVC 5; instead, it was mixed together in a single Authorization filter. As of MVC 5, there is a nice and clear separation of concerns with authentication and authorization.

Creating a filter involves implementing two functions:

OnAuthentication

This function is called at the start of the life cycle and is responsible for validating the credentials, if supplied. This is described in more detail in the following text.

OnAuthenticationChallenge

This function is called at the end of the life cycle for every request. It is responsible for requesting authentication when the request is unauthorized.

The role of the OnAuthentication function is three-fold (a flowchart is shown in Figure 10-1):

1. If no authentication is provided, the filter does nothing. This is important because it clearly implies that the Authentication filter doesn’t prevent requests because the authentication was not provided. It is left to the Authorization filters to determine whether the user must be authenticated to proceed.

2. If authentication is provided and the credentials are valid, the Authentication filter defines an identity to the application context with an authenticated principal (commonly a user).

3. If authentication is provided and the credentials are invalid, the Authentication filter sets an error result with an unauthorized request. The MVC framework is notified that authentication has failed and should not proceed further.

Figure 10-1. OnAuthentication flowchart

Authorization Overview

An Authorization filter no longer needs to validate credentials; instead, it can focus on whether an authenticated principal is set. If the principal is not authenticated, the filter will set the request as unauthorized. Furthermore, an Authorization filter can perform further validation. For example, the default Authorize attribute can optionally validate that the authenticated principal exists within a specific group, allowing easy role-based authorization.

Figure 10-2 contains a flowchart that demonstrates how the request begins with the Authentication filter and, before returning a response, ends with the Authentication filter.

Figure 10-2. Life cycle flowchart

As the flowchart demonstrates, whether the request is successful or unauthenticated/unauthorized before the response is sent back from the server, the OnAuthenticationChallenge is called for each request. If the request is not authenticated, it does not proceed on through to authorization. Likewise, if the request is not authorized, it does not proceed with executing the requested action.

Implementing an Authentication Filter

Creating your own Authentication filter involves inheriting from the same ActionFilterAttribute used for common Action filters (described in Chapter 9), as well as implementing the IAuthenticationFilter interface.

Implementing the IAuthenticationFilter interface involves creating the two aforementioned functions: OnAuthentication and OnAuthenticationChallenge (shown in Example 10-1). I’ve decided to call this class BasicAuthenticationAttribute and have created it within the existing Filtersfolder.

Example 10-1. Empty Authentication filter

using BootstrapIntroduction.Models;

using System;

using System.Linq;

using System.Net;

using System.Security.Principal;

using System.Text;

using System.Web.Mvc;

using System.Web.Mvc.Filters;

namespace BootstrapIntroduction.Filters

{

public class BasicAuthenticationAttribute

: ActionFilterAttribute, IAuthenticationFilter

{

public void OnAuthentication(AuthenticationContext filterContext)

{

}

public void OnAuthenticationChallenge(AuthenticationChallengeContext

filterContext)

{

}

}

}

The first thing the OnAuthentication function will do is check whether the Authorization header is set in the filterContext request headers. If no authorization is found, or it doesn’t contain the word “Basic” in it, the function returns and stops processing (shown in Example 10-2).

Example 10-2. Checking for authorization

using BootstrapIntroduction.Models;

using System;

using System.Linq;

using System.Net;

using System.Security.Principal;

using System.Text;

using System.Web.Mvc;

using System.Web.Mvc.Filters;

namespace BootstrapIntroduction.Filters

{

public class BasicAuthenticationAttribute

: ActionFilterAttribute, IAuthenticationFilter

{

public void OnAuthentication(AuthenticationContext filterContext)

{

var request = filterContext.HttpContext.Request;

var authorization = request.Headers["Authorization"];

// No authorization, do nothing

if (string.IsNullOrEmpty(authorization) || !authorization.Contains("Basic"))

return;

}

public void OnAuthenticationChallenge(AuthenticationChallengeContext

filterContext)

{

}

}

}

If the authorization is found in the request header, the function proceeds to parse out the username and password from the header. The authorization is Base64 encoded, so the first thing to do is Base64 decode the string. At this time, it is also removing the word “Basic” to focus on extracting the username and password. The decoded string is stored in a byte array, so this is extracted into a usable string. And finally, with that string, the username and password are separated by a colon (:), so the string is split up by this value and stores the username and password into local variables for further use. This is shown in Example 10-3.

Example 10-3. Extracting the username and password

using BootstrapIntroduction.Models;

using System;

using System.Linq;

using System.Net;

using System.Security.Principal;

using System.Text;

using System.Web.Mvc;

using System.Web.Mvc.Filters;

namespace BootstrapIntroduction.Filters

{

public class BasicAuthenticationAttribute

: ActionFilterAttribute, IAuthenticationFilter

{

public void OnAuthentication(AuthenticationContext filterContext)

{

var request = filterContext.HttpContext.Request;

var authorization = request.Headers["Authorization"];

// No authorization, do nothing

if (string.IsNullOrEmpty(authorization) || !authorization.Contains("Basic"))

return;

// Parse username and password from header

byte[] encodedDataAsBytes = Convert.FromBase64String(

authorization.Replace("Basic ", ""));

string value = Encoding.ASCII.GetString(encodedDataAsBytes);

string username = value.Substring(0, value.IndexOf(':'));

string password = value.Substring(value.IndexOf(':') + 1);

}

public void OnAuthenticationChallenge(AuthenticationChallengeContext

filterContext)

{

}

}

}

Now it’s time for the validation. Two different validation checks are performed. First, I think it’s a good idea to ensure that both the username and password are not empty strings. If either are, the result is set to an HttpUnauthorizedResult.

Once we know the username and password are properly set, they are used to find a valid user of the system. In this example, I’ve created a new User model (shown in Example 10-7) and an AuthenticatedUsers list (shown in Example 10-8) that contains a list of valid usernames and passwords. This list is searched for a matching username and password combination. If no user is found, the result is set to an HttpUnauthorizedResult. If a user is found, a new GenericPrincipal is instantiated with the user that matched the criteria. This is shown in Example 10-4.

Example 10-4. Authenticating the user

using BootstrapIntroduction.Models;

using System;

using System.Linq;

using System.Net;

using System.Security.Principal;

using System.Text;

using System.Web.Mvc;

using System.Web.Mvc.Filters;

namespace BootstrapIntroduction.Filters

{

public class BasicAuthenticationAttribute

: ActionFilterAttribute, IAuthenticationFilter

{

public void OnAuthentication(AuthenticationContext filterContext)

{

var request = filterContext.HttpContext.Request;

var authorization = request.Headers["Authorization"];

// No authorization, do nothing

if (string.IsNullOrEmpty(authorization) || !authorization.Contains("Basic"))

return;

// Parse username and password from header

byte[] encodedDataAsBytes = Convert.FromBase64String(

authorization.Replace("Basic ", ""));

string value = Encoding.ASCII.GetString(encodedDataAsBytes);

string username = value.Substring(0, value.IndexOf(':'));

string password = value.Substring(value.IndexOf(':') + 1);

if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))

{

filterContext.Result = new HttpUnauthorizedResult(

"Username or password missing");

return;

}

// Validate username and password

var user = AuthenticatedUsers.Users

.FirstOrDefault(u => u.Name == username && u.Password

== password);

if (user == null)

{

filterContext.Result = new HttpUnauthorizedResult(

"Invalid username and password");

return;

}

// Set principal

filterContext.Principal = new GenericPrincipal(user, user.Roles);

}

public void OnAuthenticationChallenge(AuthenticationChallengeContext

filterContext)

{

}

}

}

And finally, to complete the Authentication filter, the OnAuthenticationChallenge that executes every time will generate a new result that encapsulates the current result (more on this in a minute). Once the request is executed, it can assert that the StatusCode is not set to 401 Unauthorized. If it is an unauthorized request, it adds a WWW-Authenticate header with the value of Basic. When a browser receives this header, it will prompt the user for credentials as shown in Figure 10-3. Example 10-5 demonstrates the OnAuthenticationChallenge function.

Example 10-5. OnAuthenticationChallenge

using BootstrapIntroduction.Models;

using System;

using System.Linq;

using System.Net;

using System.Security.Principal;

using System.Text;

using System.Web.Mvc;

using System.Web.Mvc.Filters;

namespace BootstrapIntroduction.Filters

{

public class BasicAuthenticationAttribute

: ActionFilterAttribute, IAuthenticationFilter

{

public void OnAuthentication(AuthenticationContext filterContext)

{

// Truncated for example

}

public void OnAuthenticationChallenge(AuthenticationChallengeContext

filterContext)

{

filterContext.Result = new BasicChallengeActionResult

{

CurrrentResult = filterContext.Result

};

}

}

}

Example 10-5 sets the result to a newly generated BasicChallengeActionResult (shown in Example 10-6). This class extends the basic ActionResult and contains a public property called CurrentResult. The OnAuthenticationChallenge function instantiates this class and sets theCurrentResult with the current filterContext.Result. Example 10-6 overrides the ExecuteResult function, and the first thing it does is execute the CurrentResult. Doing this will allow the next step to happen, which is to determine if the response is 401 unauthorized. If it is, the WWW-Authenticate header is added.

Example 10-6. BasicChallengeActionResult

using BootstrapIntroduction.Models;

using System;

using System.Linq;

using System.Net;

using System.Security.Principal;

using System.Text;

using System.Web.Mvc;

using System.Web.Mvc.Filters;

namespace BootstrapIntroduction.Filters

{

public class BasicAuthenticationAttribute : ActionFilterAttribute,

IAuthenticationFilter

{

public void OnAuthentication(AuthenticationContext filterContext)

{

// Truncated for example

}

public void OnAuthenticationChallenge(AuthenticationChallengeContext

filterContext)

{

filterContext.Result = new BasicChallengeActionResult

{

CurrrentResult = filterContext.Result

};

}

}

class BasicChallengeActionResult : ActionResult

{

public ActionResult CurrrentResult { get; set; }

public override void ExecuteResult(ControllerContext context)

{

CurrrentResult.ExecuteResult(context);

var response = context.HttpContext.Response;

if (response.StatusCode == (int)HttpStatusCode.Unauthorized)

response.AddHeader("WWW-Authenticate", "Basic");

}

}

}

Figure 10-3. Basic authentication

Before this example will compile, the User model class and AuthenticatedUsers class need to be created. Example 10-7 creates a new class in the Models directory called User.

Example 10-7. User Model

using System.Collections.Generic;

using System.Security.Principal;

namespace BootstrapIntroduction.Models

{

public class User : IIdentity

{

public User(string username, string password, string[] roles,

List<string> validIpAddresses)

{

Name = username;

Password = password;

Roles = roles;

ValidIpAddresses = validIpAddresses;

}

public string Name { get; private set; }

public string Password { get; private set; }

public string[] Roles { get; private set; }

public List<string> ValidIpAddresses { get; private set; }

public bool IsAuthenticated { get { return true; } }

public string AuthenticationType { get { return "Basic"; } }

}

}

The User model implements the IIdentity interface, which requires three properties to be set: AuthenticationType, IsAuthenticated, and Name. Because the model implements the IIdentity interface, as demonstrated in Example 10-4, the User can be set when the new GenericPrincipal is instantiated.

The User model also contains properties for a list of Roles and a list of ValidIpAddresses and, of course, a password. The username is stored in the required Name property. The list of ValidIpAddresses will be used in the next section when a custom Authorization filter is created.

And finally, a new AuthenticatedUsers class can be created in the same Models folder. Example 10-8 shows the AuthenticatedUsers class.

Example 10-8. AuthenticatedUsers

using System.Collections.Generic;

namespace BootstrapIntroduction.Models

{

public static class AuthenticatedUsers

{

private static List<User> _users = new List<User>

{

new User("jamie", "munro", null, new List<string> { "::1" } )

};

public static List<User> Users { get { return _users; } }

}

}

The AuthenticatedUsers class contains a public list of Users that are used in Example 10-4 to search this list for a user that contains the same username and password. The AuthenticatedUsers is a static class that, during application start, creates a list of valid users. In this scenario, I have created one user with a username of jamie and a password of munro. The roles are set to null because they are currently not required for this example. ValidIpAddresses is instantiated with a single item that contains the value ::1 (more on this in the next section).

Implementing the Authentication filter can be done in one of two ways: globally or on a per-controller/action basis. Because the Authentication filter is only responsible for validating authorization credentials, if they are provided, I think it makes most sense to apply this globally. However, when it comes to authorization, I think it makes more sense to apply on a per-controller/action basis (unless your entire site requires authorization).

Example 10-9 updates the FilterConfig class inside the App_Start directory to register the new BasicAuthenticationAttribute across all requests.

Example 10-9. BasicAuthentication globally

using BootstrapIntroduction.Filters;

using System.Web;

using System.Web.Mvc;

namespace BootstrapIntroduction

{

public class FilterConfig

{

public static void RegisterGlobalFilters(GlobalFilterCollection filters)

{

filters.Add(new HandleErrorAttribute());

filters.Add(new OnExceptionAttribute());

filters.Add(new BasicAuthenticationAttribute());

}

}

}

Implementing an Authorization Filter

Creating your own Authorization filter involves creating a class that extends the existing AuthorizeAttribute and overriding the OnAuthorization function. Example 10-10 creates a new BasicAuthorizationAttribute class inside the existing Filters folder.

Example 10-10. Empty BasicAuthorizationAttribute

using BootstrapIntroduction.Models;

using System.Web.Mvc;

namespace BootstrapIntroduction.Filters

{

public class BasicAuthorizationAttribute : AuthorizeAttribute

{

public override void OnAuthorization(AuthorizationContext filterContext)

{

}

}

}

The first responsibility of the OnAuthorization function is to check that there is a valid User. Example 10-11 demonstrates this, and if there is no user or the user is not authenticated, the result is set to HttpUnauthorizedResult and processing stops.

Example 10-11. Checking for valid user

using BootstrapIntroduction.Models;

using System.Web.Mvc;

namespace BootstrapIntroduction.Filters

{

public class BasicAuthorizationAttribute : AuthorizeAttribute

{

public override void OnAuthorization(AuthorizationContext filterContext)

{

var userIdentity = filterContext.HttpContext.User.Identity as User;

if (userIdentity == null || !userIdentity.IsAuthenticated)

{

filterContext.Result = new HttpUnauthorizedResult();

return;

}

}

}

}

So far, the Authorization filter does nothing custom. If you recall back in the previous section, when the User model was created, it contained a list of validIpAddresses. Let’s put those to good use. Example 10-12 extracts the user’s IP address from the server variables. The IP address can be set in one of three spots, depending on things like if the user is using a proxy, browsing from localhost (like I am), etc.

Example 10-12. Extracting the IP address

using BootstrapIntroduction.Models;

using System.Web.Mvc;

namespace BootstrapIntroduction.Filters

{

public class BasicAuthorizationAttribute : AuthorizeAttribute

{

public override void OnAuthorization(AuthorizationContext filterContext)

{

var userIdentity = filterContext.HttpContext.User.Identity as User;

if (userIdentity == null || !userIdentity.IsAuthenticated)

{

filterContext.Result = new HttpUnauthorizedResult();

return;

}

string visitorIPAddress =

filterContext.HttpContext.Request.ServerVariables["HTTP_X_FORWARDED_FOR"];

if (string.IsNullOrEmpty(visitorIPAddress)) visitorIPAddress =

filterContext.HttpContext.Request.ServerVariables["REMOTE_ADDR"];

if (string.IsNullOrEmpty(visitorIPAddress))

visitorIPAddress = filterContext.HttpContext.Request.UserHostAddress;

}

}

}

With the user’s IP address, the custom authorization will validate that the IP address exists within the logged-in User list of ValidIpAddresses. As shown in Example 10-13, if the IP address is not in the list, the request is set to HttpUnauthorizedResult; otherwise, the authorization has succeeded, and MVC continues executing down the chain to the action requested.

Example 10-13. Validating the IP address

using BootstrapIntroduction.Models;

using System.Web.Mvc;

namespace BootstrapIntroduction.Filters

{

public class BasicAuthorizationAttribute : AuthorizeAttribute

{

public override void OnAuthorization(AuthorizationContext filterContext)

{

var userIdentity = filterContext.HttpContext.User.Identity as User;

if (userIdentity == null || !userIdentity.IsAuthenticated)

{

filterContext.Result = new HttpUnauthorizedResult();

return;

}

string visitorIPAddress =

filterContext.HttpContext.Request.ServerVariables["HTTP_X_FORWARDED_FOR"];

if (string.IsNullOrEmpty(visitorIPAddress)) visitorIPAddress =

filterContext.HttpContext.Request.ServerVariables["REMOTE_ADDR"];

if (string.IsNullOrEmpty(visitorIPAddress))

visitorIPAddress = filterContext.HttpContext.Request.UserHostAddress;

if (userIdentity.ValidIpAddresses != null &&

!userIdentity.ValidIpAddresses.Contains(visitorIPAddress))

{

filterContext.Result = new HttpUnauthorizedResult();

return;

}

}

}

}

Like all filters, the BasicAuthorizationAttribute can be implemented globally or on a per-controller/action level. Based on the current site’s functionality, I would implement it only on the actions that require security. For example, most of the site is public; however, I might only want authenticated users to be able to add, edit, and delete authors. Example 10-14 contains an abbreviated AuthorsController that enforces authorization on the aforementioned actions.

Example 10-14. Adding authorization to AuthorsController

using System;

using System.Collections.Generic;

using System.Data;

using System.Data.Entity;

using System.Linq;

namespace BootstrapIntroduction.Controllers

{

public class AuthorsController : Controller

{

// Truncated for example

// GET: Authors/Create

[BasicAuthorization]

public ActionResult Create()

{

return View("Form", new AuthorViewModel());

}

// GET: Authors/Edit/5

[BasicAuthorization]

public ActionResult Edit(int? id)

{

// Truncated for example

}

// GET: Authors/Delete/5

[BasicAuthorization]

public ActionResult Delete(int? id)

{

// Truncated for example

}

// POST: Authors/Delete/5

[HttpPost, ActionName("Delete")]

[ValidateAntiForgeryToken]

[BasicAuthorization]

public ActionResult DeleteConfirmed(int id)

{

// Truncated for example

}

}

}

If you debug your web application and attempt to add a new author, you would receive a request for authentication as shown earlier in Figure 10-3.

Summary

This chapter has demonstrated how to create your own custom Authentication and Authorization filters. To make the examples more focused on the inner workings of these filters, proper security of passwords and storage of user data in a database was not demonstrated. I would encourage that your next steps be to create a new MVC application from Visual Studio and select one of the built-in authorization methods.

The new project will provide a ton of code to implement FormsAuthentication together with ASP.NET Membership. The ASP.NET Membership provides great functionality for managing your users in a database with password hashing and many other security features. As a great exercise, after you review how it works, try replacing the static AuthenticatedUsers class with the ASP.NET Membership in the BasicAuthenticationAttribute.