ASP.NET MVC and mixed mode authentication

This is called mixed authentication mode. Basically you cannot achieve this within a single application because in IIS once you set up Windows authentication for a virtual directory it will no longer accept users from different domains. So basically you need to have two applications, the first with Windows Authentication and the second (the main application) using Forms authentication. The first application will consist of a single address which will simply redirect to the main application by issuing an authentication ticket for the domain user.


This can be done. Reverse the configuration, set the app/root to use Anonymous and Forms Authentication... In this way, you can configure mixed authentication within the same web application, but it is tricky. So first, configure you app for Forms Authentication with loginUrl="~/WinLogin/WinLogin2.aspx". In MVC, routing overrides authentication rules set by IIS, so need to use an aspx page, as IIS can set authentication on the file. Enable Anonymous and Forms Authentication on the root web application. Enable Windows Authentication and disable anonymous authentication in root/WinLogin directory. Add custom 401 and 401.2 error pages to redirect back to the Account/Signin URL.

This will allow any browser capable of pass-through to use windows integrated authentication to auto signin. While some devices will get prompted for credentials (like iPhone) and other devices like blackberry redirected to signin page.

This also creates a cookie explicitly adding the users roles and creates a Generic principle so that role-based authorization can be used.

in WinLogin2.aspx (in WinLogin directory under "root" web application in IIS, and configured to use Windows Authentication, Anonymous disabled, and Forms enabled (as can't turn off...note IIS will complain when you enable windows authentication, just ignore) :

var logonUser = Request.ServerVariables["LOGON_USER"];
if (!String.IsNullOrWhiteSpace(logonUser))
{
    if (logonUser.Split('\\').Length > 1)
    {
        var domain = logonUser.Split('\\')[0];
        var username = logonUser.Split('\\')[1];

        var timeout = 30;

        var encTicket = CreateTicketWithSecurityGroups(false, username, domain, timeout);

        var authCookie = new HttpCookie(".MVCAUTH", encTicket) { HttpOnly = true };
        Response.Cookies.Add(authCookie);
    }
    //else
    //{
    // this is a redirect due to returnUrl being WinLogin page, in which logonUser will no longer have domain attached
    // ignore as forms ticket should already exist
    //}

    string returnUrl = Request.QueryString["ReturnUrl"];

    if (returnUrl.IsEmpty())
    {
        Response.Redirect("~/");
    }
    else
    {
        Response.Redirect(returnUrl);
    }
}

public static string CreateTicketWithSecurityGroups(bool rememberMe, string username, string domain, int timeout)
{
    using (var context = new PrincipalContext(ContextType.Domain, domain))
    {
        using (var principal = UserPrincipal.FindByIdentity(context, IdentityType.SamAccountName, username))
        {
            var securityGroups = String.Join(";", principal.GetAuthorizationGroups());

            var ticket =
                new FormsAuthenticationTicket(1,
                                                username,
                                                DateTime.UtcNow,
                                                DateTime.UtcNow.AddMinutes(timeout),
                                                rememberMe,
                                                securityGroups,
                                                "/");

            string encTicket = FormsAuthentication.Encrypt(ticket);
            return encTicket;
        }
    }
}

In IIS 7.5, click Error Pages, set the 401 page to File path of Redirect401.htm file, with this code:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <script>
      window.location.assign('../Account/Signin');
    </script>
</head>
<body>
</body>
</html>

In AccountController...

public ActionResult SignIn()
{
    return View(new SignInModel());
}

//
// POST: /Account/SignIn
[HttpPost]
public ActionResult SignIn(SignInModel model, string returnUrl)
{
    if (ModelState.IsValid)
    {
        if (Membership.ValidateUser(model.UserName, model.Password))
        {
            string encTicket = CreateTicketWithSecurityGroups(model.RememberMe,  model.UserName, model.Domain, FormsAuthentication.Timeout.Minutes);

            Response.Cookies.Add(new HttpCookie(".MVCAUTH", encTicket));

            //var returnUrl = "";
            for (var i = 0; i < Request.Cookies.Count; i++)
            {
                HttpCookie cookie = Request.Cookies[i];
                if (cookie.Name == ".MVCRETURNURL")
                {
                    returnUrl = cookie.Value;
                    break;
                }
            }

            if (returnUrl.IsEmpty())
            {
                return Redirect("~/");
            }

            return Redirect(returnUrl);
        }

        ModelState.AddModelError("Log In Failure", "The username/password combination is invalid");
    }

    return View(model);
}

//
// GET: /Account/SignOut
public ActionResult SignOut()
{
    FormsAuthentication.SignOut();

    if (Request.Cookies[".MVCRETURNURL"] != null)
    {
        var returnUrlCookie = new HttpCookie(".MVCRETURNURL") { Expires = DateTime.Now.AddDays(-1d) };
        Response.Cookies.Add(returnUrlCookie);
    }

    // Redirect back to sign in page so user can 
    //   sign in with different credentials

    return RedirectToAction("SignIn", "Account");
}

In global.asax:

protected void Application_BeginRequest(object sender, EventArgs e)
{
    try
    {
        bool cookieFound = false;

        HttpCookie authCookie = null;

        for (int i = 0; i < Request.Cookies.Count; i++)
        {
            HttpCookie cookie = Request.Cookies[i];
            if (cookie.Name == ".MVCAUTH")
            {
                cookieFound = true;
                authCookie = cookie;
                break;
            }
        }

        if (cookieFound)
        {
            // Extract the roles from the cookie, and assign to our current principal, which is attached to the HttpContext.
            FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(authCookie.Value);
            HttpContext.Current.User = new GenericPrincipal(new FormsIdentity(ticket), ticket.UserData.Split(';'));
        }
    }
    catch (Exception ex)
    {
        throw;
    }
}


protected void Application_AuthenticateRequest()
{
    var returnUrl = Request.QueryString["ReturnUrl"];
    if (!Request.IsAuthenticated && !String.IsNullOrWhiteSpace(returnUrl))
    {
        var returnUrlCookie = new HttpCookie(".MVCRETURNURL", returnUrl) {HttpOnly = true};
        Response.Cookies.Add(returnUrlCookie);
    }
}

web.config

<system.web>
  <!--<authorization>
    <deny users="?"/>
  </authorization>-->
  <authentication mode="Forms">
    <forms name=".MVCAUTH" loginUrl="~/WinLogin/WinLogin2.aspx" timeout="30" enableCrossAppRedirects="true"/>
  </authentication>
  <membership defaultProvider="AspNetActiveDirectoryMembershipProvider">
    <providers>
      <add
           name="AspNetActiveDirectoryMembershipProvider"
           type="System.Web.Security.ActiveDirectoryMembershipProvider, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
           connectionStringName="ADService" connectionProtection="Secure" enablePasswordReset="false" enableSearchMethods="true" requiresQuestionAndAnswer="true"
           applicationName="/" description="Default AD connection" requiresUniqueEmail="false" clientSearchTimeout="30" serverSearchTimeout="30"
           attributeMapPasswordQuestion="department" attributeMapPasswordAnswer="division" attributeMapEmail="mail" attributeMapUsername="sAMAccountName"
           maxInvalidPasswordAttempts="5" passwordAttemptWindow="10" passwordAnswerAttemptLockoutDuration="30" minRequiredPasswordLength="7"
           minRequiredNonalphanumericCharacters="1" />
    </providers>
  </membership>
  <machineKey decryptionKey="..." validationKey="..." />
</system.web>
<connectionStrings>
  <add name="ADService" connectionString="LDAP://SERVER:389"/>
</connectionStrings>

Credit owed to http://msdn.microsoft.com/en-us/library/ms972958.aspx