Switch User Functionality using MVC4 and Windows Authentication






4.67/5 (4 votes)
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."))
@: <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