How can I integrate Google One Tap sign in with Meteor

https://developers.google.com/identity/one-tap/web/retrieve-credentials

I find it quite nifty for google oauths as it shows the logged in google accounts, but the problem is how to use the retrieved idToken to do a login via Meteor. Is there a way to do so?

1 Like

This is a great question. The Meteor loginwithgoogle is limited…I am integrating a Meteor application with Google’s One Tap. Attempting to use Meteor’s loginWithGoogle in order to get the user to save to Meteor Accounts. However,

One-Tap library is not meant to authorize the user (i.e. produce Access Token), only to authenticate the user

Thus, what I’ve had to do is authenticate the user using Google Api, or gapi to retrieve the necessary access_token and id_token.

What I’ve got so far is as follows:

HTML

<div data-prompt_parent_id="g_id_onload" style={{ position: "absolute", top: "5em", right: "1em" }} id="g_id_onload"></div>

CLENT SIDE

google.accounts.id.initialize({
  prompt_parent_id: "g_id_onload",
  client_id: "42424242-example42.apps.googleusercontent.com",
  auto_select: false,
  callback: handleCredentialResponse
});

const handleCredentialResponse = async oneTapResponse => {
  // see the SERVER SIDE code, which is where validation of One Tap response happens
  Meteor.call("verifyOneTap", oneTapResponse.credential, oneTapResponse.clientId, (error, result) => {
    if (error) {
      console.log(error);
    }
    if (result) {
      // Initialize the JavaScript client library.
      gapi.load("auth2", function() {
        // Ready. Make a call to gapi.auth2.init or some other API 
        gapi.auth2.authorize(
          {
            client_id: oneTapResponse.clientId,
            scope: "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email",
            response_type: "code token id_token",
            prompt: "none",
            // this is the actual email address of user, example@gmail.com, passed back from the server where we validated the One Tap event...
            login_hint: result.email
          },
          function(result, error) {
            if (error) {
              // An error happened.
              console.log(error);
              return;
            }
            //these are the authentication tokens taht are so difficult to capture...
            let theAccessToken = result.access_token;
            let theIdToken = result.id_token;

            //*********************************
            //this is the part that doesn't work
            //trying to get it to create the account without another Google prompt...
            Meteor.loginWithGoogle({ accessToken: theAccessToken, idToken: theIdToken, prompt: "none" }, function(err, res) {
              if (err) {
                console.log(err)
              }
            });
            //*********************************
          }
        );
      });
    }
  });
};

google.accounts.id.prompt(notification => {
  //this just tells you when things go wrong...
  console.log(notification);
});

SERVER SIDE

const { OAuth2Client } = require("google-auth-library");
const clientOA2 = new OAuth2Client("42424242-example42.apps.googleusercontent.com");

// the token and clientId are returned from One Tap in an object, are credential (token) and clientId (clientId)
verifyOneTap: async (token, clientId) => {
  const ticket = await clientOA2.verifyIdToken({
    idToken: token,
    audience: clientId // Specify the CLIENT_ID of the app that accesses the backend
    // Or, if multiple clients access the backend:
    //[CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3]
  });
  const payload = await ticket.getPayload();

  //perform validation here so you don't get hacked...

  return payload;
  // If request specified a G Suite domain:
  // const domain = payload['hd'];
}

Tried writing this in different ways on the client/server, as well as considered ways to go around this and just signing up with Meteor’s Accounts.createUser, but it is less than ideal. What is wrong with the [options] that I am passing to loginWithGoogle ? I would think accessToken and idToken were enough…

What happens is that on login, it does log me in through the first stage of Google One Tap, but then options that I threw into Meteor.loginWithGoogle are somehow not being recognized:

first step of One Step flow works => but then it asks for login again

The documentation on loginWithGoogle states that the format is typically:

Meteor.loginWith<ExternalService>([options], [callback])

and with regards to loginWithGoogle :

options may also include Google’s additional URI parameters

Google’s Additional URI Parameters

Required : client_id, nonce, response_type, redirect_uri, scope

Optional : access_type, display, hd, include_granted_scopes, login_hint, prompt

Unfortunately, it is clearly not recognizing something in the [options] that I am passing, otherwise it would save the user to MongoDB, which it isn’t doing.

1 Like

I will leave my method here just in case it can help you. I don’t have any issues with the Google login flow, it seem to work in the way you expect.


// dispatch is kinda 'return' into the Redux framework
const signInGoogle = route => { // the route to go to after successful login
  return dispatch => {
    const { scope, loginStyle } = ServiceConfiguration.configurations.findOne({ service: 'google' })
   // scope looks like:
   // Service configuration detailed below

    Meteor.loginWithGoogle({ requestPermissions: scope, loginStyle, requestOfflineToken: true }, err => {
      if (err) {
        console.log(err)
        if (err !== 'The user canceled the sign-in flow.') {
          toastr.light('Could not sign in', /*  `Error: ${err}`, */ { timeOut: 2000, icon: (<TiWarningOutline style={{ fontSize: 42, color: 'red' }} />) })
        }
        dispatch({
          type: SIGN_IN_GOOGLE,
          payload: `Error logging Google: ${err}`
        })
      } else { // on success
        dispatch(subscribeBasicUser()) // method to get some user data
        dispatch(push(route)) // go to route after successful login
        dispatch({
          type: SIGN_IN_GOOGLE,
          payload: 'with Google'
        })
      }
    })
  }
}

Service Configuration: run in startup/server

import { Meteor } from 'meteor/meteor'
import { ServiceConfiguration } from 'meteor/service-configuration'
import { Accounts } from 'meteor/accounts-base'

Meteor.startup(() => {
  const services = Meteor.settings.private.oAuth
  if (services) {
    for (const service in services) {
      ServiceConfiguration.configurations.upsert({ service: service }, {
        $set: services[service]
      })
    }
  }

  Accounts.config({
    sendVerificationEmail: true,
    passwordResetTokenExpirationInDays: 1
  })
})

Meteor settings:

{
....
 "google": {
        "clientId": "xxxxxxxx.apps.googleusercontent.com",
        "secret": "xxxxxxxx",
        "loginStyle": "popup",
        "scope": ["email", "https://www.googleapis.com/auth/plus.login"]
      }
}

I think what you have here is just the regular sign in with google (loginWithGoogle works no problem), not Google One Tap. I found a solution: https://stackoverflow.com/questions/62922444/google-one-tap-integration-with-meteor