Hi Community,
Today’s post is about a common issue faced by many Web developers when they build an MVC Web application that uses ADFS as its authentication mechanism. The problem lies that sessions might be abandoned by IIS when their time is up, but the MVC application might not even be aware of this fact, therefore, by requesting the same page or navigating to another page IIS will re-create a session but this might represent a security flaw or risk because users are not being redirected to the login page to re-enter their credentials.
ASP.NET and all its features (Web Forms or MVC) are tightly coupled to IIS, and in most cases and before this “Federation” era we are currently in, this was taken care of by leveraging “Form-based Authentication” (FBA), but as I’ve previously mentioned there is a new player in this picture, and that is ADFS.
ADFS (stands for Active Directory Federation Services) and it’s a software component developed by Microsoft that can be installed on Windows Server operating systems to provide users with single sign-on access to systems and applications located across organizational boundaries. ADFS uses and relies on claims-based access (CBA) to enforce and maintain application security.
By implementing ADFS, the standard ASP.NET FBA is ignored by delegating its task to ADFS. Everything else remains the same, like session management in this case we’re assuming it is “InProc”.
The security issue arises when session times out but users are never prompted to re-enter their credentials, in order to make this solution work we must then store a tiny value in a session variable. Remember, MVC shares a lot of functionality with Web Forms and even when storing information in the Session object might cause more problems than resolving issues, it’s always a good practice avoid storing much information in it (regardless of whether it’s a Web Form or MVC application).
We just store a very simple value to the recently created session – Expiration time.
/////////////////
/* Global.asax */
////////////////
/// <summary>
/// Handles the Start event of the Session control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
protected void Session_Start(object sender, EventArgs e) {
var currentTime = DateTime.Now;
var timeOut = Session.Timeout;
Session["_Expiration_"] = currentTime.AddMinutes(timeOut);
}
/// <summary>
/// Handles the End event of the Session control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
protected void Session_End(object sender, EventArgs e) {
Session.Clear();
}
ASP.NET MVC provides a flexible yet powerful mechanism that allows developers to decorate their controllers and the actions they can do. By implementing this custom action filter, and decorating the “BaseController” or any controller we can ensure that it’ll be executed before any method within the controller.
//////////////////////////
/* SessionExpiration.cs */
/////////////////////////
/// <summary>
/// Filter responsible for signing out user if sessio has expired
/// </summary>
/// <seealso cref="System.Web.Mvc.ActionFilterAttribute" />
public class SessionExpiration : ActionFilterAttribute {
/// <summary>
/// Called when [action executing].
/// </summary>
/// <param name="filterContext">The filter context.</param>
public override void OnActionExecuting(ActionExecutingContext filterContext) {
var ctx = HttpContext.Current;
var replyUrl = ConfigurationManager.AppSettings["SignOutReply"];
var encodedReply = WebUtility.HtmlEncode(replyUrl);
var signoutUrl = ConfigurationManager.AppSettings["FederatedSignOutUrl"];
var signOut = $"{signoutUrl}?wa=wsignout1.0&wreply={encodedReply}";
if (ctx.Session != null) {
// check if a new session id was generated
if (ctx.Session.IsNewSession) {
// If it's a new session, but an existing cookie exists, then it must have timed out hence it's redirected to signout page
var sessionCookie = ctx.Request.Headers["Cookie"];
if (!string.IsNullOrEmpty(sessionCookie) &&
(sessionCookie.IndexOf("ASP.NET_SessionId", StringComparison.InvariantCultureIgnoreCase) >= 0))
ctx.Response.Redirect(signOut);
}
}
base.OnActionExecuting(filterContext);
}
}
In order to wire-up our custom action filter, we must register it by adding it to the GlobalFilterCollection, otherwise it won’t run.
/////////////////////
/* FilterConfig.cs */
/////////////////////
public class FilterConfig {
/// <summary>
/// Registers the global filters.
/// </summary>
/// <param name="filters">The filters.</param>
public static void RegisterGlobalFilters(GlobalFilterCollection filters) {
filters.Add(new SessionExpiration());
}
}
And that’s pretty much it. If session times out and user tries to refresh the page or go to any other page is taken back to the ADFS logon page so they can re-enter their credentials. We could have also made something fancier by adding client side code and accomplish the same thing using AJAX, but it’s not the intent or scope of this post.
Regards,
Angel
Excellent post, thanks. Where can I learn how to set your variables “SignOutReply” and “SignoutOutURL”? I’m new to ADFS, and I only have app settings for “ida:ADFSMetadata” and “ida:Wtrealm” which are set to where to login, and where to be redirected after login, respectively.
The ADFS authentication works fine, but I left the site in debug mode when I went home last night , and realized an obvious thing this morning – how long should Single sign on last? not forever.
Thanks again
Hi Dave,
Those are in the application’s config file and they point to ADFS’ URLs.
Cheers,
Angel
My friend you saved me a lot of time (again)… Thank you!, God Bless You!
No worries at all, my old good friend.
Happy to help and God bless you too!
Great post, Angel!
I tried to do it in my project. There’s only one problem “ASP.NET_SessionId” is always exists :(, so I get an error from adfs “to many requests”. Any idea?