Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Switch User Functionality using MVC4 and Windows Authentication

0.00/5 (No votes)
23 Oct 2014 1  
How to implement Switch user functionality using MVC4 and Windows Authentication (a bit like SharePoint)

Introduction

While implementing an MVC4 Intranet Web Application while using Windows authentication (LDAP), I ran into several obstacles I had to overcome. For example: how to configure your application and webserver for using Windows Authentication (See my tip: Windows authentication on intranet website using AD and Windows Server 2012 (or higher)).

The obstacle this tip is about is the Switch User functionality (like you might know from SharePoint).

The solution describes a small login partial, one corresponding action method in a controller, a small helper class and a 401 MVC Route configuration.

The solution requires the use of a cookie.

Using the Code

Let's start by creating a partial view that you can include in the header of your _Layout view (@Html.Partial("_LoginPartial")):

@using System.Security.Principal
@using My_MVC4_App.App_GlobalResources
<script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.js")" 
type="text/javascript"></script>
<div id="usernameDiv">
    
   @using (Html.BeginForm("SwitchUser", "Login", 
   new { returnUrl = Request.Url == null ? "" : Request.Url.PathAndQuery }, FormMethod.Post))
    {
        @Html.AntiForgeryToken()
        @(User.Identity.IsAuthenticated ? "Welcome, " + 
        User.Identity.Name : (WindowsIdentity.GetCurrent() != null ? "Welcome, " + 
        WindowsIdentity.GetCurrent().Name : "Not logged in."))
        @:&nbsp;<input type="submit" 
        id="SwitchUserButton" value="@Texts.SwitchUser" />
    }
</div>

When the SwitchUserButton is clicked, the SwitchUser action method in the LoginControlled is called:

using System.Web.Mvc;
using My_MVC4_App.Helpers;

namespace My_MVC4_App.Controllers
{
    public class LoginController : Controller
    {
        [AllowAnonymous]
        public ActionResult SwitchUser(string returnUrl)
        {
            ViewBag.ReturnUrl = returnUrl;
            
            var lh = new LoginHelper(Request, Response);
            lh.DisablePageCaching();
            lh.AuthenticationAttempts = lh.AuthenticationAttempts + 1;
            if (lh.AuthenticationAttempts == 1)
            {
                lh.PreviousUser = User.Identity.Name;
                lh.Send401(returnUrl);
            }
            else
            {
                // If the browser uses "auto sign in with current credentials", a second 401 response
                // needs to be send to let the browser re-authenticate him self
                if (lh.AuthenticationAttempts == 2 && lh.CurrentUser.Equals(lh.PreviousUser))
                {
                    lh.AuthenticationAttempts = 0;
                    lh.Send401(returnUrl);
                }
                else
                {
                    lh.AuthenticationAttempts = 0;
                }
            }

            // If a valid returnUrl is passed to the action method, the browser will redirect to this 
            // url when the user is authenticated
            if (Url.IsLocalUrl(returnUrl) && returnUrl.Length > 1 && returnUrl.StartsWith("/")
                        && !returnUrl.StartsWith("//") && !returnUrl.StartsWith("/\\"))
            {
                return Redirect(returnUrl);
            }

            // If the returnUrl is invalid, this will redirect to the home controller
            return RedirectToAction("Index", "Home");
        }
    }
}

I think the code is quite self explanatory, so I will minimize my comments for this action method:

In the code, you see that a new instance of the LoginHelper class is loaded and the current HttpRequestBase and HttpResponseBase is passed.

Within the LoginHelper class, the request is used to read the authentication cookie and to get the current authenticated Windows user. The Response is used to add the modified cookie, handle caching and to trigger the browser to have the Windows Authentication Login popup appear.

using System;
using System.Web;

namespace My_MVC4_App.Helpers
{
    public class LoginHelper
    {
        private readonly HttpRequestBase _request;
        private readonly HttpResponseBase _response;
        private readonly HttpCookie _cookie;

        public LoginHelper(HttpRequestBase request, HttpResponseBase response)
        {
            _request = request;
            _response = response;
            _cookie = _request.Cookies["TSWA-Last-User"] ?? new HttpCookie("TSWA-Last-User")
                {
                    Expires = DateTime.Now.AddMinutes(60)
                };
        }
        
        private int _authenticationAttempts;
        public int AuthenticationAttempts
        {
            get
            {
                if (_cookie != null && 
                !string.IsNullOrWhiteSpace(_cookie["AuthenticationAttempts"]))
                {
                    int.TryParse(_cookie["AuthenticationAttempts"], out _authenticationAttempts);
                }
                return _authenticationAttempts;
            }
            set
            {
                _authenticationAttempts = value;
                _cookie["AuthenticationAttempts"] = _authenticationAttempts.ToString();
                _cookie["CurrentUser"] = _currentUser;
                _cookie["PreviousUser"] = PreviousUser;
                _response.Cookies.Add(_cookie);
            }
        }

        private string _currentUser = string.Empty;
        public string CurrentUser
        {
            get
            {
                _currentUser = _request.LogonUserIdentity != null ? 
                                     _request.LogonUserIdentity.Name : "";
                if (_cookie != null && !string.IsNullOrWhiteSpace(_cookie["CurrentUser"]))
                {
                    _currentUser = _cookie["CurrentUser"];
                }
                return _currentUser;
            }
            set
            {
                _currentUser = value;
                _cookie["AuthenticationAttempts"] = _authenticationAttempts.ToString();
                _cookie["CurrentUser"] = _currentUser;
                _cookie["PreviousUser"] = PreviousUser;
                _response.Cookies.Add(_cookie);
            }
        }

        private string _previousUser = string.Empty;
        public string PreviousUser
        {
            get
            {
                if (_cookie != null && !string.IsNullOrWhiteSpace(_cookie["PreviousUser"]))
                {
                    _previousUser = _cookie["PreviousUser"];
                }
                return _previousUser;
            }
            set
            {
                _previousUser = value;
                _cookie["AuthenticationAttempts"] = _authenticationAttempts.ToString();
                _cookie["CurrentUser"] = _currentUser;
                _cookie["PreviousUser"] = _previousUser;
                _response.Cookies.Add(_cookie);
            }
        }

        /// <summary>
        /// Make sure the browser does not cache this page
        /// </summary>
        public void DisablePageCaching()
        {
             _response.Expires = 0;
             _response.Cache.SetNoStore();
             _response.AppendHeader("Pragma", "no-cache");
        }

        /// <summary>
        /// Send a 401 response
        /// <param name="returnUrl">
        /// For passing the returnUrl in order to force a refresh of the
        /// current page in case the cancel button in the Login popup has been clicked</param>
        /// </summary>
        public void Send401(string returnUrl)
        {
            _response.AppendHeader("Connection", "close");
            _response.StatusCode = 0x191;
            _response.Clear();
            _response.Write("Login cancelled. Please wait to be redirected...");
            // A Refresh header needs to be added in order to keep the application going after the
            // Windows Authentication Login popup is cancelled:
            _response.AddHeader("Refresh", "0; url=" + returnUrl);
            _response.End();
        }
    }
}

The LoginHelper basically uses Cookies to keep track of the amount of authentication attempts, the previous user and the current user. (I have also tried to use the Session object to store this information, but unfortunately the Session is cleared every time the SwitchUser action method is called.)

The Send401(..) function makes sure the current Response is modified to send a 401 message to the browser and ends its default response. Also a custom text is added, which will be briefly displayed when the Popup Cancel button is clicked.

Furthermore, a header is added to the Response to trigger a refresh of the current URL. When this is not added, the user will be confronted with a white page and the custom "Login cancelled." text. The URL will then look something like "http://localhost:53130/Login/SwitchUser?returnUrl=%2F" and the user needs to manually refresh the page.

Finally, sometimes the Refresh function, after cancelling the Login popup, suddenly doesn't work (once in a blue moon it seems). In that case, a 401.2 error message is thrown.
In order to catch that error page, you need to configure a Route for the Redirect:

routes.MapRoute(
    name: "401-Unauthorized",
    url: "{controller}/{action}/{id}",
    defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);

Once this last little piece is in place, your application is good to go! Enjoy!!!

Points of Interest

In order to configure your application and webserver for using Windows Authentication, see my tip: Windows authentication on intranet website using AD and Windows Server 2012 (or higher).

History

  • 2014-10-23 - Initial publication

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here