ASP.NET MVC–Prevent an action method from being called before another action


Problem

You want to restrict access by callers to an action method unless another action method was called first.

Solution

There is already a similar functionality implemented by the Authorize attribute. The Authorize attribute is an authorization filter restricts the access to an action method to users who are both authenticated and authorized. If the user is not authenticated and authorized the Authorized attribute will set a HTTP status code of 401 Unauthorized to the response. This will get picked up by the Forms Authentication HTTP Module who will perform a redirection to the log in action. The Authorize attribute extends the FilterAttribute abstract class and implements the IAuthorizationFilter interface and so will our filter (let’s call it ActionInitializationRequired). With some help from the MVC3 source code we will have the following template to start with:

[AttributeUsage( AttributeTargets.Method , Inherited = true , AllowMultiple = false )]
public class ActionInitializationRequiredAttribute : FilterAttribute , IAuthorizationFilter
{
    protected virtual bool AuthorizeCore( HttpContextBase httpContext )
    {
        if ( httpContext == null )
        {
            throw new ArgumentNullException( "httpContext" );
        }

        //TODO: Add code to check that another action was called first

        return true;
    }

    private void CacheValidateHandler(
        HttpContext context ,
        object data ,
        ref HttpValidationStatus validationStatus
        )
    {
        validationStatus = OnCacheAuthorization( new HttpContextWrapper( context ) );
    }

    public virtual void OnAuthorization( AuthorizationContext filterContext )
    {
        if ( filterContext == null )
        {
            throw new ArgumentNullException( "filterContext" );
        }

        if ( OutputCacheAttribute.IsChildActionCacheActive( filterContext ) )
        {
            throw new InvalidOperationException(
                "ActionInitializationRequiredAttribute cannot be used within a child action caching block."
            );
        }

        if ( AuthorizeCore( filterContext.HttpContext ) )
        {
            HttpCachePolicyBase cachePolicy = filterContext.HttpContext.Response.Cache;
            cachePolicy.SetProxyMaxAge( new TimeSpan( 0 ) );
            cachePolicy.AddValidationCallback( CacheValidateHandler , null /* data */);
        }
        else
        {
            HandleUnauthorizedRequest( filterContext );
        }
    }

    protected virtual void HandleUnauthorizedRequest( AuthorizationContext filterContext )
    {
        //TODO: Add code to respond to the case when the initialization action wasn't called
    }

    protected virtual HttpValidationStatus OnCacheAuthorization( HttpContextBase httpContext )
    {
        if ( httpContext == null )
        {
            throw new ArgumentNullException( "httpContext" );
        }

        bool isAuthorized = AuthorizeCore( httpContext );
        return ( isAuthorized ) ? HttpValidationStatus.Valid : HttpValidationStatus.IgnoreThisRequest;
    }
}

We have 2 TODO lines:

  • Add code to check that another action was called first
  • Add code to respond to the case when the initialization action wasn't called

Let’s solve them one by one:

1. Add code to check that another action was called first

The first thing we need to do is find a way to know when an action was called. For this we can add some code in the action itself or use a more AOP approach by creating an Action Filter and overriding the OnActionExecuted method (this method is Called by the MVC framework after the action method executes). Let’s call this action filter ActionInitializer:

[AttributeUsage( AttributeTargets.Method , Inherited = true , AllowMultiple = false )]
public class ActionInitializerAttribute : ActionFilterAttribute
{
    public override void  OnActionExecuted( ActionExecutedContext filterContext )
    {
        var area = filterContext.RouteData.DataTokens[ "area" ] ?? string.Empty;
        string action = filterContext.ActionDescriptor.ActionName;
        string controller = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName;

        string actionInitializerKey = 
            string.Format( "{0}/{1}/{2}" , area.ToString( ) , controller , action );

        filterContext.HttpContext.Session.Add( actionInitializerKey , actionInitializerKey );

        base.OnActionExecuted( filterContext );
    }
}

This filter does a very simple thing. It gets the name of area, the name of the controller and the name of the action it is applied on and save them in Session in a area/controller/action format. This is a very basic way of stating that the action was executed. Let’s get back to the TODO number one above

In order to know what to look up in the Session object we need a way to tell the ActionInitializationRequired filter what action is responsible for initialization. For this we will the following properties:

private string _area;
private string _controller;
private string _action;

public string Area
{
    get
    {
        return _area ?? String.Empty;
    }
    set
    {
        _area = value;
    }
}

public string Controller
{
    get
    {
        return _controller ?? "Home";
    }
    set
    {
        _controller = value;
    }
}

public string Action
{
    get
    {
        return _action ?? "Index";
    }
    set
    {
        _action = value;
    }
}

private string ActionKey
{
    get
    {
        return string.Format( "{0}/{1}/{2}" , this.Area , this.Controller , this.Action );
    }
}

these properties (with the exception of the ActionKey property that is just a helper) will become Named Parameters for our authorization filter. Now we have everything we need to replace the first TODO line:

if ( httpContext.Session[ this.ActionKey ] == null )
{
    return false;
}

2. Add code to respond to the case when the initialization action wasn't called

For this we will follow a similar approach like the Authorize attribute and return a HTTP status code but instead of 401 Unauthorized we will return 403 Forbidden (we can also return a custom code and build a HTTP module that knows how to handle responses with that particular code, just like the FormsAuthentication module does but we will keep this simple). Before returning 403 let’s take a little detour and build a new ActionResult (or a new HttpStatusCodeResult because we will extend this class instead of ActionResult):

public class HttpForbiddenResult : HttpStatusCodeResult
{
    // HTTP 403 is the status code for Forbidden
    private const int ForbiddenCode = 403;

    public HttpForbiddenResult( )
        : this( null ) { }

    public HttpForbiddenResult( string statusDescription )
        : base( ForbiddenCode , statusDescription ) { }
}

This is not needed because we can use the HttpStatusCodeResult directly but it makes our code to look good. The replacement for the second TODO line is:

// Returns HTTP 403 
filterContext.Result = new HttpForbiddenResult( );

and this is it. Let’s assume the following:

  • We have a controller named Home in the root area
  • We have an action called Initializer to which we will apply the ActionInitializer attribute
  • We have an action called RequiresInitialization to which we will apply the ActionInitializationRequired attribute

This is how we use the attributes:

public class HomeController : Controller
{
    public ActionResult Index( )
    {
        return View( );
    }

    [ActionInitializer]
    public ActionResult Initializer( )
    {
        TempData[ "InitCalled" ] = "Action initializer called !!!";

        return RedirectToAction( "Index" );
    }

    [ActionInitializationRequired( Action = "Initializer" )]
    public ActionResult RequiresInitialization( )
    {
        return View( );
    }
}

The Index view looks like this:

@{
    ViewBag.Title = "Index";
}

<h2>Index</h2>

@Html.ActionLink( "Call Action Initializer" , "Initializer" ) | 
@Html.ActionLink( "Call Action that requires initialization" , "RequiresInitialization" )

<br />

@if ( TempData[ "InitCalled" ] != null )
{ 
    <span>@TempData[ "InitCalled" ].ToString( )</span>
}

How it works

The inner workings are pretty simple:

  • An action filter add a key into the Session object after the action has executed.
  • An authorization filter checks the Session for that particular key and allows the execution of the action is applied on. If the key is not found then a 403 Forbidden HTTP status code is returned.

With the actual implementation we can only perform a one time initialization, that is if we call the initialization action we can call all the action that require initialization for the life time of the session. With a small modification we can have both one time and every time initializations (we cannot call an action that requires authorization more than once. If another call is needed the initialization action needs to be called again) but this is a subject for another post.

See it in action

Action Initializer Demo

References

Original post about the subject | Authorize Attribute | MVC3 Source Code | Aspect-oriented programming (AOP)


No Comments