Introduction
ASP.NET MVC filters are used to add extra logic at the different levels of MVC Framework request processing. There are many articles available on the web about custom authorization filters. But very few have simple examples. My intention in this post is to depict the authorization filter with a step-by-step explanation using a simple example application. I will use a custom authentication filter also with this example. Our application will show the pages only after a successful login. We will create three different roles as “SuperAdmin”, “Admin” and “Normal”. Super admin-type users can see all three pages, but Admin and Normal users can view only specific pages. We will create three different users with three different roles. We will use the Entity Framework as ORM (Object-relational mapper) to connect with SQL server database. We will use the code-first approach to create all tables and insert values to tables using the database migration process.
I have already explained the custom authentication filter in my previous article with a simple example. Anyway, we can see all the actions step by step again.
Create an MVC application in Visual Studio
Choose ASP.NET Web Application template and select MVC option.
We can install EntityFrameworkNuGet packages.
We can create a “Role” class inside the “models” folder.
Role.cs
namespace CustomAuthorizationFilter.Models
{
public class Role
{
public int Id { get; set; }
public string Name { get; set; }
}
}
We can create a “User” class also.
User.cs
using System.ComponentModel.DataAnnotations;
namespace CustomAuthorizationFilter.Models
{
public class User
{
public int Id { get; set; }
[Display(Name = "User Id")]
public string UserName { get; set; }
public string Password { get; set; }
public int RoleId { get; set; }
}
}
Please note, we have used RoleIdin this classto store user role information.
We can create a DbContext class now. This class will be used in the database migration process and will be used for connecting the application with SQL database using entity framework also.
SqlDbContext.cs
using System.Data.Entity;
namespace CustomAuthorizationFilter.Models
{
public class SqlDbContext : DbContext
{
public SqlDbContext() : base("name=SqlConnection")
{
}
public DbSet<User> Users { get; set; }
public DbSet<Role> Roles { get; set; }
}
}
We have created two DbSet properties for Role and User classes. We have used a connection string “SqlConnection” inside the above DbContext class. We can create connection string inside the Web.Config also.
We can create SQL database and table using database migration process. We can enable the DB migration first. Choose “Package Manager Console” from “Tools -> NuGet Package Manager” menu item.
Use the below command to enable the migration.
enable-migrations
The above command will generate a “Configuration.cs” file inside the “Migration” folder.
We can use the below command to add new migration.
add-migration Initial
The above command will create a new migration file suffix with “_Initial” and timestamp inside the “Migrations” folder.
We can use the “Sql” command to insert a default record (Seed data) to the Role table and User table while the migration happens.
namespace CustomAuthorizationFilter.Migrations
{
using System;
using System.Data.Entity.Migrations;
public partial class Initial : DbMigration
{
public override void Up()
{
CreateTable(
"dbo.Roles",
c => new
{
Id = c.Int(nullable: false, identity: true),
Name = c.String(),
})
.PrimaryKey(t => t.Id);
CreateTable(
"dbo.Users",
c => new
{
Id = c.Int(nullable: false, identity: true),
UserId = c.String(),
UserName = c.String(),
Password = c.String(),
RoleId = c.Int(nullable: false),
})
.PrimaryKey(t => t.Id);
Sql("Insert into Roles(Name) Values ('SuperAdmin')");
Sql("Insert into Roles(Name) Values ('Admin')");
Sql("Insert into Roles(Name) Values ('Normal')");
Sql("Insert into Users (UserId, UserName, Password, RoleId) Values ('sarathlal', 'Sarathlal Saseendran', 'pwd', 1)");
Sql("Insert into Users (UserId, UserName, Password, RoleId) Values ('sarath', 'Sarath Lal', 'pwd', 2)");
Sql("Insert into Users (UserId, UserName, Password, RoleId) Values ('lal', 'Sarath Lal', 'pwd', 3)");
}
public override void Down()
{
DropTable("dbo.Users");
DropTable("dbo.Roles");
}
}
}
The above SQL command will insert three records to the Roles table and three records to Users table during migration process. We can use the below command in Package Manage Console to create a database and tables.
update-database
You can see in the SQL server that a new database and tables are created with default records. We can create an “Account” controller inside the “Controller” folder to control the log in process. Copy the below code and paste inside the controller class.
AccountController.cs
using CustomAuthorizationFilter.Models;
using System.Linq;
using System.Web.Mvc;
namespace CustomAuthorizationFilter.Controllers
{
public class AccountController : Controller
{
[HttpGet]
public ActionResult Login()
{
return View();
}
[HttpPost]
public ActionResult Login(User model)
{
if (ModelState.IsValid)
{
using (var context = new SqlDbContext())
{
User user = context.Users
.Where(u => u.UserId == model.UserId && u.Password == model.Password)
.FirstOrDefault();
if (user != null)
{
Session["UserName"] = user.UserName;
Session["UserId"] = user.UserId;
return RedirectToAction("Index", "Home");
}
else
{
ModelState.AddModelError("", "Invalid User Name or Password");
return View(model);
}
}
}
else
{
return View(model);
}
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult LogOff()
{
Session["UserName"] = string.Empty;
Session["UserId"] = string.Empty;
return RedirectToAction("Index", "Home");
}
}
}
We have validated the user id and password in the database for authentication purposes. Also, we created a logout action to delete the user name and user id stored in session storage in login action.
We can create a Login view and add the below code.
Login.cshtml
using CustomAuthorizationFilter.Models;
using System.Linq;
using System.Web.Mvc;
namespace CustomAuthorizationFilter.Controllers
{
public class AccountController : Controller
{
[HttpGet]
public ActionResult Login()
{
return View();
}
[HttpPost]
public ActionResult Login(User model)
{
if (ModelState.IsValid)
{
using (var context = new SqlDbContext())
{
User user = context.Users
.Where(u => u.UserId == model.UserId && u.Password == model.Password)
.FirstOrDefault();
if (user != null)
{
Session["UserName"] = user.UserName;
Session["UserId"] = user.UserId;
return RedirectToAction("Index", "Home");
}
else
{
ModelState.AddModelError("", "Invalid User Name or Password");
return View(model);
}
}
}
else
{
return View(model);
}
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult LogOff()
{
Session["UserName"] = string.Empty;
Session["UserId"] = string.Empty;
return RedirectToAction("Index", "Home");
}
}
}
Create custom authentication filter
We can create a custom authentication filter. Create a new “Infrastructure” folder and create a “CustomAuthenticationFilter.cs” class inside it.
CustomAuthenticationFilter.cs
using System;
using System.Web.Mvc;
using System.Web.Mvc.Filters;
using System.Web.Routing;
namespace CustomAuthorizationFilter.Infrastructure
{
public class CustomAuthenticationFilter : ActionFilterAttribute, IAuthenticationFilter
{
public void OnAuthentication(AuthenticationContext filterContext)
{
if (string.IsNullOrEmpty(Convert.ToString(filterContext.HttpContext.Session["UserName"])))
{
filterContext.Result = new HttpUnauthorizedResult();
}
}
public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext)
{
if (filterContext.Result == null || filterContext.Result is HttpUnauthorizedResult)
{
// Redirecting the user to the Login View of Account Controller
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary
{
{ "controller", "Account" },
{ "action", "Login" }
});
}
}
}
}
We have implemented “ActionFilterAttribute” class and “IAuthenticationFilter” interface in the above class. Please note that there are two important methods implemented - “OnAuthentication” and “OnAuthenticationChallenge”. Inside OnAuthentication, we have checked the session value “UserName” to see if it is empty or not. If the value is empty, it will throw the result as “HttpUnauthorizedResult” and then, the second method OnAuthenticationChallenge will be executed. This method will redirect the request to a specific action and controller. In this example, we will redirect to “Login” action in “Account” controller.
Create custom Authorize attribute filter
We can create an important portion in our application, custom authorize attribute now.
Create “CustomAuthorizeAttribute” class file inside the Infrastructure folder and copy below code to the class.
CustomAuthorizeAttribute.cs
using CustomAuthorizationFilter.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace CustomAuthorizationFilter.Infrastructure
{
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
private readonly string[] allowedroles;
public CustomAuthorizeAttribute(params string[] roles)
{
this.allowedroles = roles;
}
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
bool authorize = false;
var userId = Convert.ToString(httpContext.Session["UserId"]);
if (!string.IsNullOrEmpty(userId))
{
using (var context = new SqlDbContext())
{
var userRole = (from u in context.Users
join r in context.Roles on u.RoleId equals r.Id
where u.UserId == userId
select new
{
r.Name
}).FirstOrDefault();
foreach (var role in allowedroles)
{
if (role == userRole?.Name)
{
return true;
}
}
}
}
return authorize;
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary
{
{ "controller", "Home" },
{ "action", "UnAuthorized" }
});
}
}
}
We have inherited base class “AuthorizeAttribute” into this class. We will override two important methods “AuthorizeCore” and “HandleUnauthorizedRequest”.
The first method will check the associated roles of an action in a controller with the assigned role of the user. If there is no matching role found, this method will return value “false” and authorization will be failed. If authorization failed, second overridden method “HandleUnauthorizedRequest” will be executed and the page will be redirected to a specific “UnAuthorized” action (page) in “Home” controller.
We can create this action and view now.
Open the “HomeController” class and create a new action “UnAuthorized”
HomeController.cs
using CustomAuthorizationFilter.Infrastructure;
using System.Web.Mvc;
namespace CustomAuthorizationFilter.Controllers
{
[CustomAuthenticationFilter]
public class HomeController : Controller
{
[CustomAuthorize("Normal", "SuperAdmin")]
public ActionResult Index()
{
return View();
}
[CustomAuthorize("Admin", "SuperAdmin")]
public ActionResult About()
{
ViewBag.Message = "Your application description page.";
return View();
}
[CustomAuthorize("SuperAdmin")]
public ActionResult Contact()
{
ViewBag.Message = "Your contact page.";
return View();
}
public ActionResult UnAuthorized()
{
ViewBag.Message = "Un Authorized Page!";
return View();
}
}
}
Please note, we have decorated “CustomAuthenticationFilter” in top of the controller. So that before every request to actions (any) in home controller, this filter will be executed and validate the user credentials.
Also, note that we have decorated “Index” action with “CustomAuthorize” attribute and two roles “Normal” and “SuperAdmin”. So, a user with Normal or SuperAdmin role can access this action. “About” action is decorated with “Admin” and “SuperAdmin” roles. A user with Admin or SuperAdmin role can access this action. “Contact” action is decorated with “SuperAdmin” role only. Only users with SuperAdmin role can access this action. “UnAuthorized” action is not decorated with any roles. Hence, the authorization filter will not be executed for this action.
We can create a view for UnAuthorized action now.
Right-click the action and choose the “Add View” option. Please remember to check “Use a layout page” option also.
We can add a partial view to show the username in the right-top corner along with logout link. This will show only after a successful login. If the user is not yet logged in, this will show as a simple login link. We can add the logic for that. We will create this partial view inside the shared views folder.
_LoginPartial.cshtml
@if (!string.IsNullOrEmpty(Convert.ToString(Session["UserName"])))
{
using (Html.BeginForm("LogOff", "Account", FormMethod.Post, new { id = "logoutForm", @class = "navbar-right" }))
{
@Html.AntiForgeryToken()
<ul class="nav navbar-nav navbar-right">
<li><a href="#">Welcome : @Session["UserName"]</a></li>
<li><a href="javascript:document.getElementById('logoutForm').submit()">Log off</a></li>
</ul>
}
}
else
{
<ul class="nav navbar-nav navbar-right">
<li>@Html.ActionLink("Log in", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" })</li>
</ul>
}
We can call this partial view from _Layout.cshtml.
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li>@Html.ActionLink("Home", "Index", "Home")</li>
<li>@Html.ActionLink("About", "About", "Home")</li>
<li>@Html.ActionLink("Contact", "Contact", "Home")</li>
</ul>
@Html.Partial("_LoginPartial")
</div>
We have completed the entire coding part. We can run the application now. Please note that we have added authentication filter attribute to the entire home controller. Hence, though the index action in the home controller is the default route, it will be redirected to login action automatically.
We have already inserted three different users “sarathlal”, “sarath”, and “lal” during the data migration process. User sarathlal is assigned with SuperAdmin role and user sarath is assigned with Admin role. Last user lal is assigned with Normal role. SuperAdmin can assign all three pages. Admin user can access only About page and Normal user can access Index page only. If a user tries to access a page without enough privileges, the request will be redirected to an unauthorized page.
Here, user sarath is trying to access the Index page. The index page can be accessed by Normal users and SuperAdmin users only. But this user is with Admin role. Hence, this request will be redirected to the unauthorized page automatically.
Users with enough privileges can access corresponding pages.
Conclusion
In this post, we have seen how to implement a custom authorization filter in an MVC application. We have created a database and two tables with three different users and three different roles using database migration and code first approach. We have created the sample application with custom authorization attribute and with custom authentication filter as well. Users with enough privileges can only access corresponding pages. Otherwise, the request has been redirected to unauthorized page automatically.