Base solution for your next web application
Open Closed

Derivative of ControllerBase returns 302 instead of 401 when token expired #11797


User avatar
0
hra created

I have a similar issue to here: https://github.com/aspnetboilerplate/aspnetboilerplate/issues/5164

However, I need to return IActionResult, because my method will return a FileStreamResult in the case of success, but ObjectResults for detailed errors:

    var result = new ObjectResult(new AjaxResponse(
        ErrorInfoBuilder.BuildForException(vex),
        false
    ))
{
    StatusCode = (int)HttpStatusCode.BadRequest
};
return result;

When the customers mobile application makes a request with an expired token, they get a 302 instead of a 401 - not what the app is expecting.

How do I resolve this?

UPDATE: I have noticed that this is happening on the Azure hosted application, but when running on local machine, it correctly produces a 302. Why does it behave differently in Azure?

**Note: ** Yes I am setting the ajax-request header

      options.headers = {
        ...options.headers, 
        'Authorization': 'Bearer $_token',
        'X-Requested-With': 'XMLHttpRequest'};
        

Another note: If I call methods in the Application services layer, instead of my custom Controllers, then the server produces the correct 401. It's just this 1 method in a Controller which will not return 401 :(

Another Update I have found the cause of the difference between Azure and running locally... The default ANZ code for turning on redirects... of course, I dont want to just go blindly removing ANZ default template code...

if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseStatusCodePagesWithRedirects("~/Error?statusCode={0}");
    app.UseExceptionHandler("/Error");
}

I've also remote debugged app in Azure and stepped into CookieAuthenticationEvents.IsAjaxRequest - and it definitely returns True

public Func<RedirectContext<CookieAuthenticationOptions>, Task> OnRedirectToLogin { get; set; } = context =>
{
    if (IsAjaxRequest(context.Request))
    {
        context.Response.Headers[HeaderNames.Location] = context.RedirectUri;
        context.Response.StatusCode = 401;
    }
    else
    {
        context.Response.Redirect(context.RedirectUri);
    }
    return Task.CompletedTask;
};

Tracking this through to the StatusCodePagesMiddleware, I can see that the response code is 401 (as I want), but then the StatusCodePagesMiddleware overwrites it, returning the 302.

I also debugged an ajax call to a method that resides in the application service layer (not a pure controller) and that looks like during the authentication, the response gets closed early, before the StatusCodePagesMiddleware has a chance to execute (i.e, the client has already received the 401 before the server middleware finishes evaluating, hence the "bad" behavior of the StatusCodePagesMiddleware doesnt matter - it's too late to mess things up).

I've simplified my code all the way back to a very basic method in the controller, declared to return a POCO instead of IActionResult - and it still returns 302 instead of 401. It looks like controllers just have this problem - only the application services correctly return 401 when an auth error occurs. Where do I go from here?


9 Answer(s)
  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi @hra

    Could you share the full definition of your controller action ?

    Thanks,

  • User Avatar
    0
    hra created

    Hi, I get a failure with this example

    
    namespace HRA.Portal.Web.Controllers
    {
        [AbpMvcAuthorize]
        [Route("Sync/[action]")]
        [DisableAuditing]
        public class SyncController : PortalControllerBase
        {
            public SyncController()
            {
            }
    
            [HttpPost]
            public Task&lt;MyResult&gt; UploadFile()
            {
                return Task.FromResult(new MyResult());
            }
        }
    
        public class MyResult
        {
    
        }
    }
    
  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi,

    I think it is handled here https://github.com/aspnetboilerplate/aspnetboilerplate/blob/dev/src/Abp.AspNetCore/AspNetCore/Mvc/Authorization/AbpAuthorizationFilter.cs#L37. You can remove this filter and create a similar one to change it as you wish.

  • User Avatar
    0
    hra created

    Hi @ismcagdas,

    The authorization filter never fires if I'm calling a ControllerBase method with an expired token - neither my customised one you recommended, nor the built-in ABP one - which is not surprising, because this specifically relates to an expired token that is being passed - so the MS auth middleware would be rejecting the request pretty early.

    Calling an ApplicationService function with an expired token DOES execute the authorization filter however.

    So, clearly the logic of the Authorization Filter is not the cause, because it never gets a chance to run.

    When I disable the below code - the auth filter does fire, and I get the correct result back - but I already knew that. The "UseStatusCodePages" middleware appears to be the cause - but I didnt add that - it's part of ANZ.

                app.UseStatusCodePagesWithRedirects("~/Error?statusCode={0}");
    

    So, a couple of core questions

    1. I am expecting that an expired token should trigger the same HTTP result code (401) if the client is calling a ControllerBase method, or an ApplicationService method. Currently that is not the case. Should I be expecting that?
    2. Assuming it IS a bug - where to from here. Clearly, the authorizationfilter log is good, because when it runs I get the correct result, when it never fires I get a bad result.

    Thanks!

  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi,

    Does it make any difference if you use AbpAuthorize instead of AbpMvcAuthorize ?

  • User Avatar
    0
    hra created

    Hi,

    Does it make any difference if you use AbpAuthorize instead of AbpMvcAuthorize ?

    No difference. Incidentally, I did originally use AbpAuthorize, I only recently switched it to AbpMvcAuthorize while I was trying to overcome this issue.

    What is your answer to

    1. I am expecting that an expired token should trigger the same HTTP result code (401) if the client is calling a ControllerBase method, or an ApplicationService method. Currently that is not the case. Should I be expecting that?
  • User Avatar
    0
    ismcagdas created
    Support Team

    Theoritically it is correct but I'm not %100 sure at the moment since you are returining IActionResult. I will test this and get back to you.

  • User Avatar
    0
    hra created

    Thanks @ismcagdas,

    Just to be clear - please note that I changed my example to return a POCO instead of IActionResult - just to prove it actually makes no difference what the return type is - the problem remains.

    Thanks for helping out here. What I am concerned about, is that while I've been building our product on ANZ, I have inadvertently caused the authentication behavior to deviate between ControllerBase and ApplicationServer. If you find that baseline ANZ correctly returns HTTP 402 for both ControllerBase and ApplicationService, then I'm going to need to figure out how I've broken it. If, however, you find that ControllerBase fails to return the same HTTP error code as an ApplicationService, when you have an invalid token - then I guess we can work together to fix both our code bases.

    Of course, I'm quietly rooting for the latter :)

    Note: A real easy way to test this without mucking around with tokens, is in ProductJwtSecurityTokenHandler.cs on line 45, simply throw exception.

  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi @hra

    Thanks for the additional info. I will also test with a POCO return type as well.