Skip to content

Google Authentication & Authorization via Oauth in 2022

Posted on:April 29, 2022 at 03:33 PM
Google Authentication & Authorization via Oauth in 2022

Overview

I feel like I’ve had the misfortune of re-learning this topic over and over so I’ll document the process this time. Besides, Google is discontinuing Google Sign-In:

We are discontinuing the Google Sign-In JavaScript Platform Library for web. Beginning April 30th, 2022 new web applications must use the Google Identity Services library, existing web apps may continue using the Platform Library until the March 31, 2023 deprecation date. For authentication and user sign-in, use the new Google Identity Services SDKs for both Web and Android instead.

So I have to do a new implementation anyway. The newer library is called ”Sign In With Google.” I guess if you’ve got to rename it, why not just change the order of the words. Done.

Problem

Ok, why the change? First, they slightly improved security, lower friction between sign-in and sign-up, and have a more consistent experience across the web. The later likely has to do with Chrome browser sessions, and also the ability to notice that google users are already signed into Google, when they happen to been on other websites, and leverage that active session.

Another reason is to explicitly separate the to A’s. Authentication, basically allowing a user to say who they are for sign-in/sign-up, and Authorization, which allows users access to Google Services like Drive, Calendar, etc via OAUTH2 tokens.

Previously my team didn’t use the Google libraries directly, but relied on Passport for authentication, and direct REST calls for authorization and API access. Now though, we are moving to using Google’s provided web javascript client, and their Nodejs library.

If you happen to be using apis.google.com/js/api.js, apis.google.com/js/client.js, or apis.google.com/js/platform.js, it’s time to update to the newer, Google Identity Services

Gotchas

In this post, I will not repeat what you can find on Googles A&A pages, however I will link to them, and I will also explain a few gotchas which were not clear and I only solved via online searches and tracing through the open source Google’s API for Nodejs code.

Namely:

My particular use case is to leverage Google for user sign in, and also to use a Google API to read and write Calendar events. With that said, this post can be applied to using any other Google services, besides Calendar, like Maps, Drive, Gmail, etc. At the end, you will understand how to show a login button which could also reflect an existing Google Session, and upon login, check for user tokens, show a consent window if necessary, and refresh an expired access token.

Authentication

Ok, rather than writing your own user authentication management system, you’ve decided to use Google to handle that.

If you only need to use Google for Authentication, you should follow this guide:

https://developers.google.com/identity/gsi/web/guides/overview

As mentioned above, start by creating a project and get a ClientID. You’ll also find the client button code generator helpful. The advantage in leveraging the google client JS library, is that the button will be dynamic, rendered with existing Google session information else a generic button.

Follow the google instructions to create a projectHere is some example code, to keep things simple, and readable, i’m including JS in the html snippet:

<div
  class="g\_id\_signin"
  data-type="standard"
  data-shape="pill"
  data-theme="outline"
  data-text="signin\_with"
  data-size="medium"
  data-logo\_alignment="left"
></div>
<button id="oauthBtn">Login with Google Account</button>

Basically, that is a synchronous script to get the Google Sign In client library, then I initialize the client using the client ID, scope(s), the ux_mode, and the redirect_url which should match the url you gave when registering your project.

Once Google does the authentication, you will want google to post information to your backend webserver. It will come in the form of a POST request and the body will contain a JWT which contains the logged in users information.

...
async function authCB(req, resp) {
  if (req.query.error) {
    return resp.send(req.query.error);
  }
  const { tokens } = await oauth2Client.getToken(req.query.code);
  const userInfo = (
    await oauth2Client.verifyIdToken({
      idToken: tokens.id\_token,
      audience: config.google.clientID,
    })
  ).payload;

  if (!UserIndex.email\[userInfo.email\]) {
    const newUser = {
      name: userInfo.name,
      email: userInfo.email,
      sub\_id: userInfo.sub,
      picture: userInfo.picture,
      r\_token: tokens.refresh\_token,
    };
...

Besides decoding the JWT, we check if we have the current email in our database, and if not, we create a new record.

Authorization

We can do the Authorization in one of two ways. We can trigger it with a button click on a html page:

...
<script src="https://accounts.google.com/gsi/client"></script>
<script>
  let client;
  function initClient() {
    client = google.accounts.oauth2.initCodeClient({
      client\_id: 'xxxx-xxxxx.apps.googleusercontent.com',
      scope:
        'https://www.googleapis.com/auth/userinfo.profile \\
       https://www.googleapis.com/auth/userinfo.email'
      ux\_mode: 'redirect',
      redirect\_uri: 'http://localhost:5000/oauth2callback',
    });
  }
  // Request an access token
  function getAuthCode() {
    client.requestCode();
  }
  initClient();
  document.getElementById('oauthBtn').addEventListener('click', getAuthCode);
</script>
...

The client code is similar to the the Authentication code above, however unlike using data attributes in the sign in button, you’ll be setting parameters in javascript, for example you’ll include a “scopes” array. A difference on the server-side callback request is that it is a GET operation, and unlike getting user profile information (like email, full name, etc), you’ll need to specifically request ’https://www.googleapis.com/auth/userinfo.profile’ and ’https://www.googleapis.com/auth/userinfo.email’ scopes.

Or we can trigger the code in our initial sign-in server-side callback. After doing the sign-in, we check if we have a refresh token already for that user. If so we use that, if not, they are a new user, we authorize them by generating a URL and sending it to the client as a redirect (gotcha 2, solved):

 ...
  // check for refresh token
  if (!user.refresh_token) {
    // get oauth2 tokens
    const url = oauth2Client.generateAuthUrl({
      access_type: 'offline', // include refresh token in response
      scope: 'https://www.googleapis.com/auth/calendar.readonly https://www.googleapis.com/auth/calendar.events',
      login_hint: user.sub, // use previously authenticated user, don't reprompt to select user
      prompt: user.refresh_token ? 'none' : 'consent', // consent screen to force a new refresh token if necessary
    });
    return resp.redirect(url);

https://cloud.google.com/nodejs/docs/reference/google-auth-library/latest/google-auth-library/generateauthurlopts

This is useful when linked with the client initiated sign-in Authentication button above. To link the two, notice the “login_hint” parameter, this prevents account selection after having already done that previously. (gotta 1, solved)

With either the html/javascript or backend redirect, you’ll need a backend endpoint to process the auth from Google. It will be a GET request and will contain a query parameter called code, which you will use to get your access token, refresh token (assuming you requested offline access), and user info via id_token. To decode the id_token which is in JWT format, you can call the verifyIdToken method of the oauth2Client object.

Here’s a look how to handle the callback:

...
async function authorizeCB(req, resp) {
  if (req.query.error) {
    return resp.send(req.query.error);
  }
  const { tokens } = await oauth2Client.getToken(req.query.code);
  const user = indexBy.id[req.session.id];
  user.refresh_token = tokens.refresh_token;
  const result = await userCol
    .updateOne({ _id: ObjectId(req.session.id) }, { $set: { refresh_token: user.refresh_token } })
    .catch((err) => {
      console.log(err);
    });
  indexBy.id[req.session.id].access_token = tokens.access_token;
  indexBy.id[req.session.id].expiry_date = tokens.expiry_date;
  return resp.redirect('/home.html');
...

Handling Expired Access Tokens

If you are missing the access_token, or it is expired (only have a 60min life), a request is automatically made by the google library in the preflight to get a valid access_token (gotcha 3):


// oauth2client.js .... async refreshTokenNoCache(refreshToken) { if
(!refreshToken) { throw new Error('No refresh token is set.'); } const url = OAuth2Client.GOOGLE_OAUTH2_TOKEN_URL\_;
const data = { refresh_token: refreshToken, client_id: this.\_clientId, client_secret: this.\_clientSecret,
grant_type: 'refresh_token', }; // request for new token const res = await this.transporter.request({ method: 'POST',
url, data: querystring.stringify(data), headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, }); const
tokens = res.data;

Ok, so even without an access token, or with an expired one, the api calls will succeed. The last gotcha, was how to record the updated access token and expiration if they were automatically refreshed. Well after you make your api request, you will still have access to your oauth2Client, and you will find that the credentials property contains a new access_token and expiry_date (gotcha 4, solved), which was updated from the code above.

Update: You can attach an event listener to the oauth2Client object. Once a refresh of the access token happens, it will emit a ‘tokens’ event. To keep things in a single place, I’d recommend relying on this rather then doing multiple checks for the credentials property on api calls as I previously outlined above.

You can set the handler like this:

oauth2Client.on("tokens", tokens => {
  const { email } = JSON.parse(atob(tokens.id_token.split(".")[1])); //decode JWT and destructure email property
  indexBy.email[email].access_token = tokens.access_token;
  indexBy.email[email].expiry_date = tokens.expiry_date;
});

Since it is in a separate request callback, I don’t have access to the original user id, however the tokens returned include the id_token, which is a JWT that I can decode and lookup which user the associated access_token and expiration date applies to.

More information from Google

https://developers.google.com/identity/oauth2/web/guides/how-user-authz-works

https://developers.google.com/identity/protocols/oauth2/openid-connect

https://developers.google.com/identity/oauth2/web/guides/use-token-model

While doing testing, you’ll might need to sometimes remove granted access to your app, you can do that here:

https://myaccount.google.com/permissions