These will be a series articles about security issue of ASP.NET Authentication:
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
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
- using SecurityDemoMVC.Models;
- using System.Linq;
- using System.Web.Mvc;
-
- namespace SecurityDemoMVC.Controllers
- {
- public class AccountsController : Controller
- {
- [HttpGet]
- public ActionResult Login()
- {
- return View();
- }
-
- [HttpPost]
- public ActionResult Login(User model)
- {
- if (ModelState.IsValid)
- {
- using (var context = new UserDBContext())
- {
- User user = context.Users
- .Where(u => u.UserName == model.UserName && u.UserPassword == model.UserPassword)
- .FirstOrDefault();
-
- if (user != null)
- {
- Session["UserName"] = user.UserName;
- 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;
- return RedirectToAction("Index", "Home");
- }
- }
- }
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 - @if (!string.IsNullOrEmpty(Convert.ToString(Session["UserName"])))
- {
- using (Html.BeginForm("LogOff", "Accounts", 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", "Accounts", routeValues: null, htmlAttributes: new { id = "loginLink" })</li>
- </ul>
- }
And we still need to add one line in Views/Shared/_Layout.cshtml, as previous
- <body>
- <div class="navbar navbar-inverse navbar-fixed-top">
- <div class="container">
- <div class="navbar-header">
- <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
- <span class="icon-bar"></span>
- <span class="icon-bar"></span>
- <span class="icon-bar"></span>
- </button>
- @Html.ActionLink("Application name", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" })
- </div>
- <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>
- </div>
- </div>
- ......
- </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:
- using System;
- using System.Web.Mvc;
- using System.Web.Mvc.Filters;
- using System.Web.Routing;
-
- namespace SecurityDemoMVC.Infrastructure
- {
- public class CustomAuthFilter : 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)
- {
-
- filterContext.Result = new RedirectToRouteResult(
- new RouteValueDictionary
- {
- { "controller", "Accounts" },
- { "action", "Login" }
- });
- }
- }
- }
- }
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:
- using SecurityDemoMVC.Infrastructure;
- using System.Web.Mvc;
-
-
-
-
- namespace SecurityDemoMVC.Controllers
- {
-
-
- public class HomeController : Controller
- {
- [AllowAnonymous]
- public ActionResult Index()
- {
- return View();
- }
-
- public ActionResult About()
- {
- ViewBag.Message = "Your application description page.";
-
- return View();
- }
- [CustomAuthFilter]
- public ActionResult Contact()
- {
- ViewBag.Message = "Your contact page.";
-
- return View();
- }
- }
- }
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 access
is protected:
This article discusses the implementation of Forms Authentication by using its own database with Filter.
References