Skip to content

Instantly share code, notes, and snippets.

@josheinstein
Created February 11, 2014 16:19
Show Gist options
  • Save josheinstein/8938122 to your computer and use it in GitHub Desktop.
Save josheinstein/8938122 to your computer and use it in GitHub Desktop.
When your ASP.NET MVC login session depends on session state, annotating your controller with this attribute will ensure that the required session state is present. Otherwise it will attempt to re-load it or log the user out if it cannot be reloaded.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using Microsoft.Owin.Security;
namespace CustomerPortal.Web.Authentication
{
/// <summary>
/// Ensures that if the request is authenticated (the user is logged in) that the
/// session state contains an object with a given key. If not, the user object will
/// be re-loaded and stored in session state if possible. If not, the user will be
/// redirected to the login page.
/// </summary>
public class VerifySessionStateAttribute : ActionFilterAttribute
{
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="T:VerifySessionStateAttribute" /> class.
/// </summary>
/// <param name="loginUrl">The login URL.</param>
/// <param name="sessionStateKey">The session state key.</param>
public VerifySessionStateAttribute( string loginUrl, string sessionStateKey )
{
Args.ThrowIfNull( loginUrl, "loginUrl" );
Args.ThrowIfNullOrEmpty( sessionStateKey, "sessionStateKey" );
LoginUrl = loginUrl;
SessionStateKey = sessionStateKey;
}
#endregion
#region Properties
/// <summary>
/// Gets the URL to redirect the user to if the session is reset before the
/// login cookie times out. A ReturnUrl query string parameter will be appended
/// to this in the case of an aborted GET request.
/// </summary>
public string LoginUrl
{
get;
private set;
}
/// <summary>
/// The key of an object to check for in the session state. If the value
/// with this key is null, the user will be logged out.
/// </summary>
public string SessionStateKey
{
get;
private set;
}
#endregion
/// <summary>
/// Called by the ASP.NET MVC framework before the action method executes.
/// </summary>
/// <param name="filterContext">The filter context.</param>
public override void OnActionExecuting( ActionExecutingContext filterContext )
{
var httpContext = filterContext.HttpContext;
var owinContext = httpContext.GetOwinContext( );
var authManager = owinContext.Authentication;
var request = httpContext.Request;
var response = httpContext.Response;
var session = httpContext.Session;
// When the user's session expires before their authentication cookie,
// the session state will lack the all-important InteleBillUser object.
// We need to detect this condition because ASP.NET considers them
// "logged in" but without this object, there is not much we can do.
if ( request.IsAuthenticated && session != null ) {
// The request is authenticated - which means there is a valid
// authentication cookie in the request. However, this does not
// mean that we have the user profile information loaded in session
// state. If not, we need to retrieve it again because it is
// assumed to be available for authenticated sessions.
// Is the session key empty?
// Try to fill it.
if ( session[SessionStateKey] == null ) {
session[SessionStateKey] = LoadUserProfile( httpContext.User.Identity.Name );
}
// Is the session key still empty?
if ( session[SessionStateKey] == null ) {
// User profile for the authenticated user could not be loaded.
// This could mean the user no longer exists. Redirect to the
// login page so that they can sort it out from there.
authManager.SignOut( );
session.Abandon( );
if ( request.IsAjaxRequest( ) ) {
// No sense in redirecting an AJAX request to the login page
// instead we will return a HTTP 401 result.
filterContext.Result = new HttpStatusCodeResult( 401, "Authentication required." );
}
else {
if ( request.HttpMethod == "GET" ) {
// This will result in them being redirected to the
// login page with the ReturnUrl parameter set to the
// url they were trying to access.
filterContext.Result = new RedirectResult( LoginUrl + "?returnUrl=" + request.RawUrl );
}
else {
// If the request was not a GET request, then we can't
// assume they can be redirected back to the same URL
// so we will send them to the home page.
filterContext.Result = new RedirectResult( LoginUrl );
}
}
}
}
}
/// <summary>
/// Synchronously loads the <see cref="T:InteleBillUser"/> object for the user with the given
/// user name (billing account number).
/// </summary>
/// <remarks>
/// Note that the password is not checked again at this point since the valid authentication
/// cookie assumes that the user was previously logged in successfully.
/// </remarks>
/// <param name="userName">The user's login name (billing account number).</param>
/// <returns>The InteleBillUser for the given login or null if the user could not be loaded.</returns>
private static InteleBillUser LoadUserProfile( string userName )
{
// REPLACE THIS METHOD WITH WHATEVER YOU NEED TO LOAD INTO
// SESSION STATE!
try {
// Important: This MUST be called in a separate task, because ASP.NET MVC
// does not currently support async action filters. The InteleBillUserManager
// API only supports the async/await model. If we attempt to call this
// API synchronously, it will result in a deadlock.
var task = Task.Factory.StartNew( ( ) => {
using ( var userManager = new InteleBillUserManager( ) ) {
return userManager.FindByNameAsync( userName ).Result;
}
}, TaskCreationOptions.LongRunning );
return task.Result; // blocks on the above task
}
catch ( Exception ) {
// An error occurred while loading the user from the database.
// By returning null, it will cause the user to be redirected
// to the login page.
return null;
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment