6

I am using Google Identity Services, and facing some problems. Have a look at the function below to loginUser and get the access_token:

const client = (window as any).google.accounts.oauth2.initTokenClient({
  client_id: process.env.GOOGLE_CLIENT_ID,
  scope: `profile email`,
  callback: '' // defined at request time
});

const loginUser = async () => {
  const tokenResponse = await new Promise<TokenResponse>((resolve, reject) => {
    try {
      // Settle this promise in the response callback for requestAccessToken()
      client.callback = (resp) => {
        if (resp.error !== undefined) {
          reject(resp);
        }
        resolve(resp);
      };
      // requesting access token
      client.requestAccessToken({ prompt: 'consent' });
    } catch (err) {
      console.log(err)
    }
  });
  return tokenResponse;
}

Invoking loginUser() causes a new pop-up.

  • If the user selects an account, I get the tokenResponse (which contains access_token). Works great.
  • But if the user closes the pop-up, the Promise never resolves, since we are waiting for the callback to fire, which never happens.

Is there a way we could detect if the user has closed the pop-up?

xmedeko
  • 7,336
  • 6
  • 55
  • 85
subhamX
  • 299
  • 4
  • 13

4 Answers4

6

I think you can do something in the "error_callback". You can find details at: Handle Errors

const client = google.accounts.oauth2.initCodeClient({
  client_id: 'YOUR_GOOGLE_CLIENT_ID',
  scope: 'https://www.googleapis.com/auth/calendar.readonly',
  ux_mode: 'popup',
  callback: myCallback,
  error_callback: myErrorCallback  // You can do something when popup window closed
});
Fred
  • 116
  • 1
  • 3
5

(Update) Prospective Solution

It looks like the google developers have added the error handlers now into the new Google Identity Services. :) Checkout the documentation at https://developers.google.com/identity/oauth2/web/guides/error.

(I still haven't tested it. Hence putting it as a prospective solution). Happy coding!

Original Answer

Here are the two solutions which you can consider if you're facing this issue.

Solution 1

Go back to the old gapi based login. (Not recommended, as it will be deprecated soon). For more details, on deprecation, refer to this blog by Google.

Solution 2

We add a javascript focus event listener just after opening the popup. So, whenever the user closes the popup and returns to the parent window, we shall consider it as client_focused_back_to_window / pop_up_closed event.

The only edge case is when the user doesn't close the popup and directly returns to the window; the focus event listener will be fired. But I think that's okay because if the user again clicks on Sign In with Google button again, the same pop-up window gets reused (thanks to _blank parameter used by Google Identity services while creating the popUp window).

const client = (window as any).google.accounts.oauth2.initTokenClient({
  client_id: process.env.GOOGLE_CLIENT_ID,
  scope: `profile email`,
  callback: '' // defined at request time
});

/**
 * Function to login the user and return the tokenResponse
 * 
 * It throws error if the login fails or the user cancels the login process
 */
const loginUser = async () => {
  const tokenResponse = await new Promise<google.accounts.oauth2.TokenResponse>(
    (resolve, reject) => {
      const focusEventHandler = () => {
        reject({
          error: 'client_focused_back_to_window',
        });
        window.removeEventListener('focus', focusEventHandler); // removing the event listener to avoid memory leaks
      };
      // adding an event listener to detect if user is back to the webpage
      // if the user "focus" back to window then we shall close the current auth session
      window.addEventListener('focus', focusEventHandler);

      // Settle this promise in the response callback for requestAccessToken()
      client.callback = (resp) => {
        if (resp.error) {
          reject(resp);
        }
        resolve(resp);
      };
      // requesting access token
      client.requestAccessToken({ prompt: 'consent' });
    },
  );
  return tokenResponse;
}

PS: We've been using this solution in production, and so far, thousands, if not millions, of users have tried to log in via Google. Everything is working fine so far.

subhamX
  • 299
  • 4
  • 13
  • 1
    Very nice little workaround. Thank you for this! :) – Robert Desmond Aug 29 '22 at 16:24
  • 1
    Great workaround. Unfortunately, does not work when need to authorize on page load and the popup is blocked by a browser. Workroud to this may be to hook on console.error and watch for log "Failed to open popup window on url:... Maybe blocked by the browser?" – xmedeko Sep 12 '22 at 11:37
2

It appears that this is not working for the current version of GSI.

It did work for the old gapi version and if the popup were to be closed you would get a response with the error: {error: "popup_closed_by_user"}. As referenced in this answer: Google SSO login error: "popup_closed_by_user"

Hopefully adding the #google-oauth tag will allow someone at Google to see this and hopefully update this script.

Please see other referenced question: Google Oauth popup cancellation callback

This is referring to the documentation on https://developers.google.com/identity/oauth2/web/guides/use-code-model#trigger_oauth_20_code_flow and https://developers.google.com/identity/oauth2/web/guides/use-token-model#initialize_a_token_client

In fact the documentation states: Users may close the account chooser or sign-in windows, in which case your callback function will not be invoked..

Question for Google - how can we detect this?!

0

For your question, here my below answer, hope it will help you.

Please integrate below my code in your one of any script tag of your index.html or default or main page, and then you can control your window.open based on URL.

     Function.prototype.isNative = function() {
  return this.toString().indexOf('[native code]') > -1;-  }


if (window.open.isNative()) {
    //   debugger;
     var originalOpen = window.open;
     window.open = function(URL, name, specs, replace) {
       console.log(originalOpen, 'originalOpen called');
         var newWindow = originalOpen(URL, name, specs, replace);
         console.log(originalOpen, 'originalOpen in new window called', URL, name, specs, replace);
        // debugger;
         if (URL.indexOf('https://accounts.google.com/') === 0) {
             var interval = setInterval(function() {
             //  console.log('Interval Started');
                 if (newWindow.closed) {
                     clearInterval(interval);
                     setTimeout(function() {
                       //Your conditional code goes here..
                        
                     }, 500);
                 }
             }, 1000);
         }
         return newWindow;
     }
 }

And run your application, above is for Google authentication window, and based on facebook, LinkedIn or Microsoft you can change URL conditions. Thanks!