Introduction
This article is a continuation of my previous article about “ASP.NET MVC 5: Building Your First Web Application - Part 1”.
What you will learn:
- Creating a Login page that would validate and authenticate users using Forms Authentication
- Creating a custom role-based page authorization using a custom Authorize filter
For this specific demo, I will show how to create a simple Login form by implementing a custom authentication and role-based page authorization, without using ASP.NET Membership or ASP.NET Identity. If you want to build an app that allows users to login using their social media accounts like Facebook, Twitter, Google Plus and so on, then you may want explore ASP.NET Identity instead.
Note that I will not elaborate more on the details about the model, view and controllers function, so before proceeding further, I'd suggest you to check my previous article “ASP.NET MVC 5: Building Your First Web Application - Part 1” first, especially if you are new to ASP.NET MVC web development.
Before we get our hands dirty ,let's talk about a bit of security in general.
Forms Authentication Overview
Security is an integral part of any Web-based application. The majority of web sites currently heavily rely on authentication and authorization for securing their application. You can think of a web site as somewhat analogous to a company office where an office is open for people like applicants or messengers to come, but there are certain parts of the facility, such as workstations and conference rooms, that are accessible only to people, such as employees, with certain credentials. An example is when a you build a shopping cart application that accepts user's credit card information for payment purposes and stores them into your database. ASP.NET helps protect your database from public access by providing authentication and authorization. For more information, read on: ASP.NET Web Application Security Fundamentals
Forms authentication lets you authenticate users using your own code and then maintain an authentication token in a cookie or in the page URL. To use forms authentication, you create a login page that collects credentials from the user that includes code to authenticate the credentials. Typically you configure the application to redirect requests to the login page when users try to access a protected resource, such as a page that requires authentication. If the user's credentials are valid, you can call methods of the FormsAuthentication class to redirect the request back to the originally requested resource with an appropriate authentication ticket (cookie). You can read more about Forms Authentication here.
Let’s get our hands dirty!
As a recap, here's the previous project structure below:
Figure 1: Project Solution Structure
Implementing the Login Page
Enabling Forms Authentication
To allow forms authentication, the very first thing to do in your application is to configure FormsAuthentication that manages forms authentication services to your web application. The default authentication mode for ASP.NET is “Windows”. To enable forms authentication, add the <authentication>
and <forms>
elements under the <system.web>
element in your web.config as in the following:
<system.web>
<authentication mode="Forms">
<forms loginUrl="~/Account/Login" defaultUrl="~/Home/Welcome"></forms>
</authentication>
</system.web>
Setting the loginUrl
enables the application to determine where to redirect an un-authenticated user who attempts to access a secured page. The defaultUrl
redirects users to the specified page after successful log-in.
Adding the UserLoginView Model
Let's go ahead and create a model view class for our Login page by adding the following code within the “UserModel
” class:
public class UserLoginView
{
[Key]
public int SYSUserID { get; set; }
[Required(ErrorMessage = "*")]
[Display(Name = "Login ID")]
public string LoginName { get; set; }
[Required(ErrorMessage = "*")]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; }
}
The fields defined above will be used in our Login page. You may also see that the fields are decorated with Required, Display and DataType attributes. Again, these attributes are called Data Annotations.
Adding the GetUserPassword() Method
Add the following code for the “UserManager
” class:
public string GetUserPassword(string loginName) {
using (DemoDBEntities db = new DemoDBEntities()) {
var user = db.SYSUsers.Where(o => o.LoginName.ToLower().Equals(loginName));
if (user.Any())
return user.FirstOrDefault().PasswordEncryptedText;
else
return string.Empty;
}
}
As the method name suggests, it gets the corresponding password from the database for a specific login name using a LINQ
query.
Adding the Login Action Method
Add the following code for the “AccountController
” class:
public ActionResult LogIn() {
return View();
}
[HttpPost]
public ActionResult LogIn(UserLoginView ULV, string returnUrl) {
if (ModelState.IsValid) {
UserManager UM = new UserManager();
string password = UM.GetUserPassword(ULV.LoginName);
if (string.IsNullOrEmpty(password))
ModelState.AddModelError("", "The user login or password provided is incorrect.");
else {
if (ULV.Password.Equals(password)) {
FormsAuthentication.SetAuthCookie(ULV.LoginName, false);
return RedirectToAction("Welcome", "Home");
}
else {
ModelState.AddModelError("", "The password provided is incorrect.");
}
}
}
return View(ULV);
}
As you can see, there are two methods above with the same name. The first one is the "Login
" method that simply returns the LogIn.cshtml view. We will create this view in the next step. The second one is also named "Login
" but it is decorated with the "[HttpPost]
" attribute. This attribute specifies the overload of the "Login
" method that can be invoked only for POST
requests.
The second method will be triggered once the Button
"LogIn" is fired. What it does is, first it will check if the required fields are supplied, so it checks for ModelState.IsValid
condition. It will then create an instance of the UserManager
class and call the GetUserPassword()
method by passing the user LoginName
value supplied by the user. If the password returns an empty string then it will display an error to the view. If the password supplied is equal to the password retrieved from the database then it will redirect the user to the "Welcome" page, otherwise it will display an error stating that the login name or password supplied is invalid.
Adding the Login View
Before adding the view, be sure to build your application first to ensure that the application is error-free. After a successful build, navigate to the “AccountController
” class and right-click on the Login
action method and then select “Add View”. This will bring up the following dialog below:
Figure 2: Add View dialog
Take note of the values supplied for each field. Now click on Add to let Visual Studio scaffolds the UI for you. Here is the modified HTML markup:
@model MVC5RealWorld.Models.ViewModel.UserLoginView
@{
ViewBag.Title = "LogIn";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>LogIn</h2>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
<hr />
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
<div class="form-group">
@Html.LabelFor(model => model.LoginName, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.LoginName, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.LoginName, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Password, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Password, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.Password, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Login" class="btn btn-default" />
</div>
</div>
</div>
}
<div>
@Html.ActionLink("Back to Main", "Index", "Home")
</div>
Implementing the Logout Functionality
The logout code is quiet simple. Just add the following method below in the AccountController
class.
[Authorize]
public ActionResult SignOut() {
FormsAuthentication.SignOut();
return RedirectToAction("Index", "Home");
}
The FormsAuthentication.SignOut
method removes the forms-authentication ticket from the browser. We then redirect the user to the "Index" page after signing out.
Here’s the corresponding action link for the "Logout" that you can add within your "Home" page:
@Html.ActionLink("Signout","SignOut","Account")
Output
Running your application should display something like the following.
When validation triggers:
Figure 3: Validation triggers
After successful Logging in:
Figure 4: Successful logging-in
After logging out:
Figure 5: After logging out
Pretty simple! Now let’s have a look at how to implement a simple role-based page authorization.
Implementing a Simple Role-Based Page Authorization
Authorization is a function that specifies access rights to a certain resource or page. One practical example is having a page that only a certain user role can have access to. For example, only allow an administrator to access the maintenance page for your application. In this section we will create a simple implementation on how to do that.
Creating the UserIsInRole Method
Add the following code to the "UserManager
" class:
public bool IsUserInRole(string loginName, string roleName) {
using (DemoDBEntities db = new DemoDBEntities()) {
SYSUser SU = db.SYSUsers.Where(o => o.LoginName.ToLower().Equals(loginName))?.FirstOrDefault();
if (SU != null) {
var roles = from q in db.SYSUserRoles
join r in db.LOOKUPRoles on q.LOOKUPRoleID equals r.LOOKUPRoleID
where r.RoleName.Equals(roleName) && q.SYSUserID.Equals(SU.SYSUserID)
select r.RoleName;
if (roles != null) {
return roles.Any();
}
}
return false;
}
}
The method above takes the loginName
and roleName
as parameters. What it does is it checks for the existing records in the user's table and then validates if the corresponding user has roles assigned to it.
Creating a Custom Authorization Attribute Filter
If you remember, we are using the [Authorize]
attribute to restrict anonymous users from accessing a certain action method. The [Authorize]
attribute provides filters for users and roles and it’s fairly easy to implement it if you are using a membership provider. Since we are using our own database for storing users and roles, we need to implement our own authorization filter by extending the AuthorizeAttribute
class.
The AuthorizeAttribute
specifies that access to a controller or action method is restricted to users who meet the authorization requirements. Our goal here is to allow page authorization based on user roles and nothing else. If you want to implement custom filters to do a certain task and value the separation of concerns then you may want to look at IAutenticationFilter
instead.
To start, add a new folder and name it “Security
”. Then add the “AuthorizeRoleAttribute
” class. The following is a screen shot of the structure:
Figure 6: Project Structure
Here’s the code block for our custom filter:
using System.Web;
using System.Web.Mvc;
using MVC5RealWorld.Models.DB;
using MVC5RealWorld.Models.EntityManager;
namespace MVC5RealWorld.Security
{
public class AuthorizeRolesAttribute : AuthorizeAttribute
{
private readonly string[] userAssignedRoles;
public AuthorizeRolesAttribute(params string[] roles) {
this.userAssignedRoles = roles;
}
protected override bool AuthorizeCore(HttpContextBase httpContext) {
bool authorize = false;
using (DemoDBEntities db = new DemoDBEntities()) {
UserManager UM = new UserManager();
foreach (var roles in userAssignedRoles) {
authorize = UM.IsUserInRole(httpContext.User.Identity.Name, roles);
if (authorize)
return authorize;
}
}
return authorize;
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) {
filterContext.Result = new RedirectResult("~/Home/UnAuthorized");
}
}
}
There are two main methods in the class above that we have overridden. The AuthorizeCore()
method is the entry point for the authentication check. This is where we check the roles assigned for certain users and returns the result specifying whether or not the user is allowed to access a page. The HandleUnuathorizedRequest()
is a method in which we redirect un-authorized users to a certain page ~ the "UnAuthorized" page.
Adding the AdminOnly and UnAuthorized Pages
Now switch back to “HomeController
” and add the following code:
[AuthorizeRoles("Admin")]
public ActionResult AdminOnly() {
return View();
}
public ActionResult UnAuthorized() {
return View();
}
If you notice, we decorated the AdminOnly
action with our custom authorization filter by passing the value of “Admin
” as the role name. This means that only allow admin users have access to the AdminOnly
page. To support multiple role access, just add another role name by separating it with a comma, for example [AuthorizeRoles(“Admin”,”Manager”)]
. Note that the value of “Admin
” and “Manager
” should match the role names from your database. Finally, be sure to reference the namespace below before using the AuthorizeRoles
attribute:
using MVC5RealWorld.Security;
Here’s the AdminOnly.cshtml View:
@{
ViewBag.Title = "AdminOnly";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>For Admin users only!</h2>
And here’s the UnAuthorized.cshtml view:
@{
ViewBag.Title = "UnAuthorized";
Layout = "~/Views/Shared/_Layout.cshtml";
}
And here’s the UnAuthorized.cshtml View:
@{
ViewBag.Title = "UnAuthorized";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>Unauthorized Access!</h2>
<p>Oops! You don't have permission to access this page.</p>
<div>
@Html.ActionLink("Back to Main", "Welcome", "Home")
</div>
Testing the Functionality
Before we test the functionality, lets add an admin user to the database first. For this demo I have inserted the following data to the database:
INSERT INTO SYSUser (LoginName,PasswordEncryptedText, RowCreatedSYSUserID, RowModifiedSYSUserID)
VALUES ('Admin','Admin',1,1)
GO
INSERT INTO SYSUserProfile (SYSUserID,FirstName,LastName,Gender,RowCreatedSYSUserID, RowModifiedSYSUserID)
VALUES (2,'Vinz','Durano','M',1,1)
GO
INSERT INTO SYSUserRole (SYSUserID,LOOKUPRoleID,IsActive,RowCreatedSYSUserID, RowModifiedSYSUserID)
VALUES (2,1,1,1,1)
Okay, now we have data to test and we are ready to run the application.
Output
The following are some of the screenshots captured during my test.
When logging in as a normal user and accessing the following URL: http://localhost:15599/Home/AdminOnly
Figure 7: Un-authorized access
When logging in as an Admin user and accessing the following URL: http://localhost:15599/Home/AdminOnly
Figure 8: The Admin page
In the next part of this series, we will see how to do Fetch, Edit, Update and Delete (FEUD) operations within our MVC 5 application. You can check the next part here: ASP.NET MVC 5: Building Your First Web Application - Part 3
That’s it! I hope you find this article useful.
Summary
In this series, we've learned how to implement a simple login page and how to integrate a custom role-based page authorization in ASP.NET MVC 5 application.