Authentication using OWIN and Active Directory

While Kaliko CMS provides an optional ASP.NET Identity based authentication provider, it's possible to use any authentication provider that would work in a standard .NET project. This tutorial will show how to add authentication using OWIN and Active Directory to your Kaliko CMS project.

This article assumes that you already have Kaliko CMS project setup and running. If you haven't here's a guide on how to do it. It also assumes that you haven't installed the KalikoCMS.Identity nuget. If you have (and you don't plan to do a mixed mode aunthentication scheme) you can remove it from the package manager.

Install the required packages

Install the following NuGet-packages to your project:

  • Microsoft.Owin.Security.Cookies
  • Microsoft.Owin.Host.SystemWeb
  • System.DirectoryServices.AccountManagement

Setup OWIN

Add a new class to App_Start called Startup.cs:

using AdAuthenticationLab;
using Microsoft.Owin;
using Owin;

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

Add another class to App_Start called Startup.Auth.cs:

namespace AdAuthenticationLab {
    using System;
    using Microsoft.Owin;
    using Microsoft.Owin.Security.Cookies;
    using Owin;

    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
            });
        }
    }
}

Create the login service

Add a new class to your project called AdAuthenticationService.cs:

namespace AdAuthenticationLab.Models {
    using System;
    using System.DirectoryServices.AccountManagement;
    using System.Security.Claims;
    using Microsoft.Owin.Security;

    public class AdAuthenticationService {
        public class AuthenticationResult {
            public AuthenticationResult(string errorMessage = null) {
                ErrorMessage = errorMessage;
            }

            public string ErrorMessage { get; private set; }
            public bool IsSuccess => string.IsNullOrEmpty(ErrorMessage);
        }

        private readonly IAuthenticationManager _authenticationManager;

        public AdAuthenticationService(IAuthenticationManager authenticationManager) {
            _authenticationManager = authenticationManager;
        }

        public AuthenticationResult SignIn(String username, String password) {
            // Use ContextType authenticationType = ContextType.Machine; if you need for local development
            ContextType authenticationType = ContextType.Domain;

            PrincipalContext principalContext = new PrincipalContext(authenticationType, "YOURDOMAINHERE");
            bool isAuthenticated = false;
            UserPrincipal userPrincipal = null;
            try {
                userPrincipal = UserPrincipal.FindByIdentity(principalContext, username);
                if (userPrincipal != null) {
                    isAuthenticated = principalContext.ValidateCredentials(username, password, ContextOptions.Negotiate);
                }
            }
            catch (Exception exception) {
                return new AuthenticationResult("Username or Password is not correct");
            }

            if (!isAuthenticated) {
                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));
            }

            var groups = userPrincipal.GetAuthorizationGroups();
            foreach (var @group in groups) {
                identity.AddClaim(new Claim(ClaimTypes.Role, @group.Name));
            }

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

            return identity;
        }
    }
}

Be sure to change YOURDOMAINHERE in the code above to your domain instead.

Create a login view

Add a new controller to your project called LoginController:

namespace AdAuthenticationLab.Controllers {
    using System.ComponentModel.DataAnnotations;
    using System.Web;
    using System.Web.Mvc;
    using Models;

    public class LoginController : Controller {
        [AllowAnonymous]
        public virtual ActionResult Index(string returnUrl) {
            var model = new LoginViewModel {ReturnUrl = returnUrl};
            return View(model);
        }

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

            var authenticationManager = HttpContext.GetOwinContext().Authentication;
            var authService = new AdAuthenticationService(authenticationManager);

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

            if (authenticationResult.IsSuccess) {
                return RedirectToLocal(model.ReturnUrl);
            }

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

        private ActionResult RedirectToLocal(string returnUrl) {
            if (Url.IsLocalUrl(returnUrl)) {
                return Redirect(returnUrl);
            }
            return Redirect("/");
        }

        public virtual ActionResult Logoff() {
            var 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; }

        public string ReturnUrl { get; set; }
    }
}

Add a new view under Views\Login\Index.cshtml:

@model AdAuthenticationLab.Controllers.LoginViewModel

@{
    ViewBag.Title = "Login";
    Layout = null;
}

<html>
<head>
    <title>Log in</title>
    <meta name="robots" content="noindex, nofollow" />
    <style type="text/css">
        /*! normalize.css v3.0.2 | MIT License | git.io/normalize */html { font-family: sans-serif; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100% }body { margin: 0 }article, aside, details, figcaption, figure, footer, header, hgroup, main, menu, nav, section, summary { display: block }audio, canvas, progress, video { display: inline-block; vertical-align: baseline }audio:not([controls]) { display: none; height: 0 }[hidden], template { display: none }a { background-color: transparent }a:active, a:hover { outline: 0 }abbr[title] { border-bottom: 1px dotted }b, strong { font-weight: 700 }dfn { font-style: italic }h1 { font-size: 2em; margin: .67em 0 }mark { background: #ff0; color: #000 }small { font-size: 80% }sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline }sup { top: -.5em }sub { bottom: -.25em }img { border: 0 }svg:not(:root) { overflow: hidden }figure { margin: 1em 40px }hr { -moz-box-sizing: content-box; box-sizing: content-box; height: 0 }pre { overflow: auto }code, kbd, pre, samp { font-family: monospace, monospace; font-size: 1em }button, input, optgroup, select, textarea { color: inherit; font: inherit; margin: 0 }button { overflow: visible }button, select { text-transform: none }button, html input[type=button], input[type=reset], input[type=submit] { -webkit-appearance: button; cursor: pointer }button[disabled], html input[disabled] { cursor: default }button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0 }input { line-height: normal }input[type=checkbox], input[type=radio] { box-sizing: border-box; padding: 0 }input[type=number]::-webkit-inner-spin-button, input[type=number]::-webkit-outer-spin-button { height: auto }input[type=search] { -webkit-appearance: textfield; -moz-box-sizing: content-box; -webkit-box-sizing: content-box; box-sizing: content-box }input[type=search]::-webkit-search-cancel-button, input[type=search]::-webkit-search-decoration { -webkit-appearance: none }fieldset { border: 1px solid silver; margin: 0 2px; padding: .35em .625em .75em }legend { border: 0; padding: 0 }textarea { overflow: auto }optgroup { font-weight: 700 }table { border-collapse: collapse; border-spacing: 0 }td, th { padding: 0 }

        /* Custom styles */
        body { background: #1570A6; color: #222222; }
        body, input { font-family: Arial, sans-serif; font-size: 15px; }
        div { box-sizing: border-box; }
        .login { background: #fafafa; width: 420px; margin: 100px auto 0; border: 10px solid #12608C; }
        .head { background: none repeat scroll 0 0 #f0f0f0; border-bottom: 1px solid #d8d8d8; line-height: 50px; text-align: center; }
        .login-form { padding: 20px 20px; }
        .login-form p { margin-top: 0; }
        .form-group { width: 100%; }
        .control-label { font-size: 12px; font-weight: bold; }
        .form-group input[type=text],
        .form-group input[type=password] { width: 340px; padding: 10px; z-index: 9; position: relative; font-size: 15px; margin-top: 5px; margin-bottom: 5px; border: 1px solid #d8d8d8; border-radius: 3px; }
        .form-group .controls { display: inline-block; }
        .btn { background: -moz-linear-gradient(top, #d8d8d8 0%, #f8f8f8 100%); background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #d8d8d8), color-stop(100%, #f8f8f8)); background: -webkit-linear-gradient(top, #d8d8d8 0%, #f8f8f8 100%); background: -o-linear-gradient(top, #d8d8d8 0%, #f8f8f8 100%); background: -ms-linear-gradient(top, #d8d8d8 0%, #f8f8f8 100%); background: linear-gradient(to bottom, #d8d8d8 0%, #f8f8f8 100%); border: 1px solid #bbbbbb; border-radius: 3px; box-shadow: 1px 1px 0 #ffffff inset, 1px 3px 2px #d8d8d8; padding: 8px 20px; cursor: pointer; width: 100px; }
        .btn:active { box-shadow: 2px 2px 1px #cccccc inset; }
        .checkbox { float: right; line-height: 40px; }
    </style>
</head>
<body>
<div class="login">
    <div class="head">
        Login
    </div>
    <div class="login-form">
        @using (Html.BeginForm("Index", "Login", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) {
            @Html.AntiForgeryToken()
            
            @Html.ValidationSummary(true, "", new { @class = "text-danger" })
            @Html.HiddenFor(x => x.ReturnUrl)
            <div class="form-group">
                @Html.LabelFor(m => m.Username, new { @class = "col-md-2 control-label" })
                <div>
                    @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>
                    @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>
                    <input type="submit" value="Log in" class="btn" />
                </div>
            </div>
        }
    </div>
</div>
</body>
</html>

Rewire logout.aspx

The administration interface links to a WebForm for logout. For this to work with our new controller we need to add a redirect.

Create a WebForm in your project root called Logout.aspx. Add a redirect in the code-behind of your new WebForm:

using System;

namespace AdAuthenticationLab {
    public partial class Logout : System.Web.UI.Page {
        protected void Page_Load(object sender, EventArgs e) {
            Response.Redirect("login/logoff");
        }
    }
}

Update the configuration

Update the web.config and set the allowed roles and/or users under the admin-location:

  <location path="Admin">
    <!-- Denies access for users except admins by default, change roles to match your authentication scheme -->
    <system.web>
      <authorization>
        <allow roles="MyActiveDirectoryGroup" />
        <deny users="*" />
      </authorization>
    <pages validateRequest="false" />
    <httpRuntime requestValidationMode="2.0" />
  </system.web>

Do not include the domain part for the roles. Instead of MYDOMAIN\MyGroup just use only MyGroup.

Final notes

The code provided above should be seen as a sample and might contain parts that could be improved. Acknowledgement goes to this article that does a good job describing OWIN and Active Directory usage in a MVC-app.