Forms Authentication (C), Using Database With Filter

These will be a series articles about security issue of ASP.NET Authentication:

Introduction

 
After I wrote the first security article, Authentication (1): Windows Authentication, I originally planned to write other two articles to summarize Forms Authentication and Passport Authentication, then concentrate on the new, cutting edge security OAuth and OpenID Authentication. However, I found out the staff in, such as, Forms Authentication is quite huge, it is very hard to summarize this topic within one article (I do not want to just make words summary, but with sample for support), then I think I'd better to split one article into severals with each article only has one topic.  That must be fit to the SOLID principle, especially for principle one and two:
  • The Single-responsibility principle: Every class should have only one responsibility.
  • The Open–closed principle: Software entities ... should be open for extension, but closed for modification.
This way will make me easier to write the articles, and also easier for readers to read.
 

C: Forms Authentication using Own Database with Filter

 
This example will be similar to the previous one, Forms Authentication (B), using Own Database, such as using database for authentication, except the authorization enforced by Filter instead of by web.config file.
 
Step 0: Create a Database
 
Step 1: Create an ASP.NET MVC App
 
Step 2: Setup Entity and Database Connection
 
Here, we skip Step 0~2, using exactly the same result from the previous article. We start to work from Step 3.
 
Step 3: Setup Login, Log Out
 
1, Add AccountsController
 
Controllers/AccountsController.cs
  1. using SecurityDemoMVC.Models;  
  2. using System.Linq;  
  3. using System.Web.Mvc;  
  4.   
  5. namespace SecurityDemoMVC.Controllers  
  6. {  
  7.     public class AccountsController : Controller  
  8.     {  
  9.         [HttpGet]  
  10.         public ActionResult Login()  
  11.         {  
  12.             return View();  
  13.         }  
  14.   
  15.         [HttpPost]  
  16.         public ActionResult Login(User model)  
  17.         {  
  18.             if (ModelState.IsValid)  
  19.             {  
  20.                 using (var context = new UserDBContext())  
  21.                 {  
  22.                     User user = context.Users  
  23.                                        .Where(u => u.UserName == model.UserName && u.UserPassword == model.UserPassword)
  24.                                        .FirstOrDefault();  
  25.   
  26.                     if (user != null)  
  27.                     {  
  28.                         Session["UserName"] = user.UserName;  
  29.                         return RedirectToAction("Index""Home");  
  30.                     }  
  31.                     else  
  32.                     {  
  33.                         ModelState.AddModelError("""Invalid User Name or Password");  
  34.                         return View(model);  
  35.                     }  
  36.                 }  
  37.             }  
  38.             else  
  39.             {  
  40.                 return View(model);  
  41.             }  
  42.         }  
  43.   
  44.         [HttpPost]  
  45.         [ValidateAntiForgeryToken]  
  46.         public ActionResult LogOff()  
  47.         {  
  48.             Session["UserName"] = string.Empty;  
  49.             return RedirectToAction("Index""Home");  
  50.         }  
  51.     }  

here, the authentication job is done by database as previous sample:
  • User user = context.Users.Where(u => u.UserName == model.UserName && u.UserPassword == model.UserPassword).FirstOrDefault();
while the authorization job is done not by FormsAuthentication Class, but by Session:
  • Session["UserName"] = user.UserName;
  • Session["UserName"] = string.Empty;
where Session authorization is due to we will use Filter to enforce the Authorization, instead of using web.config
 
2, Login, Signup Views, keep the same as previous
 
3, Layout page
 
The layout page will be:
 
Views/Shared/_LoginPartial.cshtml
  1. @if (!string.IsNullOrEmpty(Convert.ToString(Session["UserName"])))  
  2. {  
  3.     using (Html.BeginForm("LogOff""Accounts", FormMethod.Post, new { id = "logoutForm", @class = "navbar-right" }))  
  4.     {  
  5.         @Html.AntiForgeryToken()  
  6.         <ul class="nav navbar-nav navbar-right">  
  7.             <li><a href="#">Welcome : @Session["UserName"]</a></li>  
  8.             <li><a href="javascript:document.getElementById('logoutForm').submit()">Log off</a></li>  
  9.         </ul>  
  10.     }  
  11. }  
  12. else  
  13. {  
  14.     <ul class="nav navbar-nav navbar-right">  
  15.         <li>@Html.ActionLink("Log in""Login""Accounts", routeValues: null, htmlAttributes: new { id = "loginLink" })</li>  
  16.     </ul>  
And we still need to add one line in Views/Shared/_Layout.cshtml, as previous
  1. <body>  
  2.     <div class="navbar navbar-inverse navbar-fixed-top">  
  3.         <div class="container">  
  4.             <div class="navbar-header">  
  5.                 <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">  
  6.                     <span class="icon-bar"></span>  
  7.                     <span class="icon-bar"></span>  
  8.                     <span class="icon-bar"></span>  
  9.                 </button>  
  10.                 @Html.ActionLink("Application name""Index""Home"new { area = "" }, new { @class = "navbar-brand" })  
  11.             </div>  
  12.             <div class="navbar-collapse collapse">  
  13.                 <ul class="nav navbar-nav">  
  14.                     <li>@Html.ActionLink("Home""Index""Home")</li>  
  15.                     <li>@Html.ActionLink("About""About""Home")</li>  
  16.                     <li>@Html.ActionLink("Contact""Contact""Home")</li>  
  17.                 </ul>  
  18.                 @Html.Partial("_LoginPartial" 
  19.             </div>  
  20.         </div>  
  21.     </div> 
  22.     ......
  23. </body>
Run the App, the Login/Logout/Signin operations will work:
 
 
Step 4: Configure Authentication
 
Different from the previous article, we do not configure web.config file, but use an Authentication filter to trigger the Authentication mode.
 
1, Add one Authentication Filter
 
Add one Filter at Infrastructure/CustomAuthFilter.cs:
  1. using System;  
  2. using System.Web.Mvc;  
  3. using System.Web.Mvc.Filters;  
  4. using System.Web.Routing;  
  5.   
  6. namespace SecurityDemoMVC.Infrastructure  
  7. {  
  8.     public class CustomAuthFilter : ActionFilterAttribute, IAuthenticationFilter  
  9.     {  
  10.         public void OnAuthentication(AuthenticationContext filterContext)  
  11.         {  
  12.             if (string.IsNullOrEmpty(Convert.ToString(filterContext.HttpContext.Session["UserName"])))  
  13.             {  
  14.                 filterContext.Result = new HttpUnauthorizedResult();  
  15.             }  
  16.         }  
  17.         public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext)  
  18.         {  
  19.             if (filterContext.Result == null || filterContext.Result is HttpUnauthorizedResult)  
  20.             {  
  21.                 //Redirecting the user to the Login View of Account Controller  
  22.                 filterContext.Result = new RedirectToRouteResult(  
  23.                 new RouteValueDictionary  
  24.                 {  
  25.                      { "controller""Accounts" },  
  26.                      { "action""Login" }  
  27.                 });  
  28.             }  
  29.         }  
  30.     }  

Where the CustomAuthFilter class is derived from ActionFilterAttribute to make the CustomAuthFilter as an attribute class; and derived from IAuthenticationFilter interface to implement two methods:
  • OnAuthentication: Authenticate user, actually by Session;
  • OnAuthenticationChallenge: redirect Anonymous user to the login page for the authentication
2, Set Authorize Filter
 
We do it in controller/action by Attributes, instead of in web.config file:
  1. using SecurityDemoMVC.Infrastructure;  
  2. using System.Web.Mvc;  
  3.   
  4. // This App is for my article: Forms Authentication (1), using Database  
  5. // https://www.c-sharpcorner.com/article/forms-authentication-1-using-database/  
  6.   
  7. namespace SecurityDemoMVC.Controllers  
  8. {  
  9.     //[Authorize]  
  10.   
  11.     public class HomeController : Controller  
  12.     {  
  13.         [AllowAnonymous]  
  14.         public ActionResult Index()  
  15.         {  
  16.             return View();  
  17.         }  
  18.           
  19.         public ActionResult About()  
  20.         {  
  21.             ViewBag.Message = "Your application description page.";  
  22.   
  23.             return View();  
  24.         }  
  25.         [CustomAuthFilter]  
  26.         public ActionResult Contact()  
  27.         {  
  28.             ViewBag.Message = "Your contact page.";  
  29.   
  30.             return View();  
  31.         }  
  32.     }  
What we did,
  • Authentication
    • In the Authentication Filter:
      • OnAuthentication opens the (Forms) Authentication Mode
    • In the AccountsController:
      • Database does the Authentication job
  • Authorization
    • In the Authentication Filter:
      • Call OnAuthenticationChallenge when authorization challenge occurs.
    • In the AccountsController:
      • Add Authorization filters (Attributes) in either Controller level or Action level
      • FormsAuthentication Class does the Authorization job:
        • Session["UserName"] = user.UserName;
        • Session["UserName"] = string.Empty;
Run the App: the Home page will be viewable for anybody, but navigating to Contract page will be redirected to the login page automatically because accessis protected:
 
Home page
 
 
About page
 
 

Summary

 
This article discusses the implementation of Forms Authentication by using its own database with Filter.
 
References