Redirect when cookie timeout

Hi,
Is there a way for me to redirect when a user either deletes their cookie or gets timed out?
When trying to do actions such as Commands and Queries the frontend will receive status 302 code and CORS problems. As soon as I refresh the page I will get a new cookie and all of my queries and commands will work perfectly fine again.

A way of handling this could be in the frontend but I feel this is a wrong approach. The right way is to let the backend handle these types of situation.

I will also upload some of the error messages I’m getting.

(Obviously I have marked out some of the redirect urls since it’s not relevant)

02
As you can see here i’m getting 302 of all requests that i’m creating.

2 Likes

34
Was not able to upload two images in a post, So here’s the other error message i’m getting.

Thanks for the question @Khalid.

The Coordinators are using the Fetch API underneath, which has the default behaviour of following redirects returned, this is probably failing because of the cors header not being set correctly when it’s attempting to do the redirect.

The Coordinators have a convenience callback (that isn’t yet documented) where you can override any headers on the request. These callbacks can be set up during the startup of your application and will be evaluated before every command or query.

    // call these during the setup / bootstrapping process of your app. 
    CommandCoordinator.beforeHandle((options) => modifyHeaders(options));
    QueryCoordinator.beforeExecute((options) => modifyHeaders(options));

    //run before every request for commands or queries
    function modifyHeaders(options) { //options passed into the fetch call 
       
        options.headers = {...}; //modify headers before each request
    }

@michael, could you chime in regarding setting up CORS policy on the backend? Are there any convenience methods for this in place when ex using Sentry, or would one need to manually set up a policy?

2 Likes

There’s nothing specific about configuring CORS beyond what is provided by AspNetCore, but that should be sufficient.

With AspNetCore, the order in which you configure things is important. If I remember correctly, you have to configure CORS before you configure MVC.

What is needed is to allow the origins for the Auth0 endpoint. Remember it has to be an exact match on schema/host/port or it is considered another origin.

What CORS does is to set the Access-Control-Allow-Origin header. It is this that the browser will look for when determining whether to allow a request to another server. You also have to specifically allow the sending of credentials, but that doesn’t apply here.

It seems to me that everything is working as intended and it’s only the CORS issue that needs to be resolved. The server is configured to challenge requests when the credentials are missing / invalid and return a redirect to the authority. This is what is happening. It’s up to the client to honour that redirect. It seems that the client is attempting the redirect (which is nice out of the box! :-)) but it is the browser that is blocking it.

I’m guessing if you configure CORS to allow the authority origin then it will work.

2 Likes

Actually, there might still be a problem here. If it’s not a CORS request, then AspNetCore CORS middleware won’t include the CORS headers so it won’t work.

What we might have to do is detect whether the request is an api call, and if it is, return a 401 instead. There’s an OnRedirectToIdentityProvider event that we can use.

I’ll try this out and get back to you.

Thanks Michael,
I followed your solution yesterday trying to fix the CORS problem. I was
sadly unsuccessful. Not sure though if the request is an API call or not.
Either way I appreciate all the help I can get solving this problem :slight_smile:

Hi Khalid. This turned out to be way more complex than I was expecting.

First, I tried to manually add the appropriate CORS headers (as I said, AspNetCore strips them out if it doesn’t think it’s a CORS request) but that didn’t do anything. Then I tried to do stuff within Fetch in the browser but Fetch doesn’t handle 302s. The browser intercepts them and Fetch gets an undefined response.

So, what I’ve come up with now is a bit convoluted but at least it works.

First, when the authorization kicks in and challenges the request, you have a OnRedirectToIdentityProvider event. Here we are going to change the result from a 302 to a 401 when we have received an xhr request. Unfortunately, there’s no good way to determine that it’s an xhr request so I’ve settled for a proxy just now… we can tell that it’s a request to the dolittle api.

We then set the status to 401 and return a json in the body with a redirect url.

                    options.Events.OnRedirectToIdentityProvider = ctx =>
                    {
                        if (!ctx.Request.Path.Value.ToLower().Contains("api/dolittle")) 
                            return Task.CompletedTask;
                        
                        if (ctx.Response.StatusCode == (int) HttpStatusCode.OK)
                        {
                            ctx.Response.StatusCode = StatusCodes.Status401Unauthorized;
                            ctx.Response.Body.Write(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new { redirectUrl = ctx.ProtocolMessage.CreateAuthenticationRequestUrl()})));
                        }
                        ctx.HandleResponse();
                        return Task.CompletedTask;
                    };

Then we need to handle this in the front end.

function setupAuthenticationRedirect() {
    const oldFetch = fetch;
    fetch = function jwtFetch(input: any, init?: any): Promise<Response> {
        return oldFetch(input, init).then((response: Response) => {
            if (response.status == 401) {
                response.json().then(_ => {
                    if (_.redirectUrl) {
                        window.location.reload();
                    }
                }).catch(err => {})
            }
            return response;
        }).catch(err => {
            console.error(err);
        });
    };
}

This basically wraps the existing Fetch into a new fetch that knows about the 401 / redirectUrl convention and honours it. One last little gotcha. When I tried to navigate to the url that was returned it failed on the return from the sign-in with a correlation error. Basically it thinks we’ve tried to spoof the request.

Rather than do that, I just force a refresh of the page and the normal 302 redirect kicks in and it works. The drawback is that you lose the link you are on and end up back at the root of the site.

Hope this helps.

1 Like

Hi again,
First of thank you for finding a way of doing this.

Backend part is pretty straight forward as you explained it. But i’m a little bit unsure how to solve this in the frontend. You say that I need to overwrite a the old fetch and use a new one. I’m currently using Query and Command-Coordinators in Dolittle to do requests in the frontend. Which means I need to handle a 302 error in every request that the frontend has to do. Maybe a solution could be merging your solution and @Paveet together in this case.
Unless you have any thoughts around how to solve this?

The monkey patching of fetch was a quick fix as the specific requirement really is cross cutting. If we get any 401 unauthorized we want to redirect to the identity provider login. I’m not suggesting this is the best way to do it. We need to make the Coordinators more extensible and give you more entry points to deal with the responses as you want.

@pavneet can chip in about any issues with the front-end part.

1 Like

There seem to be a few options here:

  1. Monkey-patch fetch directly like in @michael’s example
    I think monkey-patching directly can be a decent enough solution. This would typically be done during the bootstrap / startup phase of your application.

  2. Use a 3rd party library to wrap fetch
    A lot like option 1. You could perhaps do this slightly more cleanly through a 3rd party library like https://www.npmjs.com/package/fetch-wrap, which give you the possibility to write your own middleware for headers, options, response handling & errors. I have not used this before

  3. Override with your own implementation of the Command / Query Coordinator in your project
    This would mean copying the implementation from the CommandCoordinator & QueryCoordinator, adapting it to meet your specific needs and using that.

  4. Official lifecycle methods in the Coordinators to handle the response.
    This is probably the long-term way solution that should go into the official Coordinators.

Variants 1,2 & 4 would mean finding the right place in your bootstrap / startup process to write these hooks.

Variant 3 would mean you’d use your project-specific implementation directly or through a dependency inversion container, like in Aurelia.

1 Like

I’ve added a JavaScript-contrib project with an implementation of a custom callback handler that allows you to take over the handling of the request.

This means you also have to return the default behaviour like the example below.

Here’s an installation and usage example based on the above code:

Installing:

npm install @dolittle/contrib

Using:

import { CommandCoordinator, QueryCoordinator } from '@dolittle/contrib'

…
// somewhere in app-start / main / bootstrap
CommandCoordinator.responseHandler((response) =>  handleResponse(response));
QueryCoordinator.responseHandler((response) =>  handleResponse(response));

function handleResponse(response) {
    if (response.status == 401) {
        response.json().then(_ => {
            if (_.redirectUrl) {
                window.location.reload();
            }
        }).catch(err => {})
    }
    return response.json(); //default behaviour
}

2 Likes

I got a tip to have a look at this library which silently refreshes tokens in an iframe behind the scenes. I haven’t had the opportunity to try it out, and see what is involved. The example in the article is for Angular, but it should be usable from any framework.

1 Like

Wow, Thanks for actually creating a small library @pavneet. I will download it now :smiley:

1 Like

Use version ^1.0.1. I forgot to actually export the package contents :man_facepalming:t4:

1 Like