How to use Google APIs with OAuth2.0 in your web app

Arjun H
9 min readSep 10, 2021

A month ago, I decided to play with Google APIs. I thought this would be a fun way for me to learn APIs, OAuth2.0, authentication, etc. Let me just say, the journey was not so pleasant as I hoped. It felt more like a treasure hunt, not that there was any lack of documentation, but the opposite, they were all scattered pretty well, which can confuse a lot of first-time users. Therefore, this tutorial is my attempt at giving you a straightforward solution.

The methods mentioned below works with any front-end framework. This guide includes a Plain JavaScript example (from the official JavaScript quick-start tutorial) and (for typescript) an Angular example. This guide makes use of the Google JavaScript client library (google-api-javascript-client).

Alternatively, you can just copy the sample that has been provided at the end and use this post to learn what each function does.

image representing frameworks using google API.

Note:

  1. If you just want to add Sign In functionality to your application, then use Google Identity Services.
  2. The client and auth2 library will become obsolete in favor of the new Google Identity service. However, there has not been a replacement for the client library that I am aware of yet. Learn more out about it here: https://stackoverflow.com/questions/68815693/how-to-use-scoped-apis-with-gsi-google-identity-services.
  3. If you don’t want access to the user’s sensitive scopes, then OAuth2.0 is not necessary, and that means, you just need to follow steps one and two, also in the second step you do not need to specify the client Id. Check out this sample: github.com/google/google-api-javascript-client/blob/master/docs/samples.md#LoadinganAPIandMakingaRequest
  4. You can also use firebase authentication with just a few extra steps, but this tutorial focuses on just the client & auth2 library.

Prerequisites:

  1. You should already have a project with your desired API enabled in your google developer console. Also, be sure to create an OAuth2.0 clientId in the credentials tab and remember to set up the OAuth consent screen. Check the official doc: https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow.
  2. Also, check this readme for the google-api javascript client library: google-api-javascript-client/blob/master/docs/start.md
  3. Copy the clientId and/or to an environment file. In Angular, add it to both the environment files, in other frameworks, it may involve installing an npm package. Remember to add it to the .gitignore file too.
  4. For typescript support, you may need to install types/gapi. It did not display auto-completion in my project, but according to a discussion from stack overflow, some errors may be prevented with it.

The flow:

  1. Load the client and auth2 libraries.
  2. Call the init method.
  3. Sign In the user.
  4. Google prompts the user for login and consent to the requested scopes.
  5. If approved, the auth2 library obtains the authorization token.
  6. This token is used to call the google api.
  7. Refresh the token every 45 mins.

1. Load the client and auth2 libraries

First, include this in your index.html file.

<script async defer src="https://apis.google.com/js/api.js"  </script>

Now, decide where you want to put the client library logic in. For example, in a small web app with just one .js file, you can load the library there itself or create a separate file for it. In Angular, you can create a service file like authService or gapiService. You can do it in any way, but it should be accessible to all the components that need it.

In JavaScript, add the following code inside a function (like a gapiInit() function), usually, it is invoked on page load.

gapi.load('client:auth2', ()=>{// callback function});

the “:” is used to load more than one library. Here it is used to load both the client library and auth2 library.

In Typescript:

To use a JavaScript library in a typescript file, just below your import statements add declare var gapi: any as shown;

import { Injectable } from ‘@angular/core’;
import { environment } from 'src/environments/environment';
declare var gapi: any;

This is because the client library is a JavaScript library with no shape. So we declare gapi as a variable gapi of type any.

Now, like in the js example create a function and add the following inside:

async gapiInit()
{
gapi.load('client:auth2', () => {// callback function});
}

2. Call the init method

The init method starts the flow by requesting the google servers for the Auth token. The init method requires an API key and/or clientId, discovery docs, and the scopes.

API key and client Id can be obtained from your google project console. Check the prerequisites.

Discovery docs are just an array of strings that specifies to the library which API methods to load. For example, for the drive API the discovery doc is:

var DISCOVERY_DOCS = ["https://www.googleapis.com/discovery/v1/apis/drive/v3/rest"];

Or in ts,

//"private" before "DISCOVERY_DOCS" is optional, but it is good practice to use it //wherever applicable.private DISCOVERY_DOCS = ['https://www.googleapis.com/discovery/v1/apis/drive/v3/rest'];

To know more about them, go to https://github.com/google/google-api-javascript-client/blob/master/docs/discovery.md.

A scope is just a string specifying the resources your application can access on the user’s behalf. You can access more than one scope by separating them with spaces. The scopes you request will appear on the user’s consent screen.

It is recommended to request access to scopes as you need it rather than asking for it all at once. By doing this, the user will more easily understand why your application needs the access it is requesting.

var SCOPES = 'https://www.googleapis.com/auth/drive.metadata.readonly';

for ts,

//"private" before "SCOPES" is optional
private SCOPES = 'https://www.googleapis.com/auth/drive';

Now let’s implement the init method,

function initClient() {
gapi.client.init({
apiKey: API_KEY,
clientId: CLIENT_ID,
discoveryDocs: DISCOVERY_DOCS,
scope: SCOPES
})
}

In Angular,

initClient() {
gapi.client.init({
apiKey: environment.API_KEY,
clientId: environment.GAPI_CLIENT_ID,// client id is stored in
discoveryDocs: this.DISCOVERY_DOCS, // an environment file
scope: this.SCOPES,})
}

Note:

  • The initClient() method should be called only after both the client and auth2 libraries have been loaded. So, add this function as the callback function in gapi.load() function from the previous step.

It should be “scope” and not “scopes” in the gapi.client.init method.

  • The gapi.client.init() is an asynchronous function and any method from the client and auth2 libraries can only be called after the gapi.client.init() method has been resolved.

3. Sign in the user

Use the following function as a callback to an event like clicking the Sign In button.

gapi.auth2.getAuthInstance().signIn();

gapi.auth2.getAuthInstance() returns a Google Auth object. This object is used to call the sign-in method above.

The Google Auth (gapi.auth2.getAuthInstance()) object has some more functions in it like:

gapi.auth2.getAuthInstance().signOut(); //To Sign Out
gapi.auth2.getAuthInstance().isSignedIn.get()
// to check the sign-in status
gapi.auth2.getAuthInstance().isSignedIn.listen()// to listen for sign-in status changes

for all the methods in the auth2 library and their return types, check out: https://developers.google.com/identity/sign-in/web/reference.

example,

gapi.client.init({
apiKey: API_KEY,
clientId: CLIENT_ID,
discoveryDocs: DISCOVERY_DOCS,
scope: SCOPES
}).then(function () {
gapi.auth2.getAuthInstance().isSignedIn.listen(updateSigninStatus);
//the listen function returns a boolean which is passed to another //function updateSigninStatus()
}
updateSigninStatus(isLoggedIn){
if(isLoggedIn){
//you can disable the sign-in button(if any) because the user has //already logged in.
// you can access your api library from here because the user is //logged in
}
else{
//you can disable the sign-out button here because the user already //clicked sign-out}
}

For ts, just add the types,

gapi.client.init({
apiKey: environment.API_KEY,
clientId: environment.GAPI_CLIENT_ID,
discoveryDocs: DISCOVERY_DOCS,
scope: SCOPES
})
.then(function () { gapi.auth2.getAuthInstance().isSignedIn.listen(updateSigninStatus);
//the listen function returns a boolean which is passed to another //function updateSigninStatus()
}
updateSigninStatus(isLoggedIn:boolean){
if(isLoggedIn){
//you can disable the sign-in button(if any) because the user has //already logged in.
// you can access your api library from here because the user is //logged in
}
else{
//you can disable the sign-out button here because the user already //clicked sign-out}
}

4. Google prompts the user for login and consent to the requested scopes

The consent screen only appears the first time the user signs in. Once permission is given for a scope the consent screen will not appear again unless the user or the app revokes the permission.

5. If approved, the auth2 library obtains the authorization token

On successful authorization, the auth2 library obtains an authorization token. The auth2 library handles the token for you, but if you need to access it you can with the following method:

GoogleUser.getAuthResponse().access_token;
//returns a string

GoogleUser is the object returned by GoogleAuth.currentUser.get() method.

6. Call the API methods

If the user has given consent to the requested scopes and has successfully signed in, the application can start making API requests.

APIs in the gapi client library follow the format of:

gapi.client.(API name).(collection).(method)

where:

  • API name: is the name of your API.
  • collection: Go to your API’s documentation=> click References => Find under which collection your desired method lies. This is the collection name.
  • method: Methods are found in the references tab in the documentation.

for example, for the drive API, to get the list of all the files in a user’s drive the method will be:

gapi.client.drive.files.list()

The list of methods, parameters and return type can be found in the reference page of your API’s documentation.

Here is a practical example of accessing a user’s spreadsheet files from the drive and logging the response.

getSheetsFromDrive(){
gapi.client.drive.files.list({
fields: 'files(name, id, thumbnailLink)',
corpora: 'user',
q: "mimeType='application/vnd.google-apps.spreadsheet'", supportsAllDrives: false,
}).then((response: any) => {
const files = response.result.files;
if (files) {
console.log(response);}
else {
console.log('no files');}
});
}

7. Refresh the token

The access token obtained expires after 45 minutes. To have continued access to the user’s resource for more than 45 minutes, the token should be reloaded every 40 minutes or less.

First, set up a function to run every 30minutes:

For Javascript,

const refreshTime= 1800*1000;
setInterval(()=>{
//refresh the token here
},refreshTime);

For Angular,

interval(1800 * 1000).pipe(     
startWith(5000) ).subscribe(()=>{
//refresh the token here
});

The token can be refreshed by calling the GoogleUser.reloadAuthResponse().

Sample

Plain Js Sample,

// Client ID and API key from the Developer Console
var CLIENT_ID = '<YOUR_CLIENT_ID>';
var API_KEY = '<YOUR_API_KEY>';
var DISCOVERY_DOCS = ["https://www.googleapis.com/discovery/v1/apis/drive/v3/rest"];

var SCOPES = 'https://www.googleapis.com/auth/drive.metadata.readonly';

function handleClientLoad() {
gapi.load('client:auth2', initClient);
}



function initClient() {
gapi.client.init({
apiKey: API_KEY,
clientId: CLIENT_ID,
discoveryDocs: DISCOVERY_DOCS,
scope: SCOPES
}).then(function () {
// Listen for sign-in state changes. gapi.auth2.getAuthInstance().isSignedIn.listen(updateSigninStatus);

// Handle the initial sign-in state. updateSigninStatus(gapi.auth2.getAuthInstance().isSignedIn.get());

}, function(error) {
appendPre(JSON.stringify(error, null, 2));
});
}



function updateSigninStatus(isLoggedIn:boolean){
if(isLoggedIn){
//* you can disable the sign-in button(if any) because the user has //already logged in.
//* you can access your api library from here because the user has //logged in

console.log('logged in');
getSheetsFromDrive();
}
else{
//you can disable the sign-out button here because the user already //clicked sign-out
}}


// Sign in the user upon button click.
function signIn() {
gapi.auth2.getAuthInstance().signIn();
}



// Sign out the user upon button click.
function signOut() {
gapi.auth2.getAuthInstance().signOut();
}


//get only spreadSheets from drive
function getSheetsFromDrive() {
if (!gapi.auth2.getAuthInstance().isSignedIn.get())
{
signIn();
return; }
//gapi method formats: gapi.client.api.collection.method
gapi.client.drive.files.list({
fields: 'files(name, id, thumbnailLink)',
corpora: 'user',
q: "mimeType='application/vnd.google-apps.spreadsheet'",
supportsAllDrives: false,
}).then((response: any) => {
const files = response.result.files;
if (files) {
console.log(response);}
else {
console.log('no files');}
});

}

Angular Sample,

import { Injectable } from '@angular/core';
import { environment } from 'src/environments/environment';


declare var gapi: any;

@Injectable({
providedIn: 'root',
})
export class GapiService {
private DISCOVERY_DOCS = [
'https://www.googleapis.com/discovery/v1/apis/drive/v3/rest',
];
private SCOPES = 'https://www.googleapis.com/auth/drive';
constructor() {
// you can add an interval to refresh the access_token every 30
// minutes here if you want or add it wherever you need it.
}

async gapiInit() {
gapi.load('client:auth2', initClient);
}
initClient(){

gapi.client.init({
// Client ID and API key from the Developer Console
apiKey: environment.API_KEY,
clientId: environment.GAPI_CLIENT_ID,
discoveryDocs: this.DISCOVERY_DOCS,
scope: this.SCOPES,

}).then(() => {

try {
console.log('loaded client');
gapi.auth2.getAuthInstance()
.isSignedIn.listen(this
.updateSigninStatus);
this.signIn();
} catch (e) {
console.log(e);}
});
}
//get only spreadSheets from drive
getSheetsFromDrive(){
if (!gapi.auth2.getAuthInstance().isSignedIn.get())
{
this.signIn();
return; }
//gapi method formats: gapi.client.api.collection.method
gapi.client.drive.files.list({
fields: 'files(name, id, thumbnailLink)',
corpora: 'user',
q: "mimeType='application/vnd.google-apps.spreadsheet'",
supportsAllDrives: false,
}).then((response: any) => {
const files = response.result.files;
if (files) {
console.log(response);}
else {
console.log('no files');}
});
}
//authentication functions
async updateSigninStatus(isLoggedIn:boolean){
if(isLoggedIn){
//* you can disable the sign-in button(if any) because the user has //already logged in.
//* you can access your api library from here because the user has //logged in

console.log('logged in');
await this.getSheetsFromDrive();
}
else{
//you can disable the sign-out button here because the user already //clicked sign-out}
}


private async signIn() {
if (gapi.auth2.getAuthInstance().isSignedIn.get()) return;
gapi.auth2.getAuthInstance().signIn()
.then(() => {
console.log('signed in');
})
}


private async signOut() {
gapi.auth2.getAuthInstance().signOut()
.then(() => {
console.log('signed Out');
});}
}

Common Mistakes:

  • Not providing proper JavaScript origins in the project console.
  • Disabling third-party cookies: gapi makes use of cookies. Therefore, do not disable third-party cookies in your browser.

References:

  1. Javascript Client: https://github.com/google/google-api-javascript-client/tree/master/docs
  2. Google OAuth2.0: https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow?hl=ro-MD
  3. Google SignIn Reference: https://developers.google.com/identity/sign-in/web/reference#googleuserreloadauthresponse
  4. Drive API method used in the example: https://developers.google.com/drive/api/v3/reference/files/list

--

--

Arjun H

I am a frontend programmer, and I like sharing things that I find cool and worth exploring as soon as i learn about it.