Tuesday, October 14, 2014

ASP.NET MVC Forms Authentication in Eight Easy Steps

Step 0. Create an Empty ASP.NET MVC project

Step 1. Create a database

/*
use master;
drop database ReadyAspNetMvc;
*/


create database ReadyAspNetMvc;
go


use ReadyAspNetMvc;
go

create table Person
(
    PersonId int identity(1,1) primary key,
    UserName nvarchar(50) not null unique,
    PlainTextPassword nvarchar(100) not null, -- should use hashing: http://www.ienablemuch.com/2014/10/bcrypt-primer.html    
    Roles nvarchar(100) not null default '' -- In actual application, this is relational not comma-delimited
);


insert into Person(UserName, PlainTextPassword,Roles) values
('John', 'L', 'Beatles,Musician'),
('Paul', 'M', 'Beatles,Musician'),
('George', 'H', 'Beatles,Musician'),
('Ringo', 'S', 'Beatles,Musician'),
('Kurt', 'C', 'Nirvana,Musician'),
('Dave', 'G', 'Nirvana,Musician'),
('Krist', 'N', 'Nirvana,Musician'),
('Elvis', 'P', 'Musician'),
('Michael', 'J', ''),
('Freddie', 'M', '');


go


Step 2. Add Forms Authentication in web.config:
<configuration>

    <system.web>
    
        <httpRuntime targetFramework="4.5.1" />
        
        <compilation debug="true" targetFramework="4.5.1" />

        <authentication mode="Forms">
            <forms loginUrl="~/Security/Login" timeout="2880" />
        </authentication>


Step 3: Use an ORM, let's use Dapper, get it from nuget:

Step 4: Create a UserLogin model:
namespace ReadyAspNetMvc.Models
{
    public class UserLogin
    {
        public string UserName { get; set; }
        public string Password { get; set; }
        public string ReturnUrl { get; set; } // when an action accessed is not authorized, this is where the url to return to is binded 
    }
}

Here's how ReturnUrl looks like:



Step 5: Create a login page (two sub-steps):

Step 5.1 Create Login controller and actions:

using System.Web.Mvc;

using System.Linq;


using Dapper;


namespace ReadyAspNetMvc.Controllers
{
    public class SecurityController : Controller
    {       
        public ViewResult Login()
        {
            return View();
        }

        [HttpPost]
        public ActionResult Login(ReadyAspNetMvc.Models.UserLogin login)
        {
            System.Action authorize = () => System.Web.Security.FormsAuthentication.SetAuthCookie(userName: login.UserName, createPersistentCookie: true);
            

            using (var con = new System.Data.SqlClient.SqlConnection("Server=.; Database=ReadyAspNetMvc; Trusted_Connection=true;"))
            {
                var persons = con.Query("select UserName, PlainTextPassword from Person where UserName = @UserName", new { UserName = login.UserName });

                if (!persons.Any())
                    return View(login);


                var person = persons.Single();


                if (login.Password == person.PlainTextPassword)
                {                    

                    if (string.IsNullOrWhiteSpace(login.ReturnUrl))
                    {
                        authorize();
                        return RedirectToAction(controllerName: "Home", actionName: "Welcome");
                    }
                    else
                    {
                        if (Url.IsReallyLocalUrl(login.ReturnUrl))
                        {
                            authorize();
                            return Redirect(login.ReturnUrl);
                        }
                        else
                        {
                            TempData["warning_message"] = "Url was altered";
                            return RedirectToAction(controllerName: "Security", actionName: "Login");
                        }
                    }
                    

                    //// another way, but it's better to use ASP.NET MVC-proper by using return Redirect(...), so use the above
                    //else
                    //{
                    //    System.Web.Security.FormsAuthentication.RedirectFromLoginPage(userName: login.UserName, createPersistentCookie: true);
                    //    return null;
                    //}
                }
                else
                {  
                    TempData["warning_message"] = "Invalid username or password";                  
                    return View();
                }
            }
            
        }//Login action


        public RedirectToRouteResult SignOut()
        {
            System.Web.Security.FormsAuthentication.SignOut();
            return RedirectToAction(controllerName: "Home", actionName: "Welcome");
        }

    }//SecurityController
}


...

    public static class UrlExtension
    {
        // Thanks وحيد نصيري
        public static bool IsReallyLocalUrl(this UrlHelper url, string returnUrl)
        {
            var shouldRedirect = !string.IsNullOrWhiteSpace(returnUrl) &&
                url.IsLocalUrl(returnUrl) &&
                returnUrl.Length > 1 &&
                returnUrl.StartsWith("/", System.StringComparison.InvariantCultureIgnoreCase) &&
                !returnUrl.StartsWith("//", System.StringComparison.InvariantCultureIgnoreCase) &&
                !returnUrl.StartsWith("/\\", System.StringComparison.InvariantCultureIgnoreCase);
            return shouldRedirect;
        }
    }




5.2 Create the view: /Views/Security/Login.cshtml

@model ReadyAspNetMvc.Models.UserLogin

@{
    ViewBag.Title = "Login";
}

<h2>Login</h2>



@using (Html.BeginForm())
{
    <div>
        @Html.LabelFor(x => x.UserName)
    </div>
    <div>
        @Html.TextBoxFor(x => x.UserName)
    </div>

    <div>
        @Html.LabelFor(x => x.Password)
    </div>

    <div>
        @Html.PasswordFor(x => x.Password)
    </div>

    
    <p>
        <input type="submit" />
    </p>
            
}


<p>
    <a href="@Url.RouteUrl(new { controller = "Home", action = "Welcome" })">Back to Home Welcome</a>
</p>



<p style="color: red">@this.TempData["warning_message"]</p>




6. Detect roles in Global.asax.cs:
using System;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Routing;


using System.Linq;
using Dapper;

namespace ReadyAspNetMvc
{
    // Note: For instructions on enabling IIS6 or IIS7 classic mode, 
    // visit http://go.microsoft.com/?LinkId=9394801
    public class MvcApplication : System.Web.HttpApplication
    {        
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            WebApiConfig.Register(GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);                       
        }

     
        // Auto-wired-up event
        // http://stackoverflow.com/questions/4677866/how-does-global-asax-postauthenticaterequest-event-binding-happe
        void Application_PostAuthenticateRequest(object sender, EventArgs e)
        {
            if (!System.Web.Security.FormsAuthentication.CookiesSupported) 
                return;

            string cookieName = System.Web.Security.FormsAuthentication.FormsCookieName;            
            System.Web.HttpCookie cookie = this.Request.Cookies[cookieName]; 

            if (cookie == null) 
                return;

            string encryptedCookieValue = cookie.Value;
            System.Web.Security.FormsAuthenticationTicket ticket = System.Web.Security.FormsAuthentication.Decrypt(encryptedCookieValue);

            string userName = ticket.Name;
            string[] roles = null;


            using (var con = new System.Data.SqlClient.SqlConnection("Server=.; Database=ReadyAspNetMvc; Trusted_Connection=true;"))
            {
                var persons = con.Query("select UserName, Roles from Person where UserName = @UserName", new { UserName = userName });

                var person = persons.Single();


                roles = ((string)person.Roles).Split(',');


                System.Security.Principal.IIdentity identity = new System.Security.Principal.GenericIdentity(name: userName, type: "Forms");

                System.Web.HttpContext.Current.User = new System.Security.Principal.GenericPrincipal(identity, roles);
            }


        }// Application_PostAuthenticateRequest

        
    }//class MvcApplication
}


Step 7. Setup the home page. Two sub-steps

7.1. Create the Home controller:
using System.Web.Mvc;

namespace ReadyAspNetMvc.Controllers
{
    public class HomeController : Controller
    {
        //
        // GET: /Home/

        public ActionResult Index()
        {
            return RedirectToAction(actionName: "Welcome");
        }


        public ViewResult Welcome()
        {
            string userName = System.Web.HttpContext.Current.User.Identity.Name;

            ViewBag.UserName = userName;
            
            return View();
        }

    }
}

7.2. Create the view: /Views/Home/Welcome.cshtml
@{
    ViewBag.Title = "Welcome";
}



@if (!string.IsNullOrWhiteSpace(ViewBag.UserName)) 
{
    <h2>Welcome @ViewBag.UserName</h2>
}



<div>
    <a href="@Url.RouteUrl(new { controller = "Music", action = "AboutBeatles" })">About Beatles</a>    
</div>

<div>
    <a href="@Url.RouteUrl(new { controller = "Music", action = "AboutNirvana" })">About Nirvana</a>
</div>

<div>
    <a href="@Url.RouteUrl(new { controller = "Music", action = "AboutGrungeRock" })">About Grunge Rock</a>
</div>

<div>
    <a href="@Url.RouteUrl(new { controller = "Music", action = "AboutMusician" })">About Musician</a>
</div>

<div>
    <a href="@Url.RouteUrl(new { controller = "Music", action = "AmILogged" })">Am I Logged?</a>
</div>

<div>
    <a href="@Url.RouteUrl(new { controller = "Music", action = "Anyone" })">Anyone</a>
</div>


<p>
    <div>
        @if (!string.IsNullOrWhiteSpace(ViewBag.UserName)) 
        { 
            <a href="@Url.RouteUrl(new { controller = "Security", action = "SignOut" })">Sign Out</a>
        }
        else
        {
            <a href="@Url.RouteUrl(new { controller = "Security", action = "Login" })">Login</a>
        }        
    </div>
</p>



Final Step. Create a controller that will test the authorization. Note that while GenericPrincipal's roles parameter is array-based, the Authorize's Roles property is comma-delimited

Final.1. Controller:
using System.Web.Mvc;

namespace ReadyAspNetMvc.Controllers
{
    public class MusicController : Controller
    {
        
        
        [Authorize(Roles="Beatles")]
        public ViewResult AboutBeatles()
        {
            string userName = System.Web.HttpContext.Current.User.Identity.Name;
            ViewBag.Message = string.Format("Hello {0}! Beatles is the greatest rock band", userName);
            return Greet();
        }


        [Authorize(Roles = "Nirvana")]
        public ViewResult AboutNirvana()
        {
            string userName = System.Web.HttpContext.Current.User.Identity.Name;
            ViewBag.Message = string.Format("Hello {0}! Nirvana is the greatest grunge band", userName);
            return Greet();
        }

        [Authorize(Roles = "Beatles,Nirvana")]
        public ViewResult AboutGrungeRock()
        {
            string userName = System.Web.HttpContext.Current.User.Identity.Name;
            ViewBag.Message = string.Format("Hello {0}! This is grunge rock", userName);
            return Greet();
        }


        [Authorize(Roles = "Musician")]
        public ViewResult AboutMusician()
        {
            string userName = System.Web.HttpContext.Current.User.Identity.Name;
            ViewBag.Message = string.Format("Hello {0}! You are a music inventor", userName);
            return Greet();
        }
                

        [Authorize]
        public ViewResult AmILogged()
        {
            string userName = System.Web.HttpContext.Current.User.Identity.Name;
            ViewBag.Message = string.Format("Yes {0}!", userName);
            return Greet();
        }

        public string Anyone()
        {
            return "<b>Anyone</b>";
        }

        public ViewResult Greet()
        {
            return View("Greet"); 

            // return View(); // If we do this, when we visit /Music/AboutMusician ASP.NET MVC will try to find AboutMusician.cshtml instead of Greet.cshtml
        }


    }
}

Final.2. Create the view: /Views/Music/Greet.cshtml:
<h2>@ViewBag.Message</h2>


<a href="@Url.RouteUrl(new { controller = "Home", action = "Welcome" })">Back to Home Welcome</a>




Happy Coding!

1 comment:

  1. Hi, It's necessary to preventing open redirect attacks:
    var shouldRedirect = !string.IsNullOrWhiteSpace(returnUrl) &&
    url.IsLocalUrl(returnUrl) &&
    returnUrl.Length > 1 &&
    returnUrl.StartsWith("/", StringComparison.InvariantCultureIgnoreCase) &&
    !returnUrl.StartsWith("//", StringComparison.InvariantCultureIgnoreCase) &&
    !returnUrl.StartsWith("/\\", StringComparison.InvariantCultureIgnoreCase);
    Check out shouldRedirect before redirecting the user from the login page.

    ReplyDelete