Danielle

Co-Founder of Squarecat.io & LeaveMeAlone.app | Indie Dev 👩‍🎤 Dog Lover 🐶 Nomad 🎒

How we built an account creation popup for Google and Outlook OAuth

At Leave Me Alone we use Google and Microsoft OAuth for user sign in. To do this we redirect the user to the relevant login page, the user input their details, and is then directed back to our site and logged in. An unfortunate result of this is that our analytics report a great deal of referral traffic coming from “accounts.google.com” and “login.microsoft.com”.
To solve this problem, instead of redirecting it would be better if we could open a new window or popup for the OAuth flow. Also this is probably a better experience for the user than being redirected around.
We use Passport for our authentication so when the user is directed back to our app after signing in the URL contains some parameters we need, including a token we use to authenticate them on our server.
Since we want to use a popup we need an additional step in the middle of the flow to catch that redirect, retrieve the URL parameters, close the popup, and use the token in the opening window (not the popup).
We allow users to sign in with Google and Outlook and the implementation is the same for both. For ease of reading the example will use Google.

Step 1: Open the popup

To open a new window we use Window.open() with the passport login URL (/auth/google in our case) which opens the “Sign in to Leave Me Alone with Google” page in a new window. We also give the window a name and pass the requested features we want it to have.
We assign the window reference and record the previous URL so that the same window will be used or focused if the user tries to click the sign in button again, even if it’s for a different provider. We don’t want two popups for different providers floating around causing confusion.
Finally, we add an event listener for messages as the popup is going to send the URL parameters and auth token when it’s finished.
let windowObjectReference = null;
let previousUrl = null;

const openSignInWindow = (url, name) => {
   // remove any existing event listeners
   window.removeEventListener('message', receiveMessage);

   // window features
   const strWindowFeatures =
     'toolbar=no, menubar=no, width=600, height=700, top=100, left=100';

   if (windowObjectReference === null || windowObjectReference.closed) {
     /* if the pointer to the window object in memory does not exist
      or if such pointer exists but the window was closed */
     windowObjectReference = window.open(url, name, strWindowFeatures);
   } else if (previousUrl !== url) {
     /* if the resource to load is different,
      then we load it in the already opened secondary window and then
      we bring such window back on top/in front of its parent window. */
     windowObjectReference = window.open(url, name, strWindowFeatures);
     windowObjectReference.focus();
   } else {
     /* else the window reference must exist and the window
      is not closed; therefore, we can bring it back on top of any other
      window with the focus() method. There would be no need to re-create
      the window or to reload the referenced resource. */
     windowObjectReference.focus();
   }

   // add the listener for receiving a message from the popup
   window.addEventListener('message', event => receiveMessage(event), false);
   // assign the previous URL
   previousUrl = url;
 };
To get the window to open as a popup instead of a new tab we had to request the features `menubar=no,toolbar=no`.

Step 2: Get the OAuth callback parameters in the popup

When the OAuth flow is complete Google redirects the user to a callback URL. Usually this would be a server route which would perform the Passport authentication. Since the auth is happening in a popup we use a page in our app, which when which when loaded grabs the search parameters and sends them to the parent.
This callback page uses a React Use Effect Hook which executes when the page loads. We fetch the URL parameters which will include the auth token and send them to the opening window (the parent) using Window.postMessage().
export default () => {
 useEffect(() => {
  // get the URL parameters which will include the auth token
   const params = window.location.search;
   if (window.opener) {
     // send them to the opening window
     window.opener.postMessage(params);
     // close the popup
     window.close();
   }
 });
 // some text to show the user
 return <p>Please wait...</p>;
};

Step 3: Authenticate the user and redirect to the app

The OAuth flow is almost complete the and the popup is now closed, we just need to authenticate the user on our server.
The receive message function needs to check the origin of the message to make sure it’s from the same domain for security. While coding this we realised that several Chrome developer tools use postMessage() from the same origin so we also check the source before trying to extract the payload.
Once we have the OAuth parameters, we redirect to the user to our own authentication endpoint so that we can use Passport to authenticate and login.
const receiveMessage = event => {
 // Do we trust the sender of this message? (might be
 // different from what we originally opened, for example).
 if (event.origin !== BASE_URL) {
   return;
 }
 const { data } = event;
 // if we trust the sender and the source is our popup
 if (data.source === 'lma-login-redirect') {
   // get the URL params and redirect to our server to use Passport to auth/login
   const { payload } = data;
   const redirectUrl = `/auth/google/login${payload}`;
   window.location.pathname = redirectUrl;
 }
};

Finished!

The process is quite simple and all we have done here is add an intermediary step in the OAuth flow to pass through the callback parameters.
There are probably lots of implementations but this was the quickest and simplest for us using React.js.
Hopefully this helped you or provided some inspiration for your own solution.
Let us know if you have any questions or suggestions!
Originally published at https://blog.squarecat.io on April 25, 2019.

Tags

More by Danielle

Topics of interest