Easy window OAUTH (frontend)

Easy window OAUTH (frontend)

TL;DR Use localStorage to communicate between windows and check it when the window is closed.

The OAUTH e2e process is as follows:

  1. FrontEnd(FE): user clicks on social login button
  2. FE: user is redirected to either a local page that redirects behind the scenes to the page of the social network
  3. Social Network (SN): prompts the user for login information and authorization of your app
  4. SN: redirects the user back to you app to a route that you provided with a response that has data about the user in it
  5. Back End(BE): on that route you do the following:

    1. check if everything was ok with the authorization of your app in the social network provider,

    2. take the returned token, email, username and whatever else you need

    3. query your database which usually gives you two options:

      • create a new user, if there is none in your database
      • login the user in your application, if you find the user in your database
    4. redirect the user to a page in your app(homepage) with the state changed to logged in

Whenever you do an OAUTH login, you will need to setup 2 routes:

  • [get] route 1: to trigger the communication with the social network provider
  • [get] route 2: the route where the social network provider will redirect after the user authorizes your application

Example in adonisjs:

     // route 1 - the redirect from your app to google
     Route.get('/google/redirect', async (ctx) => {
       const { default: GoogleController } = await import(
         'App/Controllers/Http/GoogleController'
       )

       return new GoogleController().redirect(ctx);
     }).middleware('guest');

     // route 2 - the callback from google to your app
     Route.get('/google/callback', async (ctx) => {
      const { default: GoogleController } = await import(
       'App/Controllers/Http/GoogleController'
      )

       return new GoogleController().callback(ctx);
     }).middleware('guest');

However, a difficulty might appear if you decide to open 'route 1' in another window, as the callback will be returned in the same popup window and your user will be logged in in that window. And I hope we can agree that this is not a good UI.

Enter 'the third route'

You will need a third route, that will have the only purpose to render a blank page with some JavaScript code that will do the following:

  1. set an item in the localStorage

  2. close that window

You will redirect your app to this third route from the controller that handles route 2 after everything was fine and you've logged in the user.

    // route 3 - the login success redirect
    Route.get('/social-success-redirect', async ({ view }) => {
      return view.render('auth/social-success-redirect')
    })

The social-success-redirect template will basically be a blank html page with a script similar to this. Note this is a simplified version.

    localStorage.setItem('socialLogin', true);

    // you are in a window popup that you need to close
    window.open(location, '_self').close();

And in the login page, you will have something like this:

    const socialLoginBtn = document.getElementById('social-login');
    const socialLoginStorageKey = 'socialLogin';

    if (socialLoginBtn) {
     socialLoginBtn.addEventListener('click', socialLogin)
    }

    async function socialLogin(event) {
      event.preventDefault();

      // wait for the window to get closed
       const response = await socialLoginPromise();

      // if it's closed by the user without the actual login, show an error
       if (!response) {
        return showSocialLoginError();
       }

       // if everything is okay, you redirect to the homepage and clear the storage key
       window.location.href = '/';
       clearLocalStorageKey(socialLoginStorageKey);
     }

    function socialLoginPromise() {
      return new Promise(resolve => {
       openPopupWindow('http://localhost:3333/google/redirect', resolve);
      })
    }

    function openPopupWindow(url, promiseResolver) {
      const windowFeatures = "height=600,width=600";
      const newWindow = window.open(url, 'socialLogin', windowFeatures);

      // important step: in an interval you check if the window is closed
      // when it's closed, you resolve the promise with the local storage data
      const intervalWindowCheck = setInterval(() => {
        if (newWindow.closed) {
         clearInterval(intervalWindowCheck);

         // you get the key that should be set up in the window
         const storageLoginStatus= window.localStorage.getItem(socialLoginStorageKey)
         resolve(storageLoginStatus);
       }
      }, 1000);

      if (window.focus) {
       newWindow.focus()
      }
    }

Don't forget to do error handling.

Cheers!