Custom AuthorizeAttribute with custom authentication
Asked Answered
N

3

11

I am using ASP.NET MVC 4 Web application as a front-end for some WCF services. All the user log in/log out and session control is done on the back-end. MVC app should only store a single cookie with session ID. My client does not allow to use Forms Authentication, everything must be customized.

I have set up the following in my web.config:

  <system.web>
...
    <authentication mode="None" />
  </system.web>

  <system.webServer>
    <modules>
...
      <remove name="FormsAuthentication" />
...    
    </modules>
  </system.webServer>

I have also a global filter:

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        // Force all actions to request auth. Only actions marked with [AllowAnonymous] will be allowed.
        filters.Add(new MyAuthorizeAttribute());
    }
}

which is called in Global.asax

   FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);

I have marked with [AllowAnonymous] every controller and action which does not need authorization.

And now I have to implement MyAuthorizeAttribute. I have tried some tutorials, but none of them completely match my scenarios.

Basically, I have to handle the following scenarios for each action:

  1. If there is a valid cookie, the current request should be considered authorized (there will be no any roles to check, only one kind of users).
  2. If there is no cookie, I should override the default MVC handler (which tries to load Account/Login) and redirect users to Home/Index page with a message that the user should log in.
  3. If the WCF method call throws FaultException where our custom SecurityFault says that session has expired (SecurityFault has a custom enum field which contains the reason of exception), I should destroy my custom session cookie and again redirect the user to Home/Index page with a message that the user should log in because his last session has expired. For all the other SecurityFaults I can let them go through - I have a global error handler.

As far as I understand, I need to override AuthorizeCore (to check my cookie to see if the session exists and is still valid) and HandleUnauthorizedRequest (to redirect users to Home/Index instead of default Login page).

For redirection I tried:

    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {            
        base.HandleUnauthorizedRequest(filterContext);
        filterContext.Result = new RedirectResult("/Home/Index/NeedsLogin");
    }

which seems to handle the scenario 2nd fine (I'm not sure about that base call, though - is it needed?).

For the 1st scenario, I need to implement AuthorizeCore. I'm not sure, how to do it correctly. I have seen that AuthorizeAttribute has some code for handling caching situations and maybe many more hidden functionality and I don't want to break it.

For the 3rd scenario, I am not sure if MyAuthorizeAttribute will be able to handle it. Can AuthorizeAttribute catch exceptions which occur inside of the Action or I'll have to handle SecurityFault.SessionExpired situations in my global error handler?

Narine answered 1/10, 2012 at 11:23 Comment(0)
C
8

Not totally sure I get it but if you create an Custom Authorization Filter that inherits from System.Web.MVC.Authorize attribute like this.

    public class CustomAuthorize : AuthorizeAttribute
    {
    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        if (CookieIsValid(filterContext.Request.Cookies["cookieyouwant"])
        {
             filterContext.Result = new RedirectResult("DestUrl");
        }
        else
        {
            filterContext.Result = new RedirectResult("/Home/Index/NeedsLogin");
        }
    }
}

And then decorate your Methods that need to employ this Authorization will that do the trick?

Cannibalism answered 1/10, 2012 at 21:49 Comment(1)
Thanks, I just looked into the MVC 4 Source and now I see that it's not that complicated as I thought, AuthorizeAttribute has just a bunch of lines of code, so I was able to put my own parts where I needed. I was concerned about caching issues, so I just copied the related code from default AuthorizeAttribute.Narine
N
4

Here is how I did it for now:

  public class MyAuthorizeAttribute : AuthorizeAttribute
    {
        public override void OnAuthorization(AuthorizationContext filterContext)
        {
            bool authorized = false;

            /// MVC 4 boilerplate code follows
            if (filterContext == null)
                throw new ArgumentNullException("filterContext");

            bool skipAuthorization = filterContext.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), inherit: true)
                          || filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), inherit: true);

            if (skipAuthorization)
            {
                return;
            }

            if (OutputCacheAttribute.IsChildActionCacheActive(filterContext))
            {
                throw new InvalidOperationException(
                    "MyAuthorizeAttribute cannot be used within a child action caching block."
                );
            }
            // end of MVC code


            // custom code
            if (!AuthorizeCore(filterContext.HttpContext))
            {
                // if not authorized from some other Action call, let's try extracting user data from custom encrypted cookie
                var identity = MyEncryptedCookieHelper.GetFrontendIdentity(filterContext.HttpContext.Request);
                // identity might be null if cookie not received
                if (identity == null)
                {
                    filterContext.HttpContext.User = new GenericPrincipal(new GenericIdentity(""), null);
                }
                else
                {
                    authorized = true;
                    filterContext.HttpContext.User = new MyFrontendPrincipal(identity);
                }

                // make sure the Principal's are in sync - there might be situations when they are not!
                Thread.CurrentPrincipal = filterContext.HttpContext.User;
            }

            // MVC 4 boilerplate code follows
            if (authorized)
            {
                // ** IMPORTANT **
                // Since we're performing authorization at the action level, the authorization code runs
                // after the output caching module. In the worst case this could allow an authorized user
                // to cause the page to be cached, then an unauthorized user would later be served the
                // cached page. We work around this by telling proxies not to cache the sensitive page,
                // then we hook our custom authorization code into the caching mechanism so that we have
                // the final say on whether a page should be served from the cache.

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

        protected override bool AuthorizeCore(HttpContextBase httpContext)
        {
            if (httpContext == null)
                throw new ArgumentNullException("httpContext");

            // check to make sure the user is authenticated as my custom identity
            var principal = httpContext.User as MyFrontendPrincipal;
            if (principal == null)
                return false;

            var identity = principal.Identity as MyFrontendIdentity;
            if (identity == null)
                return false;

            return true;
        }

        protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
        {            
            // default MVC result was:
            // filterContext.Result = new HttpUnauthorizedResult();

            // but I redirect to index login page instead of kicking 401
            filterContext.Result = new RedirectResult("/Home/Index/NeedsLogin");
        }

        // MVC 4 boilerplate code follows
        private void CacheValidateHandler(HttpContext context, object data, ref HttpValidationStatus validationStatus)
        {
            validationStatus = OnCacheAuthorization(new HttpContextWrapper(context));
        }

        // This method must be thread-safe since it is called by the caching module.
        protected virtual HttpValidationStatus OnCacheAuthorization(HttpContextBase httpContext)
        {
            if (httpContext == null)
                throw new ArgumentNullException("httpContext");

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

It does not handle my 3rd scenario, though, so I'll implement it in a global error handler.

Narine answered 2/10, 2012 at 8:59 Comment(0)
W
4

Regarding your first requirement:

As you already found out, OnAuthorization takes care of a number of aspects, including e.g. caching.
If you are only interested in customizing the way in which user credentials are validated, I suggest you go for overriding AuthorizeCore instead. E.g.:

public class ClientCookieAuthorizeAttribute : AuthorizeAttribute
{
    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        HttpCookie cookie = httpContext.Request.Cookies[_tokenCookieName];

        bool isAuthenticated = ValidateUserByCookie(cookie);

        return isAuthenticated;
    }

    private bool ValidateUserByCookie(HttpCookie cookie)
    {
        var result = false;
        // Perform validation
        // You could include httpContext as well, to check further information
        return result;
    }

    private static const string _tokenCookieName = "myCookieName";
}

You might also want to give a look at this other threads:

  1. SO - Custom Authorize Attribute
  2. ASP.NET - Custom AuthorizationFilter redirect problems
  3. Diary of a ninja
Wobbly answered 13/3, 2013 at 20:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.