Single flow: sign user in via Google oAuth AND grant offline/server access?
Asked Answered
F

2

9

I'm trying to implement Google sign-in and API access for a web app with a Node.js back end. Google's docs provide two options using a combo of platform.js client-side and google-auth-library server-side:

  1. Google Sign-In with back-end auth, via which users can log into my app using their Google account. (auth2.signIn() on the client and verifyIdToken() on the server.)
  2. Google Sign-in for server-side apps, via which I can authorize the server to connect to Google directly on behalf of my users. (auth2.grantOfflineAccess() on the client, which returns a code I can pass to getToken() on the server.)

I need both: I want to authenticate users via Google sign-in; and, I want to set up server auth so it can also work on behalf of the user.

I can't figure out how to do this with a single authentication flow. The closest I can get is to do the two in sequence: authenticate the user first with signIn(), and then (as needed), do a second pass via grantOfflineAccess(). This is problematic:

  1. The user now has to go through two authentications back to back, which is awkward and makes it look like there's something broken with my app.
  2. In order to avoid running afoul of popup blockers, I can't give them those two flows on top of each other; I have to do the first authentication, then supply a button to start the second authentication. This is super-awkward because now I have to explain why the first one wasn't enough.

Ideally there's some variant of signIn() that adds the offline access into the initial authentication flow and returns the code along with the usual tokens, but I'm not seeing anything. Help?

(Edit: Some advice I received elsewhere is to implement only flow #2, then use a secure cookie store some sort of user identifier that I check against the user account with each request. I can see that this would work functionally, but it basically means I'm rolling my own login system, which would seem to increase the chance I introduce bugs in a critical system.)

Franek answered 23/2, 2021 at 19:41 Comment(6)
I am just curious to know how approach #1 helps maintaining the browser login session.Comptom
Google's library takes care of the mechanics of that (and thus I don't know the details). If you implement what's in the linked tutorial, you're able to (a) check if the user is already logged in, (b) initiate a login flow if they're not, and (c) sign them out if they are. That ensures a valid session with Google. Then you can take the token you get, send it to your server, and use Google's server-side libraries to ensure the token is valid and compare it against your own records to ensure it matches a user in your system (or create a new one).Franek
Ok, Thank you for the info. I didn't use that before. Also, now I understand why you are focused on approach #1 as well.Comptom
One other thing, if you check the "back-end auth" part of approach #1 , developers.google.com/identity/sign-in/web/… , it is mentioned that you need to create the session or register user yourself by using the id token. I think Google library will not help in maintaining the session with your server. If it's a browser based app with out a backend server, Google can do that, that's what I think.Comptom
If you want user delegated access, I think you definitely need an "access token" which I don't find anywhere using approach #1 , not 100% sure though. So, in that case , I guess approach #2 might help. Yes, it will be some more work to maintain the session, but Google libraries takes care of the refresh token , so we don't need to get a new access token our self.Comptom
It's been great conversation and learning. I have bookmarked the question. Please add the answer once you get it. I am also curious to know how to get the required thing done using approach #1 and #2 in a single authentication flow.Comptom
R
2

To add an API to an existing Google Sign-In integration the best option is to implement incremental authorization. For this, you need to use both google-auth-library and googleapis, so that users can have this workflow:

  1. Authenticate with Google Sign-In.
  2. Authorize your application to use their information to integrate it with a Google API. For instance, Google Calendar. 

For this, your client-side JavaScript for authentication might require some changes to request offline access:

$('#signinButton').click(function() {
   auth2.grantOfflineAccess().then(signInCallback);
 });

In the response, you will have a JSON object with an authorization code:

{"code":"4/yU4cQZTMnnMtetyFcIWNItG32eKxxxgXXX-Z4yyJJJo.4qHskT-UtugceFc0ZRONyF4z7U4UmAI"}

After this, you can use the one-time code to exchange it for an access token and refresh token. Here are some workflow details:

The code is your one-time code that your server can exchange for its own access token and refresh token. You can only obtain a refresh token after the user has been presented an authorization dialog requesting offline access. If you've specified the select-account prompt in the OfflineAccessOptions [...], you must store the refresh token that you retrieve for later use because subsequent exchanges will return null for the refresh token

Therefore, you should use google-auth-library to complete this workflow in the back-end. For this, you'll use the authentication code to get a refresh token. However, as this is an offline workflow, you also need to verify the integrity of the provided code as the documentation explains:

If you use Google Sign-In with an app or site that communicates with a backend server, you might need to identify the currently signed-in user on the server. To do so securely, after a user successfully signs in, send the user's ID token to your server using HTTPS. Then, on the server, verify the integrity of the ID token and use the user information contained in the token

The final function to get the refresh token that you should persist in your database might look like this:

const { OAuth2Client } = require('google-auth-library');

/**
* Create a new OAuth2Client, and go through the OAuth2 content
* workflow. Return the refresh token.
*/
function getRefreshToken(code, scope) {
  return new Promise((resolve, reject) => {
    // Create an oAuth client to authorize the API call. Secrets should be 
    // downloaded from the Google Developers Console.
    const oAuth2Client = new OAuth2Client(
      YOUR_CLIENT_ID,
      YOUR_CLIENT_SECRET,
      YOUR_REDIRECT_URL
    );

    // Generate the url that will be used for the consent dialog.
    await oAuth2Client.generateAuthUrl({
      access_type: 'offline',
      scope,
    });
    
    // Verify the integrity of the idToken through the authentication 
    // code and use the user information contained in the token
    const { tokens } = await client.getToken(code);
    const ticket = await client.verifyIdToken({
      idToken: tokens.id_token!,
      audience: keys.web.client_secret,
    });
    idInfo = ticket.getPayload();
    return tokens.refresh_token;
  })
}

At this point, we've refactored the authentication workflow to support Google APIs. However, you haven't asked the user to authorize it yet. Since you also need to grant offline access, you should request additional permissions through your client-side application. Keep in mind that you already need an active session.

const googleOauth = gapi.auth2.getAuthInstance();
const newScope = "https://www.googleapis.com/auth/calendar"

googleOauth = auth2.currentUser.get();
googleOauth.grantOfflineAccess({ scope: newScope }).then(
    function(success){
      console.log(JSON.stringify({ message: "success", value: success }));
    },
    function(fail){
      alert(JSON.stringify({message: "fail", value: fail}));
    });

You're done with the front-end changes and you're only missing one step. To create a Google API's client in the back-end with the googleapis library, you need to use the refresh token from the previous step.

For a complete workflow with a Node.js back-end, you might find my gist helpful.

Rhettrhetta answered 20/6, 2021 at 19:6 Comment(1)
Very nicely written answer with step-by-step clear minimalistic examples and links to more information. Nicely done!Pallbearer
C
1

While authentication (sign in), you need to add "offline" access type (by default online) , so you will get a refresh token which you can use to get access token later without further user consent/authentication. You don't need to grant offline later, but only during signing in by adding the offline access_type. I don't know about platform.js but used "passport" npm module . I have also used "googleapis" npm module/library, this is official by Google.

https://developers.google.com/identity/protocols/oauth2/web-server

https://github.com/googleapis/google-api-nodejs-client

Check this: https://github.com/googleapis/google-api-nodejs-client#generating-an-authentication-url

EDIT: You have a server side & you need to work on behalf of the user. You also want to use Google for signing in. You just need #2 Google Sign-in for server-side apps , why are you considering both #1 & #2 options.

I can think of #2 as the proper way based on your requirements. If you just want to signin, use basic scope such as email & profile (openid connect) to identify the user. And if you want user delegated permission (such as you want to automatically create an event in users calendar), just add the offline access_type during sign in. You can use only signing in for registered users & offline_access for new users.

Above is a single authentication flow.

Comptom answered 1/3, 2021 at 15:24 Comment(6)
I don't think offline access counts as a "scope" from Google's perspective—it's not listed on their scopes page (developers.google.com/identity/protocols/oauth2/scopes), and since the method I'm using (auth2.signIn()) expects space-delimited scopes it won't work as written. (I've tried "offline" and "offline-access" just for fun.) I'm using google-auth-library on the server and api.js on client—not sure how that compares to what you've used. Not against passport if it's easier and safe, though I worry that an auth library lacks proper SSL support on its website...Franek
My mistake, apologies for the same. It's the access type, not the scope . Check the github.com/googleapis/… . It provides refresh token for any of the scopes that Google has, the link that you have provided in the above comment. I just developed one project using offline access, so forgot the details.Comptom
I also didn't use Passport for offline access project. I have used Google provided npm library "googleapis", which actually uses google-auth-library .Comptom
The primary part is generating the authentication url, so Google provides tokens accordingly.Comptom
Edited the answer partially after first comment.Comptom
So unless I'm missing something, the GitHub docs you sent over provide server-side authentication for a Node server, similar to #2 in my original post. It doesn't cover #1 in my original post (browser-based auth/login via Google), nor the main question of how to integrate the two.Franek

© 2022 - 2025 — McMap. All rights reserved.