26

I have a scenario whereby I require users to be able to authenticate against an ASP.NET MVC web application using either Windows authentication or Forms authentication. If the user is on the internal network they will use Windows authentication and if they are connecting externally they will use Forms authentication. I’ve seen quite a few people asking the question how do I configure an ASP.NET MVC web application for this, but I haven’t found a complete explanation.

Please can someone provide a detailed explanation, with code examples, on how this would be done?

Thanks.

Alan T

Alan T
  • 3,294
  • 6
  • 27
  • 27

4 Answers4

18

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.

Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • This will not work in IIS7 using integrated mode: http://stackoverflow.com/questions/289317/iis7-and-authentication-problems – Garry English Dec 09 '11 at 22:06
  • I found that out too Garry. I'm still looking for a solution for this as I have two MVC applications now that will require this functionality. – Jeff Reddy Feb 03 '12 at 16:29
18

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

Dan Atkinson
  • 11,391
  • 14
  • 81
  • 114
Mark
  • 191
  • 1
  • 3
  • 1
    Hi - You show the Redirect401.htm file redirecting via added script. Can you not achieve this by setting error handling for 401.1 as a redirect? In IIS this would mean changing to the 'respond with 302 redirect' option for the 401 error page. Is says to use an absolute url, but root relative also works. – Paul George Jun 13 '12 at 09:43
  • Using this solution would AD users logon automatically? – Akmal Salikhov Apr 05 '18 at 06:53
4

This will probably live at the bottom of this question and never be found but I was able to implement what was described at

http://mvolo.com/iis-70-twolevel-authentication-with-forms-authentication-and-windows-authentication/

It was quite easy and trivial. Didn't require multiple applications or cookie hacks, just extending the FormsAuthModule and making some web.config changes.

Luke
  • 1,916
  • 1
  • 15
  • 15
  • 1
    I think the scenario you mentioned is different than the scenario in question. two-level authentication requires a user to be authenticated with both windows as well as forms authentication. However we require that based on the kind of user (intranet/internet) they should be authenticated by either windows or forms based authentication – Samra Sep 20 '17 at 04:03
0

I know this is an old post - but everything lives forever on the internet!

Anyway, I had to move an old website from IIS6 to IIS8. This is a WebForms website, but I assume this very simple solution is the same.

I received the error : Unable to cast object of type 'System.Security.Principal.WindowsIdentity' to type 'System.Web.Security.FormsIdentity'.

All I did was create a new application pool for the website. When creating this, I set the Managed pipeline mode to 'Classic'. (Read more here - http://www.hanselman.com/blog/MovingOldAppsFromIIS6ToIIS8AndWhyClassicModeExists.aspx) Dont forget to set the website's application pool to the new pool you just created.

jagdipa
  • 420
  • 1
  • 5
  • 18