Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom AuthorizeAttribute with custom authentication

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?

like image 282
JustAMartin Avatar asked Oct 01 '12 11:10

JustAMartin


People also ask

How do I register a custom authorization filter in .NET core?

Right-click on the solution and add a new class. Enter the class name and click on Add. Next Inherite Attribute, IAuthorizationFilter to CustomAuthorization class which has overridden the OnAuthorization method. The OnAuthorization Method has the AuthorizationFilterContext parameter.

How do I create a custom authentication filter in Web API?

Setting an Authentication Filter To apply an authentication filter to a controller, decorate the controller class with the filter attribute. The following code sets the [IdentityBasicAuthentication] filter on a controller class, which enables Basic Authentication for all of the controller's actions.


2 Answers

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.

like image 24
JustAMartin Avatar answered Oct 02 '22 20:10

JustAMartin


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?

like image 134
Jive Boogie Avatar answered Oct 02 '22 19:10

Jive Boogie