Simple Database-less Authentication for MVC.Net

There are many projects that are way too small to setup an ASP.Net membership configuration or OWIN authentication. But you still want to protect your applications by one or more accounts. What to do?

Well... there is a small - but obsolete (!!) - thing that you can try: ASP.Net Forms Authentication. While it is obsolete, it is still usable. In this article I'll show how to implement it for MVC.Net.

  1. Intro
  2. Account storage - the web.config
  3. Roles + SimpleConfigurationPrincipal
  4. The Model
  5. The Controller
  6. The View
  7. Application authentication
  8. Final thoughts
  9. Comments

Account storage - the web.config

The accounts can be stored in the web.config file - no database is needed. This is how I have configured the forms authentication element and its users:

<system.web>
  <authentication mode="Forms">
    <forms  defaultUrl="/Home/Index" 
            loginUrl="Home/Login" 
            slidingExpiration="true" 
            timeout="20">
      <credentials passwordFormat="Clear" >
        <user name="Kees" password="pXZ4UsLu8A4V5d!"/>
        <user name="Erik" password="@z3eWVxBtrFNCHK"/>
      </credentials>
    </forms>
  </authentication>
</system.web>

This example uses plain text passwords. Other supported methods are MD5 or SHA1 passwords.

Roles + SimpleConfigurationPrincipal

There is no standard way of defining roles in the web.config, that's why I'm leveraging the appSettings to store the roles for each user as a pipe-separated string.

<appSettings>
  <add key="roles:Kees" value="Administrator|Member"/>
  <add key="roles:Erik" value="Member"/>
</appSettings>

Next, we'll need to create a principal that will implement the role:

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.Security.Principal;
using System.Threading;
using System.Web;

/// <summary>
/// Principal that looks for appSetting "roles:{userName}" for the roles. Roles should be separated with pipes.
/// </summary>
/// <seealso cref="System.Security.Principal.IPrincipal" />
[DebuggerDisplay("{Identity.Name")]
public class SimpleConfigurationPrincipal : IPrincipal
{
    HashSet<string> _roles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

    /// <summary>
    /// Initializes a new instance of the <see cref="SimpleConfigurationPrincipal"/> class.
    /// </summary>
    /// <param name="identity">The identity.</param>
    public SimpleConfigurationPrincipal(IIdentity identity)
    {
        if (identity == null)
        {
            throw new ArgumentNullException(nameof(identity));
        }

        this.Identity = identity;

        string roleSetting = $"roles:{identity.Name}";
        var roles = ConfigurationManager.AppSettings[roleSetting];

        if (!string.IsNullOrEmpty(roles))
        {
            foreach (var role in roles?.Split('|'))
            {
                _roles.Add(role);
            }
        }
    }

    /// <summary>
    /// Gets the identity.
    /// </summary>
    /// <value>
    /// The identity.
    /// </value>
    public IIdentity Identity
    {
        get;
        private set;
    }

    /// <summary>
    /// Determines whether the user has the specified role.
    /// </summary>
    /// <param name="role">The role.</param>
    /// <returns><c>true</c> if the user has the role; otherwise <c>false</c>.</returns>
    public bool IsInRole(string role)
    {
        return _roles.Contains(role);
    }

    /// <summary>
    /// Sets the authenticated principal.
    /// </summary>
    /// <param name="principal">The principal.</param>
    public static void SetAuthenticatedPrincipal(IPrincipal principal)
    {
        if (principal == null || principal.Identity == null || !principal.Identity.IsAuthenticated || String.IsNullOrEmpty(principal.Identity.Name))
        {
            return;
        }

        if (!(principal is SimpleConfigurationPrincipal))
        {
            principal = new SimpleConfigurationPrincipal(principal.Identity);
        }

        if (HttpContext.Current != null)
        {
            HttpContext.Current.User = principal;
        }

        Thread.CurrentPrincipal = principal;
    }
}

The Model

MVC is about models, views and controllers... so let's look at the logon model:

using System.ComponentModel.DataAnnotations;

/// <summary>
/// Simple model for a logon.
/// </summary>
public class LogonModel
{
    /// <summary>
    /// Gets or sets the name of the user.
    /// </summary>
    /// <value>
    /// The name of the user.
    /// </value>
    [Required]
    public string UserName { get; set; }

    /// <summary>
    /// Gets or sets the password.
    /// </summary>
    /// <value>
    /// The password.
    /// </value>
    [Required]
    public string Password { get; set; }
}

The Controller

The controller will log the user on (/Logon) and log the user off (/Logon/Logoff).

using KeesTalksTech.Utilities.Web.Authentication;
using System.Security.Principal;
using System.Web.Mvc;
using System.Web.Security;

/// <summary>
/// Controller that helps with Forms Authentication.
/// </summary>
/// <seealso cref="System.Web.Mvc.Controller" />
public class LogonController : Controller
{
    /// <summary>
    /// Opens the logon page.
    /// </summary>
    /// <returns>The action result.</returns>
    public virtual ActionResult Index()
    {
        return View(new LogonModel());
    }

    /// <summary>
    /// Logs the user in.
    /// </summary>
    /// <param name="model">The model.</param>
    /// <returns>The action result.</returns>
    [HttpPost]
    public virtual ActionResult Index(LogonModel model)
    {
        Authenticate(model);
        return View(model);
    }

    /// <summary>
    /// Authenticates the specified model.
    /// </summary>
    /// <param name="model">The model.</param>
    /// <param name="redirect">if set to <c>true</c> a redirect will be performed when the user is logged on.</param>
    /// <returns><c>true</c> if the model could be authenticated; otherwise <c>false</c>.</returns>
    protected virtual bool Authenticate(LogonModel model, bool redirect = true)
    {
        bool authenticated = FormsAuthentication.Authenticate(model.UserName, model.Password);

        if (!authenticated)
        {
            return false;
        }

        var user = new GenericIdentity(model.UserName);
        var principal = new SimpleConfigurationPrincipal(user);
        SimpleConfigurationPrincipal.SetAuthenticatedPrincipal(principal);

        if (redirect)
        {
            FormsAuthentication.RedirectFromLoginPage(model.UserName, false);
        }

        return true;
    }

    /// <summary>
    /// Logs the user off.
    /// </summary>
    /// <returns>The action result.</returns>
    public virtual ActionResult Logoff()
    {
        Session.Abandon();
        FormsAuthentication.SignOut();
        FormsAuthentication.RedirectToLoginPage();
        return RedirectToAction("Index");
    }
}

The View

I've used a partial view generated by Visual Studio. I only needed to change the editor for the password field into a password control (just add a type).

@model KeesTalksTech.Examples.FormsAuthentication.Models.Logon.LogonModel

@{
    ViewBag.Title = "Login";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h2>Login</h2>
<hr/>

@using (Html.BeginForm()) 
{
    @Html.AntiForgeryToken()
    
    <div class="form-horizontal">
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div class="form-group">
            @Html.LabelFor(model => model.UserName, "Username", htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.UserName, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.UserName, "", 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", type = "password" } })
                @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="Logon" class="btn btn-default" />
            </div>
        </div>
    </div>
}

It will look like this utterly simple page:

2016-06-29_0056

Application authentication

Last but not least: add the principal to the Global.asax.cs application.

protected void Application_OnPostAuthenticateRequest(object sender, EventArgs e)
{
    var user = HttpContext.Current.User;
    SimpleConfigurationPrincipal.SetAuthenticatedPrincipal(user);
}

Ow... and you might want to enforce SSL with the following code in your application:

protected void Application_BeginRequest(object sender, EventArgs e)
{
    if (HttpContext.Current.Request.IsSecureConnection.Equals(false) && 
        HttpContext.Current.Request.IsLocal.Equals(false))
    {
        Response.Redirect("https://" + Request.ServerVariables["HTTP_HOST"] + 
                           HttpContext.Current.Request.RawUrl);
    }
}

Final thoughts

The classes are on GitHub. Don't use this method in business applications! This way of forms authentication is deemed obsolete, so I don't know how long .Net will support it - but it is very good for small (hobby) projects. I've tested it with Windows Azure and it works like a charm.

And that's it. Just add the Model, the View and the Controller, add the principal to your Global.asax.cs and add the users and roles to your web.config.

Easy. As. Pie.

  1. Julian Gómez says:

    How do I create principal class?

  2. W3SERVICES INFOTECH LLP says:

    Saved my day. Found after hard search :) Thank you so much for details. If you added code for download it will be great for learner like me.

expand_less