Seamless API Access with Google Identity Services

What seems like it should the meat-and-potatoes of the API is actually a bit elusive, whether you are new to Google or attempting to migrate deprecated code to GIS.

Blunt Jackson
10 min readJun 23, 2022

Context

After weeks of scrabbling through Google’s documentation on the new Google Identity Services, which will be a mandatory migration early next year, I finally found the holy grail: seamless low-friction authentication-and-authorization to access google APIs.

Here’s my deal: I have a SPWA in mid-stage initial development. The architecture is built entirely around a user’s Google resources, so there doesn’t need to be a “server” — and so the user always has complete control of their own data. Before bringing in my pre-alpha testers, I thought I would migrate to the new authentication architecture.

I found this migration a lot more challenging than I should have — but I take solace in the company of others, there have been several Stack Overflow threads struggling to figure out how to navigate the new service. And so, I present a full solution to my challenge here. (As well as a library if you want to take a shortcut, or use a reference solution.)

Requirements

This is the specific use case we are solving for:

  • For each user, the app needs to know a few things about who they are (name, email, etc.);
  • For each user, the app needs to be able to call Google APIs to access some of their resources;
  • A user should only be asked to authorize the app’s use of their Drive on Sign-Up.
  • A user should be auto-logged in whenever possible; e.g., page refresh events should not require any user clicks to re-connect to their account.

Concepts to Understand

You could cut to the chase, with the solution code that follows, but personally I find it helpful to understand the concepts underlying the new service.

  1. Authentication (who you are) is (almost) completely divorced from authorization (what the app has access to). The documentation calls this a win for users and developers, but I’m not yet convinced. However, these are also both delivered by the same library, so neither the extent of separation nor the particulars of dependency are intuitive.
  2. Sequence is crucial. This is intuitive, but if you play around with the APIs a bit before grasping the nuances of it all, the waters can seem muddier than they are. For example, you can authorize api access without any authentication. But you cannot (automatically) re-establish authorization without knowing who the user is on app load. Thus, a first draft POC implementation can create the illusion that authorization is either truly separate from authentication, or alternatively, more integrated than it actually is. Either way, you can go off the rails.
  3. These are two completely independent libraries, bundled into one. The authorization library relies on the authentication library in sequence, but it is on the developer to facilitate the transfer of data. This is the key insight that took me way too long to resolve: a truly seamless experience requires a very regimented sequence of:
    a. Load the libraries;
    b. Handle all authentication;
    c. Identify the user;
    d. Hint the user email address obtained from authenticaation to the authorization initialization.

Let’s Dive In!

First of all, in making this work for my app, I put together a simple library to “make the easy things easy” — so you can check out a library that puts all these pieces together with npm install gothic — source on github.

Secondly, this article assumes you have already registered your app with Google in order to obtain a client id, api key, and that you know what API’s you want to use, what their scopes and discovery definition strings are.

Step 1: Load the Libraries.

You need to load two libraries — one is GIS, the Google Identity Service library, which is used for authentication and authorization. The other is GAPI, or the Google API, which will be used to actually call Google APIs based on authorization & authentication from GIS.

I have a mild aversion to putting anything in HTML when it comes to a SPWA, so I load these entirely from javascript.

Here’s a self-contained loading method. Note that it will take your client-id as a parameter in order to configure the account.id package, and your api_key and discovery link to configure gapi.

The script elements will load the libraries we need, and by onload or onerr methods, will report that we have succeeded (or failed completely) and we can proceed to use those APIs.

Do also note that in initializing google.account.id we give it a callback function to receive sign-in responses. This function will receive the credentials from Google that identify our user, and help to smooth the way for authorization. This is implemented in Step 3.

Also note that we are configuring the gapi library with our api_key and discovery documents. The api_key is generated when we configure our client with Google, and should not be confused with the client secret, which we never reveal to the browser. The discovery documents tell Google exactly which routes we need added to our gapi library.

Step 2: Decide How We Want to Authenticate

Google offers two ready-made approaches to authentication: the button method and the onetap method.

If I think I may be seeing a user for the first time, I probably want to present an introductory experience. If I believe the user to be a return visitor I want to cut to the chase and log them straight in.

A traditional server based app might use cookies for making this determination, but the SPWA will need to use local storage. By keeping a record of whether this browser instance has visited in the past we can take conditional action.

Step 3: Sign-up / Sign-in

Although there are two ways to sign in, both are going to make use of that on_response callback we referenced in step one. Let’s implement that first.

Things do get a little trickier here than in prior versions of Google’s authentication libraries because the credentials are now returned in the form of a JSON Web Token (JWT). One option for this is the very lightweight library: jwt-decode.

Note that we set a flag on localStorage… now that the user has logged in, we can test that on a future occasion on order to make the decision described in step 2. (You should also unset it when the user logs out or revokes credentials!)

You might notice that this code snippet doesn’t do anything with the user that we loaded, or otherwise signal the app that sign-in is accomplished. Different apps might take different approaches in this regard.

Final note, this code does not validate the JWT. We could use a more sophisticated library or implement the validation ourselves. (The more robust libraries do add some weight to your app: jwt-decode is only 30k, but libraries recommended by jwt.io trend heavier: jose is 550k,jsrsasign is 850k, jose-jwe-jws is 586k.)

Step 3a: The Sign-in Button

Google’s Sign in experience in GIS is quite straightforward. This method sets a few default styling parameters to the renderButton API. Consult the Google reference for the full list of options.

I use a method like so:

Step 3b: The OneTap Flow

The OneTap option is seemingly easier, as there is nothing to configure, but we still need to be prepared for surprises.

The google.accounts.id.prompt method provides the simplest possible experience for our user. If they have previously logged in within some reasonable amount of time, they will be logged in without any taps (because we set auto_select: true when we initialized the library). But if the user has, for example, signed out in another browser, the library will prompt for account selection.

And if that prompt is presented, it can also be dismissed. What do we do then?

That’s where _handle_prompt_events comes in. Now there are a variety of permutations that are possible here (see the reference docs), but the code above seems to cover the important ones.

isSkippedMoment tells us that the user chose not to select an account at the prompt. At this point the app may want to fall back to displaying a button, or suggest alternative sign in options. If a user does skip the account selection dialog, this will be remembered by google for some period of time.

isNotDisplayed and the suppressed_by_user reason are what happens after the user skips the dialog. You can’t just throw the prompt right back at them, naturally. That would be rude! And Google doesn’t want you to be rude. So, if the user skips the dialog, Google will not display the dialog and provide you with this event. Again, time to go to the backup plan.

If the sign-in process is successful, our previous on_response function will take care of business.

(This means, the callback for this function receives failures only, successes will go to on_response)

Step 4: Authorization

So far, so good. But where I really got stuck in the weeds was with authorization. So here goes!

Your app has received the user credentials, parsed the JWT, now all you need to do is authorize use of APIs for your app to function.

Google offers two methods of handling authorization, the token model and the code model. The code model is suitable for apps that have their own servers: it is the traditional model by which Google issues a code that can be used in conjunction with the app’s secret key to obtain authorization and refresh tokens. But it is a massive security breach to put your secret in the browser.

Thus we will implement the token model.

First, let’s tie a couple of pieces together: in step 1 we loaded gapi, configuring it with our api_key and discovery document. Now we request authorization from the user for the scopes we need. This process will automatically update gapi with an active token. This token, by the way is short lived (1 hour). We’re going to need to keep that token_client around to get a new token when our api calls start failing (see step 5).

Note that we are requesting the access token with a configuration of {prompt: ''} — this is not the only option, but it is the most flexible option. To learn more about these options — go to the reference docs. The empty string prompt will not prompt if (A) the user account is known, and (B) the user account has previously granted authorization.

This means our first-time user will have an additional step in the login-process, the step where they authorize our app to call Google api’s for them. This is not new, and necessary, although the sequential pop-ups are more clunky than the prior model.

But also note that in configuring the token client, we provide a hint property, mapped to the user’s email address, which we have previously obtained during sign in. If this is not provided, and the user has more than one Google account, they will be prompted to choose the account, even though they just logged in with one. This caused me consternation for some time! Fortunately, the hint takes care of business.

Of course, this is another occasion when the user doesn’t need to do what we want them to, so if no access_token is available, our api calls will not work. Your app will need to handle that scenario!

5. Retry Logic

Remember that Google does not handle keeping your authorization current within a user session anymore.

This is a new inconvenience for users (and developers) that is apparently required to improve security. Personally, I’d be happier with 4 hour tokens, as that would cover a much higher rate of complete user sessions, and diminish the frequency of interruptions. But for now, it is a one-hour token.

The net effect is that every Google API call can fail with a permission denied error, indicating that your app should re-run the request for a valid token. This may require substantial re-architecture of your API code to cleanly handle the use-case, and will result in an interruption to the user experience every hour or so while the (no-click-required) pop-up window flashes, and the new token is obtained. This only takes a second or two, but remains a demonstrable new “disappointing moment” for users.

(There is an example of managing this in Google’s migration documentation.)

Conclusion

For those who are using Google’s prior generation of authentication APIs for a SPWA context, even once you have worked through the details, the net effect on user experience feels like a small-but-noticeable step backwards. For some apps, it might be enough of an annoyance to give up on the token model entirely, add servers, and get busy setting up refresh tokens, which appears to be the model that Google prefers we use.

The driving concern is security: bad actors have been getting more sophisticated, and Google is always going to be one of their top targets. (As recently as May of this year (2022), Google was forced to address a security flaw in this very area [Reference].) The security issues are not purely academic!

Nonetheless, if you have followed along with this guide you should now have all the tools you need to make the best possible user experience for your app, as well as some architectural directions to go if your user experience needs to be even better!

Good luck out there!

Thanks…

Gratitude to Morfinismo on Stack Overflow for helping me resolve some JWT confusion, and to Rohey Livne at Google for graciously receiving my rants about the new API and connecting me with experts, and to Google expert Brian Daugherty for engaging in a constructive dialog about security, developer solutions, and user experience.

--

--

Blunt Jackson

Building web applications since 1992. Crikey, that’s a long time.