Custom AuthorizeAttribute with custom authentication

16,849

Solution 1

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?

Solution 2

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.

Solution 3

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
Share:
16,849
JustAMartin
Author by

JustAMartin

I'm just another programmer. Life forces me to behave like a full-stack developer, but I prefer to do the backend stuff more. And even more I'd prefer doing some uncommon stuff, such as developing low-level network, audio and wireless systems. Unfortunately, there are very few choices for such projects nearby and I have my bills to pay, so I mostly just go with the flow. BTW, I'm handicapped, I have vision issues since birth. It's serious enough to never being allowed to drive a vehicle. While I still can write some code I'm good, yay. Ok, you got me, I filled this section to get the StackOverflow Autobiographer badge. Are you happy now?

Updated on July 19, 2022

Comments

  • JustAMartin
    JustAMartin almost 2 years

    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?

  • JustAMartin
    JustAMartin over 11 years
    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.