Google-api-javascript-client: How to authenticate user with a token from Google One-Tap signin?

Created on 23 Feb 2018  Â·  23Comments  Â·  Source: google/google-api-javascript-client

@TMSCH I just started implementing the Google One-Tap signin.

Since I use gapi.auth2.init() and as you told me, this logs the user in automatically, I check after the initialization wether or not there is a user logged in. If not I will use googleyolo.retrieve() to get the login popup.

Next step is that I receive the token from the googleyolo signin.
After I receive the credential, I change the object to use it with gapi.client.setToken(token). However When trying gapi.auth2.getAuthInstance().isSignedIn.get() after this, my user is still logged out...

I tried to look at the GAPI functions to log in a user through the token, but I couldn't find any function. I thought gapi.client.setToken() is enough. What else should I do to log in the user?

I tried to check the official One-Tap Signin documentation but they have guides/samples on ever library besides the JavaScript web library...... >.>

Here is my code:

initOneTapSignin(){
  googleyolo.retrieve(oneTapConfig)
  .then(credential => {
    let token = {access_token: credential.idToken}
    window.gapi.client.setToken(token)
  })
}
function loadAndInitGAPI() {
  return new Promise((resolve, reject) => {
    let script = document.createElement('script')
    script.type = 'text/javascript'
    script.src = 'https://apis.google.com/js/api.js'
    script.onload = e => {
      window.gapi.load('client:auth2', _ => {
        console.log('loaded GAPI')
        function initGAPI(){
          if (!window.gapi || !window.gapi.client){ return reject() }
          window.gapi.client.init(gapiConfig)
          .then(_ => {
            store.dispatch('user/updateAuthStatus')
            .then(_ => resolve())
            .catch(_ => {
              console.log('No user on GAPI client init.')
              initOneTapSignin()
              resolve()
            })
          })
        }
        setTimeout(initGAPI, 10)
      })
    }
    document.getElementsByTagName('head')[0].appendChild(script)
  })
}

Most helpful comment

gapi.client.init returns a Thenable. You shouldn't call anything before it resolves.

Error 1: Provide API Key.

Error 2: Don't provide scope. As you manually initialize auth2 this is not required.

let config =  {
  response_type: 'permission',
  scope,
  client_id: clientId,
  login_hint: credential.id
};
window.gapi.auth2.authorize(config, function(response) {
  // No need to `setToken`, it's done for you by auth2.
  let config2 = {discoveryDocs} // only of google calendar
  window.gapi.client.init(config2).then(function() {
    // then execute a calendar call:
    window.gapi.client.calendar.events.list(calConf)
  });
})

All 23 comments

@mesqueeb this is actually not straightforward to use the two libraries together. You cannot just "sign in" the user by passing a token. In your use case, do you need an access token to perform requests to Google APIs?

Dear @TMSCH Thank you for your answer.
I need to access the google calendar of the user.

Instead of using the load + init functions I saw your answer in this thread:
https://github.com/google/google-api-javascript-client/issues/360
So now I have tried to use gapi.auth2.authorize instead.

Now my problem is that I still can't make a request to the calendar, because .calendar is missing from client...

I'm trying something really straight forward now. Would be great if you could have a look! : D

initOneTapSignin(){
  googleyolo.retrieve(oneTapConfig)
  .then(credential => {
    let token = {access_token: credential.idToken}
    GAPITokenSignIn(token)
  })
}
GAPITokenSignIn(token)
{
  window.gapi.load('client:auth2', _ => {
    window.gapi.client.setToken(token)
    window.gapi.auth2.authorize(googleCalendarAuthConf, function(response) {
      if (response.error) {
        return // An error happened!
      }
      // The user authorized the application for the scopes requested.
      var accessToken = response.access_token
      var idToken = response.id_token
      // You can also now use gapi.client to perform authenticated requests.
      console.log('accessToken → ', accessToken)
      console.log('idToken → ', idToken)
    })
  })
}

I have this almost literally from the documentation, I'm not sure why calendar won't show inside client! : D

You have mentioned in 1 place that my {access_token: credential.idToken} isn't correct because it's not the idToken I need here. But in the Google One-Tap documentation it says nothing about access tokens. Do you know any way to convert the idToken to an access_token?

This is my googleCalendarAuthConf btw:

{ clientId, scope: 'https://www.googleapis.com/auth/calendar', prompt: 'none' }

The docs say I don't need apiKey or discoveryDocs here.

Thank you very much for your patience @TMSCH ☆

@mesqueeb You have to initialize gapi.client first (without passing any scope or clientId so auth2 is not initialized under the hood).

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

@TMSCH I understand I'm facing 5 difficulties I don't fully understand.
It would be great if I could create a quick PoC if you could help me with the 5 questions.

I will first write what I want to do:

A new user case:

  1. Authenticate a user through Google One-Tap.
  2. The user presses a button to grant Calendar access.
  3. gapi is loaded/initialised.
  4. There is a popup for calendar access consent. (this is through gapi)
  5. This user's "consent" is somehow saved: google does it? / I have to save an access_token to the user's database?
  6. gapi retrieves calendar information of the user.

A returning user case:

  1. The user is automatically authenticated through Google One-Tap. (the library takes care of returning users)
  2. The app (google? / the user db?) remembers this user has given consent to access his calendar (retrieve the access_token).
  3. gapi is loaded/initialised.
  4. gapi is feeded with the authentication from Google One-Tap.
  5. gapi is feeded with the authorization through the access_token that was fetched.
  6. gapi retrieves calendar information of the user.

The questions

  1. (new user) Step 3: I don't know how to load/initialise gapi for just the calendar consent. When initialising with auth2 it will try to auto login users.
  2. (new user) Step 5: What token is the token for "authorisation to access the calendar"? How/when is this token returned? I have yet to find this token...
  3. (returning user) Step 3: How do I load/initialise it just to work with the calendar and not with authentication?
  4. (returning user) Step 4: I write to feed in the authentication from Google One-Tap. But is this really required to be able to use gapi to retrieve the user's calendar? Or is the access_token enough?
  5. (returning user) Step 4: In case we need to log in the user to gapi as well, how do I do this automatically after a user logs into Google One-Tap?

I've been spending the last week trying to understand everything, but I'm still a little bit confused on the above. I think if I understand these 5 questions I can finish my app's authentication within an hour. Could you help me with these?

@mesqueeb do you have to retrieve the calendar only once (right after the user signs in) or continuously?

Dear @TMSCH I need to subsequent requests when the user wants to edit calendar events from my app. It's a two-way-peg.

I'd use gapi.auth2.authorize for your use case. You can pass the credential.id returned by GoogleYolo as the login_hint param in the call to authorize. This will preselect the account.

gapi.auth2.authorize({
  response_type: 'permission', // Access Token.
  scope: 'CALENDAR_SCOPE',
  login_hint: credential.id
}, function(result) {});

Additionally, using prompt: 'none', you can try to fetch a token without any UI. This is useful to check whether you need to show an Authorize button, and also useful before making any call to the Calendar API to get a fresh token.

That should resolve your questions 1 to 4.

For 5, you should call gapi.auth2.init passing the login_hint: credential.id.

@TMSCH
Thank you very much.

1) Load of the client required?

When I tried this method there was no gapi.auth2 so I did:

window.gapi.load('client') // without any other parameters

This is correct?

2) Init of client.calendar required?

After authorising GAPI like you said, it seems I still cannot make requests to the calendar, as gapi.client.calendar does not exist.
What's the best way to load/initialise them without using the traditional load-init method? I didn't find any documentation on this.

This is my current setup:

let config =  {
  response_type: 'permission',
  scope,
  client_id: clientId,
  login_hint: credential.id
}
window.gapi.auth2.authorize(config, function(response) {
  let accessToken = response.access_token
  // You can also now use gapi.client to perform authenticated requests.
  return window.gapi.client.setToken(accessToken)
})
let config2 = {scope, discoveryDocs} // only of google calendar
window.gapi.client.init(config2)
// then execute a calendar call:
window.gapi.client.calendar.events.list(calConf)

Error in case 1: Run gapi.client.init(config2) from the chrome-console:
It seems to work. Step 2: gapi.client.calendar.events.list(calConf) and I get this error:

"domain": "usageLimits",
"reason": "dailyLimitExceededUnreg",
"message": "Daily Limit for Unauthenticated Use Exceeded. Continued use requires signup.",
"extendedHelp": "https://code.google.com/apis/console"

Error in case 2: Run gapi.client.init(config2) in my main js script:

client_id and scope must both be provided to initialize OAuth.

However, the gapi.client.init docs say:

If OAuth client ID and scope are provided, this function will load the gapi.auth2 module to perform OAuth

So this is just what we are trying to avoid right!
Any hints? : D

PS: sorry for a series of edits, I hope your email wasn't flooded...

gapi.client.init returns a Thenable. You shouldn't call anything before it resolves.

Error 1: Provide API Key.

Error 2: Don't provide scope. As you manually initialize auth2 this is not required.

let config =  {
  response_type: 'permission',
  scope,
  client_id: clientId,
  login_hint: credential.id
};
window.gapi.auth2.authorize(config, function(response) {
  // No need to `setToken`, it's done for you by auth2.
  let config2 = {discoveryDocs} // only of google calendar
  window.gapi.client.init(config2).then(function() {
    // then execute a calendar call:
    window.gapi.client.calendar.events.list(calConf)
  });
})

Dear @TMSCH I didn't think we'd ever get this far! : D
IT WORKED!!

How did you know that after authorizing the gapi client,
you needed to initialize gapi.client with only the calendar discovery docs?
I didn't find any information on this anywhere... o_O

You're really a gapi master! Thank you so much!

Glad you got it working! :)

The doc states:

If OAuth client ID and scope are provided, this function will load the gapi.auth2 module to perform OAuth

As you're manually calling gapi.auth2.authorize, no need to pass these parameters.

@TMSCH
I've documented our thread on stack-overflow as well. Thanks again.

Thanks!

EDIT: I am not sure whether a Closed Thread gets any attention, so I opened a new one #551. (Someone please feel free to delete one of these posts.)

Hey @TMSCH,

I have read all your posts here, in #304 and in #360. Sadly I'm still did not figure out how to solve my problem (accessing Google APIs trough Gapi on a Cordova device after restarting the app.)

I would appreciate every helping advice! 🤗

What I would like to achieve

1.) I would like to let users sign in with Google account into an Ionic / Cordova app running on iOS and on Android via Firebase.
2.) I would like afterwards to access the users emails in Gmail via Gapi. And being able to do so after closing and opening the app.

What I have achieved

export class GoogleLoginComponent implements OnInit {
  user: Observable<firebase.User>;
  private course: any;

  constructor(
    private afAuth: AngularFireAuth,
    private gplus: GooglePlus,
    private platform: Platform
  ) {
    this.user = this.afAuth.authState;
    this.initGapiClient();
  }

  async nativeGoogleLogin(): Promise<firebase.auth.UserCredential> {
    try {
      const gplusUser = await this.gplus.login({
        webClientId: firebaseConfig.clientId,
        offline: true,
        scopes:
          'profile ' +
          'email ' +
          'https://mail.google.com/ ' +
          'https://www.googleapis.com/auth/gmail.modify ' +
          'https://www.googleapis.com/auth/gmail.readonly'
      });

      return await this.afAuth.auth.signInWithCredential(
        firebase.auth.GoogleAuthProvider.credential(gplusUser.idToken)
      );
    } catch (err) {
      console.log(err);
    }
  }

  async getGmailMessagesList() {
    const messages = await gapi.client.gmail.users.messages.list({
      userId: 'me'
    });

}
  • I'm more or less able to solve 2.), but it only works straight after the user login and does not work after closing and opening the app (even though the user is still signed in). I'm able to do so by adding the following:

To constructor([...]):

      gapi.load('client');

To nativeGoogleLogin():

      gapi.client.setToken({ access_token: gplusUser.accessToken });
      gapi.client.load('gmail', 'v1');

What I figured out / think

  • If save the gplusUser.accessToken and provide it to gapi.client.setToken after closing and opening the app, I'm able to access the Google APIs, but from what I understand the token will expire after some time and then I would have the same problem as before.
  • From what I've read here the access_token is not meant to do the authentication for Gapi and I think the authentication is what I need to be able to close and open the app and still have access to the APIs.

I'd use gapi.auth2.authorize for your use case. You can pass the credential.id returned by GoogleYolo as the login_hint param in the call to authorize. This will preselect the account.

gapi.auth2.authorize({
  response_type: 'permission', // Access Token.
  scope: 'CALENDAR_SCOPE',
  login_hint: credential.id
}, function(result) {});

Additionally, using prompt: 'none', you can try to fetch a token without any UI. This is useful to check whether you need to show an Authorize button, and also useful before making any call to the Calendar API to get a fresh token.

That should resolve your questions 1 to 4.

For 5, you should call gapi.auth2.init passing the login_hint: credential.id.

Hello, sorry, but I did not understand one moment. I have access_token to my google account and trying auto login using this token, but i see popup "Sign in with Google". It possible sign in to my account using credentials without this popup? Or is it impossible on front JavaScript?

@TMSCH @JilReg @ZosimaQuiz It would be great, if you could help with suggestion for the offline access.

@TMSCH Thank you so much!! <33

@TMSCH , now that googleyolo was deprecated way, I can't seem to get the new one tap to play nice with the authorize flow.

      window.google.accounts.id.initialize({
        client_id: clientId,
        callback: response => {
          setToken(response.credential)
        },
      })
      window.google.accounts.id.prompt()

+

const authorize = await new Promise<gapi.auth2.AuthorizeResponse>(resolve =>
      window.gapi.auth2.authorize(
        {
          response_type: 'permission',
          scope: 'profile',
          prompt: 'none',
          client_id: clientId,
          login_hint: token,
        },
        res => resolve(res)
      )
    )
    console.log(authorize)

I am getting an authorize token but, in a scenario of 2 accounts - I get the token of the previously logged in user . and if there's no loggedin user - authorize fails with an error.

I believe the token provided by the new one tap is not what i need to pass to login_hint - but I can't figure what I should be feeding it with.

My scenario is super simple:
login page with one tap, I just need to get the user email+name.
Is there any documentation on how to achieve this without backend code?
I was able to get the normal auth2.login() flow up and running in a second.

Thanks!

the login_hint should be the email or the subject id (sub) in the ID token.

Thanks for the lead!
What I ended up doing is to verify the token using the https://oauth2.googleapis.com/tokeninfo?id_token=${token} endpoint (at the frontend).

What threw me off is the lack of an sdk function to handle this for me given the id-token - I hope I didn't miss that part.

This is the code:

interface IdToken {
  iss: string
  nbf: string
  aud: string
  sub: string
  email: string
  email_verified: string
  azp: string
  name: string
  picture: string
  given_name: string
  family_name: string
  iat: string
  exp: string
  jti: string
  alg: string
  kid: string
  typ: string
}
window.google.accounts.id.initialize({
        client_id: clientId,
        callback: async response => {
          try {
            const res = await fetch(
              `https://oauth2.googleapis.com/tokeninfo?id_token=${response.credential}`
            )
            if (res.ok) {
              const body: IdToken = await res.json()
              console.log({
                email: body.email,
                first_name: body.given_name,
                last_name: body.family_name,
                id: body.sub,
                profile_picture: body.picture,
              })
            }
          } catch (err) {
            console.log(err)
          }
        },
      })

You can also use Google API CORS to send requests to Google API if you have "access_token"

Having similar issues (except trying to sign into Meteor, which uses Google's authentication).

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

google.accounts.id.prompt(notification => {
  console.log(notification);
});

const handleCredentialResponse = oneTapResponse => {
  const { clientId } = oneTapResponse;
  Meteor.loginWithGoogle(
    {
      client_id: clientId,
      nonce: Random.id(),
      response_type: "code",
      redirect_uri: Meteor.settings.public.site_url,
      scope: ["email", "profile"],
      prompt: "none"
    },
    function(err, res) {
      if (err) {
        throw new Meteor.Error("Google login failed");
      }
      console.log(res);
    }
  );
};

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.

        const res = await fetch(`https://oauth2.googleapis.com/tokeninfo?id_token=${response.credential}`)

Not sure I see the point in this. You can just use this guide's directions, seems about the same thing:

const {OAuth2Client} = require('google-auth-library'); const client = new OAuth2Client(CLIENT_ID); async function verify() { const ticket = await client.verifyIdToken({ idToken: token, audience: CLIENT_ID, // 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 = ticket.getPayload(); const userid = payload['sub']; // If request specified a G Suite domain: // const domain = payload['hd']; } verify().catch(console.error);

I'd use gapi.auth2.authorize for your use case. You can pass the credential.id returned by GoogleYolo as the login_hint param in the call to authorize. This will preselect the account.

gapi.auth2.authorize({
  response_type: 'permission', // Access Token.
  scope: 'CALENDAR_SCOPE',
  login_hint: credential.id
}, function(result) {});

Additionally, using prompt: 'none', you can try to fetch a token without any UI. This is useful to check whether you need to show an Authorize button, and also useful before making any call to the Calendar API to get a fresh token.
That should resolve your questions 1 to 4.
For 5, you should call gapi.auth2.init passing the login_hint: credential.id.

Hello, sorry, but I did not understand one moment. I have access_token to my google account and trying auto login using this token, but i see popup "Sign in with Google". It possible sign in to my account using credentials without this popup? Or is it impossible on front JavaScript?

Did you ever figure out how to sign in without the popup "Sign in with Google"?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

indapublic picture indapublic  Â·  6Comments

nidri picture nidri  Â·  7Comments

mesqueeb picture mesqueeb  Â·  5Comments

Nilesh-P picture Nilesh-P  Â·  4Comments

Price775 picture Price775  Â·  3Comments