Using OWIN and Active Directory to authenticate users in ASP.Net MVC 5 application

UPD There is a part 2 of this blog-post explaining how to do roles and fixing a minor issue with authentication.

UPD If you are on Windows 10 and get “System.IO.FileNotFoundException: The system cannot find the file specified”, have a look on this page. Thanks to David Engel for this link.

A while back I had to implement a login system that relied on in-house Active Directory. I did spend some time on figuring out how to work this in the nicest possible ways.

One of the approaches I used in the past is to slam Windows Authentication on top of the entire site and be done with it. But this is not very user-friendly – before showing anything, you are slammed with a nasty prompt for username/password. And you need to remember to include your domain name in some cases. I totally did not want that on a new green-field project. So here goes the instructions on how to do a nice authentication against your Windows Users or in-house hosted Active Directory.

For this project I’ll use Visual Studio 2015. But steps for VS2013 will be the same. Can’t say anything nice about any earlier versions of Visual Studio – I don’t use them anymore.

First create an MVC project and make sure you select “No Authentication” when you create the project:

2016-03-09 22_41_57-Start Page - Microsoft Visual Studio (Administrator)

Just after creation of a new project, I usually update all NuGet packages – it is easier to update packages when project is empty, rather later down the line. You don’t have to do this.

Now install new NuGet packages. You will need the following and all their dependencies:

  • Microsoft.Owin.Security.Cookies
  • Microsoft.Owin.Host.SystemWeb

Now in your App_Start folder create Startup.cs file (does not have to be in this folder or with this name, only following convention). Put this into that file:

using Microsoft.Owin;
using Owin;

[assembly: OwinStartupAttribute(typeof(MyProject.Startup))]
namespace MyProject
{
    public partial class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            ConfigureAuth(app);
        }
    }
}

This will not compile because you don’t have ConfigureAuth method. Add another file in App_Start called Startup.Auth.cs and put this code there:

using System;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Owin;


namespace MyProject
{
    public static class MyAuthentication
    {
        public const String ApplicationCookie = "MyProjectAuthenticationType";
    }

    public partial class Startup
    {
        public void ConfigureAuth(IAppBuilder app)
        {
            // need to add UserManager into owin, because this is used in cookie invalidation
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = MyAuthentication.ApplicationCookie,
                LoginPath = new PathString("/Login"),
                Provider = new CookieAuthenticationProvider(),
                CookieName = "MyCookieName",
                CookieHttpOnly = true,
                ExpireTimeSpan = TimeSpan.FromHours(12), // adjust to your needs
            });
        }
    }
}

Let’s see what we’ve done so far. Startup.cs file contains OwinStartupAttribute attribute on top of the class – this tells OWIN system what Configuration method required by OWIN is named MyProject.Startup. There are other ways of doing this. Second file Startup.Auth.cs contains OWIN configuration for authentication – this is very similar to what you’d see if you created a project with ASP.Net Identity authentication. Only I’ve replaced the name of AuthenticationType with my own, stored in statically-available constant – we’ll need this value elsewhere later. Also we are saying that OWIN should redirect unauthenticated requests to /Login, but we don’t have anything there at the moment. Let’s create a controller for that.

using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;


namespace ActiveDirectoryAuthentication.Controllers
{
    public class LoginController : Controller
    {
        [AllowAnonymous]
        public virtual ActionResult Index()
        {
            return View();
        }


        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public virtual ActionResult Index(LoginViewModel model)
        {
            //TODO process login
        }
    }


    public class LoginViewModel
    {
        [Required, AllowHtml]
        public string Username { get; set; }

        [Required]
        [AllowHtml]
        [DataType(DataType.Password)]
        public string Password { get; set; }
    }
}

And Index.cshtml will be like this:

@model ActiveDirectoryAuthentication.Controllers.LoginViewModel

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

<h2>Login</h2>

<div class="row">
    <div class="col-md-8">
        <section id="loginForm">
            @using (Html.BeginForm("Index", "Login", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
            {
                @Html.AntiForgeryToken()
                <hr />
                @Html.ValidationSummary(true, "", new { @class = "text-danger" })
                <div class="form-group">
                    @Html.LabelFor(m => m.Username, new { @class = "col-md-2 control-label" })
                    <div class="col-md-10">
                        @Html.TextBoxFor(m => m.Username, new { @class = "form-control" })
                        @Html.ValidationMessageFor(m => m.Username, "", new { @class = "text-danger" })
                    </div>
                </div>
                <div class="form-group">
                    @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" })
                    <div class="col-md-10">
                        @Html.PasswordFor(m => m.Password, new { @class = "form-control" })
                        @Html.ValidationMessageFor(m => m.Password, "", new { @class = "text-danger" })
                    </div>
                </div>
                <div class="form-group">
                    <div class="col-md-offset-2 col-md-10">
                        <input type="submit" value="Log in" class="btn btn-default" />
                    </div>
                </div>
            }
        </section>
    </div>
</div>

This will give us a nice login form:

2016-03-09 23_26_12-Login - My ASP.NET Application

To check if our authentication stuff works let’s put [Authorize] attribute on one of the existing actions in HomeController:

[Authorize]
public ActionResult About()
{
    ViewBag.Message = "Your application description page.";

    return View();
}

Now if you navigate to /Home/About you should be redirected to login page. It is important that this works. If you don’t get login prompt, something is wrong and you need to go back re-do missing steps. Two possible problems I can see here: /Login route does not point to LoginController or OWIN startup configuration is not called.

All of above was easy – now we need to come up with logic that actually does checking of username and password combination against AD.

Before we start any further add System.DirectoryServices.AccountManagement as a Reference to your project. And now create this class:

using System;
using System.DirectoryServices.AccountManagement;
using System.Security.Claims;
using Microsoft.Owin.Security;
using MyProject;


namespace ActiveDirectoryAuthentication.Models
{
    public class AdAuthenticationService
    {
        public class AuthenticationResult
        {
            public AuthenticationResult(string errorMessage = null)
            {
                ErrorMessage = errorMessage;
            }

            public String ErrorMessage { get; private set; }
            public Boolean IsSuccess => String.IsNullOrEmpty(ErrorMessage); 
        }

        private readonly IAuthenticationManager authenticationManager;

        public AdAuthenticationService(IAuthenticationManager authenticationManager)
        {
            this.authenticationManager = authenticationManager;
        }


        /// <summary>
        /// Check if username and password matches existing account in AD. 
        /// </summary>
        /// <param name="username"></param>
        /// <param name="password"></param>
        /// <returns></returns>
        public AuthenticationResult SignIn(String username, String password)
        {
#if DEBUG
            // authenticates against your local machine - for development time
            ContextType authenticationType = ContextType.Machine; 
#else
            // authenticates against your Domain AD
            ContextType authenticationType = ContextType.Domain;
#endif
            PrincipalContext principalContext = new PrincipalContext(authenticationType);
            bool isAuthenticated = false;
            UserPrincipal userPrincipal = null;
            try
            {
                isAuthenticated = principalContext.ValidateCredentials(username, password, ContextOptions.Negotiate);
                if (isAuthenticated)
                {
                    userPrincipal = UserPrincipal.FindByIdentity(principalContext, username);
                }
            }
            catch (Exception)
            {
                isAuthenticated = false;
                userPrincipal = null;
            }

            if (!isAuthenticated || userPrincipal == null)
            {
                return new AuthenticationResult("Username or Password is not correct");
            }

            if (userPrincipal.IsAccountLockedOut())
            {
                // here can be a security related discussion weather it is worth 
                // revealing this information
                return new AuthenticationResult("Your account is locked.");
            }

            if (userPrincipal.Enabled.HasValue && userPrincipal.Enabled.Value == false)
            {
                // here can be a security related discussion weather it is worth 
                // revealing this information
                return new AuthenticationResult("Your account is disabled");
            }

            var identity = CreateIdentity(userPrincipal);

            authenticationManager.SignOut(MyAuthentication.ApplicationCookie);
            authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = false }, identity);


            return new AuthenticationResult();
        }


        private ClaimsIdentity CreateIdentity(UserPrincipal userPrincipal)
        {
            var identity = new ClaimsIdentity(MyAuthentication.ApplicationCookie, ClaimsIdentity.DefaultNameClaimType, ClaimsIdentity.DefaultRoleClaimType);
            identity.AddClaim(new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", "Active Directory"));
            identity.AddClaim(new Claim(ClaimTypes.Name, userPrincipal.SamAccountName));
            identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userPrincipal.SamAccountName));
            if (!String.IsNullOrEmpty(userPrincipal.EmailAddress))
            {
                identity.AddClaim(new Claim(ClaimTypes.Email, userPrincipal.EmailAddress));
            }

            // add your own claims if you need to add more information stored on the cookie

            return identity;
        }
    }
}

Then in you LoginController do the following changes:

using System;
using System.ComponentModel.DataAnnotations;
using System.Web;
using System.Web.Mvc;
using ActiveDirectoryAuthentication.Models;
using Microsoft.Owin.Security;


namespace ActiveDirectoryAuthentication.Controllers
{
    public class LoginController : Controller
    {
        [AllowAnonymous]
        public virtual ActionResult Index()
        {
            return View();
        }


        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public virtual ActionResult Index(LoginViewModel model, string returnUrl)
        {
            if (!ModelState.IsValid)
            {
                return View(model);
            }

            // usually this will be injected via DI. but creating this manually now for brevity
            IAuthenticationManager authenticationManager = HttpContext.GetOwinContext().Authentication;
            var authService = new AdAuthenticationService(authenticationManager);

            var authenticationResult = authService.SignIn(model.Username, model.Password);

            if (authenticationResult.IsSuccess)
            {
                // we are in!
                return RedirectToLocal(returnUrl);
            }

            ModelState.AddModelError("", authenticationResult.ErrorMessage);
            return View(model);
        }


        private ActionResult RedirectToLocal(string returnUrl)
        {
            if (Url.IsLocalUrl(returnUrl))
            {
                return Redirect(returnUrl);
            }
            return RedirectToAction("Index", "Home");
        }


        [ValidateAntiForgeryToken]
        public virtual ActionResult Logoff()
        {
            IAuthenticationManager authenticationManager = HttpContext.GetOwinContext().Authentication;
            authenticationManager.SignOut(MyAuthentication.ApplicationCookie);

            return RedirectToAction("Index");
        }
    }


    public class LoginViewModel
    {
        [Required, AllowHtml]
        public string Username { get; set; }

        [Required]
        [AllowHtml]
        [DataType(DataType.Password)]
        public string Password { get; set; }
    }
}

Now you will be able to login with your own PC password. On my home PC I have no Domain, so when I’m creating a authenticationType I show ContextType.Machine – this will check username/password against users on your local machine. This is handy for development time if your machine is not in domain. When you deploy to the server most likely you’ll have to change this to ContextType.Domain. In the code above I do this change by compilation type – if you compile in Debug, you’ll get Machine authentication; for Release configuration you’ll get Domain authentication. In my latest project that uses this type of authentication I specify what type of authentication I need in web.config. You can do it, but leave it for later, once you have everything running as required.

So once you authenticated an authentication cookie is set on your requests. The only minor problem is that we don’t see logged-in username anywhere and no way to log-out. Let’s make this happen.

In Views/Shared create new Razor file _LoginPartial.cshtml:

@if (Request.IsAuthenticated)
{
    using (Html.BeginForm("Logoff", "Login", FormMethod.Post, new { id = "logoutForm", @class = "navbar-right" }))
    {
        @Html.AntiForgeryToken()

        <ul class="nav navbar-nav navbar-right dropdown">
            <li class="dropdown">
                <a href="#" class="dropdown-toggle" data-toggle="dropdown">@User.Identity.Name<span class="caret"></span></a>
                <ul class="dropdown-menu" role="menu">
                    <li><a href="javascript:document.getElementById('logoutForm').submit()">Log off</a></li>
                </ul>
            </li>
        </ul>
    }
}
else
{
    <ul class="nav navbar-nav navbar-right">
        <li>@Html.ActionLink("Log in", "Index", "Login", null, new { id = "loginLink" })</li>
    </ul>
}

In Views/Shared/_Layout.cshtml add @Html.Partial("_LoginPartial"):

.......SNIP........
<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>
.......SNIP........

And now you should be able to login with your machine/Domain user credentials and logout. You may ask about password reset, but I have not implemented this yet, though I might need it pretty soon – I’ll blog about this separately.

In case you struggle, I have posted full solution to GitHub repository.