ProudMonkey

Building Web Application using Entity Framework and MVC 5: Part 2

In my previous article we've learned about how to create a database from scratch, generate entity models and implemented a simple signup page in MVC 5. If you haven't gone through my previous article then you can refer this link: Building Web Application using Entity Framework and MVC 5: Part 1

What you will learn:

  • Creating a Login page that would validate and authenticate user using Forms Authentication
  • Creating a custom role-based page authorization using custom Authorize filter

For this particular 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 allow users to login using their social media accounts like Facebook, Twitter, Google Plus and so on then you may want explore on ASP.NET Identity instead.

Note that I will not elaborate more in details about the model, view and controller function so before proceeding further, I'd suggest you to check out my previous article 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. Majority of the web sites nowadays heavily relies 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 messenger to come, but there are certain parts of the facility, such as workstations and conference rooms, that are accessible only to people with certain credentials, such as employees. An example is when a you build a shopping cart application that accepts users’ credit card information for payment purposes and stores them to your database; ASP.NET helps protect your database from public access by providing authentication and authorization mechanism.

Forms authentication lets you authenticate users by 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 and 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).

Let’s get our hands dirty!

As a recap, here's the previous project structure below:

Implementing a Login Page

STEP 1: Enabling Forms Authentication

To allow forms authentication in your application, the very first thing to do 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 system.web element in your web.config:

<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 redirect users to the specified page after successful log-in.

STEP 2: Adding the UserLoginView Model

Let's go ahead and create a model view class for our Login page by adding the following code below 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 noticed that the fields are decorated with Required, Display and DataType attributes. These attributes are called Data Annotations.

STEP 3: Adding the GetUserPassword() Method

Add the following code below under "UserManager.cs" 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 particular login name using LINQ query.

STEP 4: Adding the Login Action Methods

Add the following code below under "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.");
                    }
                }
            }

            // If we got this far, something failed, redisplay form
            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 was also named as "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 display an error stating that the login name or password supplied is invalid.

STEP 5: Adding the Login View

Before adding the view, make sure to build your application first to ensure that the application is error-free. After a successful build, navigate to "AccountController" class and right click on the Login action method and then select “Add View”. This will bring up the following dialog below:

Take note of the values supplied for each field. Now click on Add to let Visual Studio scaffolds the UI for you. Here’s the modified HTML markup below:

@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>  
STEP 6: Implementing the Logout Functionality

The logout code is pretty much easy. 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 user to Index page after signing out.

Here’s the corresponding action link for the Logout:

@Html.ActionLink("Signout","SignOut","Account")
STEP 7: Running the Application

Running your application should display something like these:

When validation triggers:

After successful logging in:

After logging out:

That 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 it. For example, only allow administrator to access the maintenance page for your application. In this section we will create a simple implementation on how to achieve that.

STEP 1: Creating the UserIsInRole Method

Add the following code below at "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 SYSUser table and then validates if the corresponding user has roles assigned to it.

STEP 2: 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 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 requirement. Our goal here to allow page authorization based on user roles and nothing else. If you want to implement custom filters to do 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 as "Security". Then add the "AuthorizeRoleAttribute" class. The following is screen shot of the structure:

And 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 if the user is allowed to access a page or not. The HandleUnuathorizedRequest() is a method in which we redirect un-authorized users to a certain page.

STEP 3: Adding the AdminOnly and UnAuthorized page

Now switch back to "HomeController" class and add the following code:

[AuthorizeRoles("Admin")]
public ActionResult AdminOnly() {  
       return View();
}

public ActionResult UnAuthorized() {  
       return View();
}

If you noticed, 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 to access 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 with the role names from your database. And 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";
}

<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>  
STEP 4: 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, we now have data to test and we are ready to run the application.

STEP 5: Running the Application

Here are some of the screen shots captured during my test.

When logging in as normal user and accessing the following URL: http://localhost:15599/Home/AdminOnly

When logging in as an Admin user and accessing the following URL: http://localhost:15599/Home/AdminOnly

That’s it! I hope you will find this article useful.

Check-out the next article here: Building Web Application using Entity Framework and MVC 5: Part 3

Source Code

Source code for this series can be found here. You can also download it at CodeProject here.