Handling OAuth Operations

— 6 minute read

In Freelance Insights we need to connect users' accounts to a provider (FreeAgent at this stage) and so I had to handle users going off-site to do the auth and then coming back, or not. I'll discuss how I tried to turn that operation into something that can be handled like a regularly AJAX call.


The process is pretty simple:

  • User signs up to FI
  • User clicks 'connect FreeAgent' button. Button turns to spinner.
  • New window opens with my API url (auth/freeagent) which then redirects to FreeAgent's auth page.
  • User authorises with FreeAgent which redirects window to the callback URL on the API (auth/freeagent/callback).
  • Callback route updates user's details. Callback's view issues a 'close window' command - window.close();
  • Happy days.

The problem with this is that most of it is happening outside of my app, in another window. With the main questions being:

  • How do I know when it's finished?
  • How do I bring users back to the app?
  • How do I verify that it was successful?
  • How do I know if it failed?

I was keen to handle this interaction as I would a normal API call - i.e. a function call that returns a promise which will either resolve on success, or reject on failure.

So this is what I came up with:

static async auth() {
const popup = window.open(`${baseURL}auth/freeagent`);

return new Promise((resolve, reject) => {
let popupTick;
const timeoutTick = setTimeout(() => {
clearInterval(popupTick);

reject(new Error('The authentication timed-out. Please try again'));
}, 15 * 60 * 1000);

popupTick = setInterval(async () => {
if (popup.closed) {
clearInterval(popupTick);
clearTimeout(timeoutTick);

if (await AuthorizationAPI.checkFreeAgentAuth()) {
resolve();
} else {
reject(new Error('An error occurred while authenticating with your provider. Please try again'));
}
}
}, 500);
});
}

It's kind of messy so let's break it down:

First we open up the new window for the auth to happen:

const popup = window.open(`${baseURL}auth/freeagent`);

We then return a new Promise that will resolve to our outcome.

Inside that we set up a fail-safe timer that will reject the promise if nothing else happens after 15 minutes.

const timeoutTick = setTimeout(() => {
clearInterval(popupTick);

reject(new Error('The authentication timed-out. Please try again'));
}, 15 * 60 * 1000);

If that triggers then we reject it with an error.

Next we set up an interval timer where we constantly check the state of the popup window that we opened.

popupTick = setInterval(async () => {
if (popup.closed) {
clearInterval(popupTick);
clearTimeout(timeoutTick);

if (await AuthorizationAPI.checkFreeAgentAuth()) {
resolve();
} else {
reject(new Error('An error occurred while authenticating with your provider. Please try again'));
}
}
}, 500);

We want to know if it's closed or not (remember, we force the window closed in the callback endpoint). If the window has closed (checking the popup.closed property) then we know something has happened - either the user has hit the callback and it's closed automatically, or the user has bailed early and closed it themselves.

We start by clearing the timers cos they've done their job. Next we hit an API endpoint which will verify whether the auth operation was successful. This method will resolve to a boolean. If it's true then we're good to go, we resolve the promise and the calling code will stop the spinner and move the process on. If it wasn't successful then we reject it and the calling code will show an error and offer the user to retry.

I think that answers all the questions that were posed at the top! Let me know if you use a different approach or if it can be improved!