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 (lh.AuthenticationAttempts == 2 && lh.CurrentUser.Equals(lh.PreviousUser))
{
lh.AuthenticationAttempts = 0;
lh.Send401(returnUrl);
}
else
{
lh.AuthenticationAttempts = 0;
}
}
if (Url.IsLocalUrl(returnUrl) && returnUrl.Length > 1 && returnUrl.StartsWith("/")
&& !returnUrl.StartsWith("//") && !returnUrl.StartsWith("/\\"))
{
return Redirect(returnUrl);
}
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);
}
}
public void DisablePageCaching()
{
_response.Expires = 0;
_response.Cache.SetNoStore();
_response.AppendHeader("Pragma", "no-cache");
}
public void Send401(string returnUrl)
{
_response.AppendHeader("Connection", "close");
_response.StatusCode = 0x191;
_response.Clear();
_response.Write("Login cancelled. Please wait to be redirected...");
_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