Ajax Fails On Authorization

2.3k views Asked by At

I am building MVC web application that for at least part of its data transfer relies on Ajax.

The controller action is

[RBAC]
[Authorize]
public string GetData(string inputdata)
{
   some code ...
   return jsondata;
}

The ajax call is

 $.ajax({
       dataType: "json",
       url: Url,
       data: { '_inputdata': selectedText },
       success: function (data)
       {
           response($.map(data,
              function(item, index) {
              return {
                   label: item.label,
                   value: item.value
               }
            }));
       },
      error: (function (jqXHR, textStatus, errorThrown, data) {
           ProcessFail(jqXHR, textStatus, errorThrown,  data);
        });
      })
  }); 

[RBAC] causes an authorization check to be done which is what I want.

    public override void OnAuthorization(AuthorizationContext filterContext)
    {
      ......
         filterContext.Result = new RedirectToRouteResult
              (new RouteValueDictionary { { "action", "Index" }, 
              { "controller", "Unauthorised" } , 
              { "Area", String.Empty }});
       .....
    } 

The problem is that I don't get anything back at the ajax except a failure. There is nothing that tells me that there was an authorization error.

Questions:

  1. Is it possible to get back information from an authorization failure into the ajax response. If so how?
  2. If the answer to 1. is no, should I be checking for this authorization before I make this call?

As always, any help appreciated.

3

There are 3 answers

0
BrownPony On BEST ANSWER

This is a complete solution that allows you to essentially decorate your actions with a single call inside your action that works the same way the standard Forms based authentication in ASP.net.

Just copy the pc's here and it should work.

That problem is that the authorization code that is implemented by decorating the action does not send back an authorization error to the Ajax.

So

[Authorize] or in my case [RBAC]
public string SomeActionCalledByAjax( some args)
{
   some stuf
}

Fails with no error message to the user.

Here is the solution I implemented. It actually uses the OnAuthorization.
My goal was to get a simple solution that allowed me to decorate the actions almost like the factory authorization code. I have succeed in this.

Credit to

How do I get the MethodInfo of an action, given action, controller and area names? credit to Miguel Angelo.

and

jQuery Ajax error handling, show custom exception messages

Credit AlexMAS

never would have figured this out if it was not for these guys.

I am using RBAC for security. Find it here. https://www.codeproject.com/articles/1079552/custom-roles-based-access-control-rbac-in-asp-ne

Excellent role based security. good system. It extends the Forms based authentication via ASP.NET Identity's framework.

So this would have been simple if you could see IPrincipal.User outside of the controller but I found I could not pass it to a method in the controller and still see the extensions that were used for RBAC that get the permissions in that method.

But you could see it here.

public class RBACAttribute:AuthorizeAttribute
{
   public override void OnAuthorization(AuthorizationContext filterContext)
   {
      do stuff.
   }
}

So the trick becomes how to get an AuthorizationContext filterContext filled properly and then I can call OnAuthorize.

This is where Miguel's code come in. It is an extension to the controller. I changed it slightly because it will actually get all of its information from the controller reference that's passed in. I only want the ActionDescriptor so I can fill a AuthorizationContext object

public static class GetControllerAttr
    {
        public static ActionDescriptor GetActionAttributes(this Controller @this,string action,string controller,string area,string method)

        {
           var actionName = action ?? @this.RouteData.GetRequiredString("action");
            var controllerName = controller ?? @this.RouteData.GetRequiredString("controller");
            var areaName = area ?? @this.RouteData.Values [ "area" ] ;
            var methodName = method  ?? @this.RouteData.GetRequiredString("action");
            var controllerFactory = ControllerBuilder.Current.GetControllerFactory();

            var controllerContext = @this.ControllerContext;

            var otherController = (ControllerBase)controllerFactory
                .CreateController(
                    new RequestContext(controllerContext.HttpContext,new RouteData()),
                    controllerName);

            var controllerDescriptor = new ReflectedControllerDescriptor(
                otherController.GetType());

            var controllerContext2 = new ControllerContext(
                new MockHttpContextWrapper(
                    controllerContext.HttpContext.ApplicationInstance.Context,
                    methodName),
                new RouteData(),
                otherController);

            var actionDescriptor = controllerDescriptor
                .FindAction(controllerContext2,actionName);

            return actionDescriptor ;
            //var attributes = actionDescriptor.GetCustomAttributes(true)
            //    .Cast<Attribute>()
            //    .ToArray();

            //return attributes;
        }
    }
    class MockHttpContextWrapper:HttpContextWrapper
    {
        public MockHttpContextWrapper(HttpContext httpContext,string method)
            : base(httpContext)
        {
            this.request = new MockHttpRequestWrapper(httpContext.Request,method);
        }

        private readonly HttpRequestBase request;
        public override HttpRequestBase Request
        {
            get { return request; }
        }

        class MockHttpRequestWrapper:HttpRequestWrapper
        {
            public MockHttpRequestWrapper(HttpRequest httpRequest,string httpMethod)
                : base(httpRequest)
            {
                this.httpMethod = httpMethod;
            }

            private readonly string httpMethod;
            public override string HttpMethod
            {
                get { return httpMethod; }
            }
        }
    }

I took Alex's code modified it slightly to get the information I wanted to send back to the JQuery

  public class ClientErrorHandler:FilterAttribute, IExceptionFilter
    {
        public void OnException(ExceptionContext filterContext)
        {
            var response = filterContext.RequestContext.HttpContext.Response;

            clsAuthorizationError _clsAuthorization = new clsAuthorizationError();
            if(filterContext.Exception.Data.Contains("ErrorCode"))
            {
                _clsAuthorization.ErrorCode = (int)filterContext.Exception.Data["ErrorCode"];
                _clsAuthorization.ReDirect = filterContext.Exception.Message;
                string _results = JsonConvert.SerializeObject(_clsAuthorization);
                response.Write(_results);

            }
            else
            {
                response.Write(filterContext.Exception.Message);
            }

            response.ContentType = MediaTypeNames.Text.Plain;


            filterContext.ExceptionHandled = true;

        }
    }
    public class clsAuthorizationError
    {
        public int ErrorCode { set; get; }
        public string ReDirect { set; get; }
    }

In the overridden OnAuthorization method I added the Url string and the error code.

   public class RBACAttribute:AuthorizeAttribute
    {
      public string Url { set; get; }
      public int ErrorCode { set; get; }
      public override void OnAuthorization(AuthorizationContext filterContext)
      {
          Url = null;

          string _action = null;
          string _controller = null;
          try
          {
           if(!filterContext.HttpContext.Request.IsAuthenticated)
           {
              //Redirect user to login page if not yet authenticated.  This is a protected resource!

             filterContext.Result = new RedirectToRouteResult(new 
                   RouteValueDictionary { { "action",_action },
                                  { "controller",_controller },
                                  { "Area",String.Empty } });
               Url =   "/__controller__/__action__/";
               Url = Url.Replace("__controller__",_controller);
               Url = Url.Replace("__action__",_action);
                ErrorCode = 401;
            }
            else
            {
             //Create permission string based on the requested controller name and action name in the format 'controllername-action'
                string requiredPermission = String.Format("0}-{1}",
                filterContext.ActionDescriptor.ControllerDescriptor.ControllerName,
                   filterContext.ActionDescriptor.ActionName);
              if(!filterContext.HttpContext.User.HasPermission(requiredPermission) & !filterContext.HttpContext.User.IsSysAdmin())
{
    _action = "Index";
    _controller = "Unauthorised";
     //User doesn't have the required permission and is not a SysAdmin, return our custom “401 Unauthorized” access error
     //Since we are setting filterContext.Result to contain an ActionResult page, the controller's action will not be run.
    //The custom “401 Unauthorized” access error will be returned to the browser in response to the initial request.
     filterContext.Result = new RedirectToRouteResult(
                   new RouteValueDictionary { { "action",_action },
                          { "controller",_controller },
                              { "Area",String.Empty } });
             Url =   "/__controller__/__action__/";
             Url = Url.Replace("__controller__",_controller);
             Url = Url.Replace("__action__",_action);
             ErrorCode = 401;
             }
   //If the user has the permission to run the controller's action, the filterContext.Result will be uninitialized and
   //executing the controller's action is dependant on whether filterContext.Result is uninitialized.
           }
         }
          catch(Exception ex)
          {
              _action ="Error";
              _controller = "Unauthorised";
              Url =   "/__controller__/__action__/";
              Url = Url.Replace("__controller__",_controller);
              Url = Url.Replace("__action__",_action);
              filterContext.Result = new RedirectToRouteResult(
                       new RouteValueDictionary(
                       new { controller = _controller,action = _action,
                          _errorMsg = ex.Message })
              ErrorCode = 500;

                }
              }
            }

In the Ajax call add the following.

complete: function (jqXHR, textStatus, errorThrown)
{               
   var jparse = JSON.parse(jqXHR.responseText);
   if (jparse.hasOwnProperty('ErrorCode'))
   {            
        var code = jparse.ErrorCode;
        var href = jparse.ReDirect;
         window.location.href = href;
   }
}

Then I created a front end to put together the pc's

public class clsOnAuthorization {

            //private string Redirect { set; get; }

            //string _Action { set; get; }
            //string _Controller { set; get; }
            //string _url { set; get; }
            AuthorizationContext _filterContext;

            public clsOnAuthorization(Controller @this)
            {         
                _filterContext = new AuthorizationContext(@this.ControllerContext,GetControllerAttr.GetActionAttributes(@this,null,null,null,null));

                Verify ( ) ;

            }
            public void Verify()
            {
                RBACAttribute _rbacAttribute = new RBACAttribute();
                _rbacAttribute.OnAuthorization(_filterContext);
                if(_rbacAttribute.Url != null)
                {
                    Exception myEx = new Exception(_rbacAttribute.Url);
                    myEx.Data.Add("ErrorCode", _rbacAttribute.ErrorCode); 

                    throw myEx;
                }
            }     
        }

Finally I decorate the action and make one call in the action.

[ClientErrorHandler]
public string JobGuid()
{
     // send controller in with constructor.
   clsOnAuthorization _clsOnAuthorization = new clsOnAuthorization(this);
    //  if authorization fails it raises and exception and never comes back here. 
    some stuff if authorization good.
} 

With one decoration and a single class instantiation all of the authorization problems went away and my ajax calls now know what went wrong and can redirect appropriately.

1
Zach On

Looks like you are using MVC rather than Web API, Web API should give you a nice JSON message by default.

One option would be to check the status code of the response, this should give you a 401 if it is an authentication failure.

Another would be to remove the [Authorize] and do a check inside of the method itself

public string GetData(string inputdata)
{
   if (User.Identity.IsAuthenticated) { 
      return  jsonData;
   } 
   return failureJson;
}

Note: I am sure there is a fancier way to do this but this should work

4
Rajput On

Use another parameter complete: just like success: and error: for checking authorization failure in your $.ajax() call. After success implement this piece of code. here 401 code is showing authorization error. check of if condition.

success: function (data)
       {
           response($.map(data,
              function(item, index) {
              return {
                   label: item.label,
                   value: item.value
               }
            }));
       },
       complete: function(jqXHR){
                 if (jqXHR.status== '401'){
                     **//Your code here whatever you want to do**
                    }
}

complete: is returned whenever your ajax call is completed.