Google OAuth2 for Dummies

Fredy Sandoval - Jun 12 '23 - - Dev Community

The Full code is at the bottom.

TLDR: In this tutorial we will Authenticate an Application using Google OAuth2. It loads credentials from a local JSON file, creates an OAuth2 client with those credentials, and sets up a few HTTP routes to handle the authentication process.

Questions to be answered:

1. How authenticate an app using Google OAuth2 in NodeJS?

You use the googleapis npm package to create an OAuth2 client with your app's credentials. Generate an authorization URL and redirect users there to grant your app permissions. When they're redirected back to your app, exchange the "code" they bring for access and refresh tokens.

2. How do you load credentials from a JSON file in Google APIs?

You can use the fs module's readFileSync function to read the contents of the file. Then, use JSON.parse to parse the contents into a JavaScript object.

3. What are the Google API scopes?

Scopes define the permissions your app is requesting from users. This script requests permission to send Gmail messages on the user's behalf.

PART 1

Creating credentials

a. Go to Google Console > Credentials
https://console.cloud.google.com/apis/credentials

Google APIs & Serices

b. Create a new Credential of type OAuth client ID

Google Oauth

c. Select type Desktop and give any name you want

Google Auth

d. Save the JSON file (client_secret_xxx…) in the root folder of
the project

Google Oauth

IMPORTANT!

You must add the email addresses of the people that will use
the application, including yours. Otherwise that person won’t
be able to use the application.

Google Auth

You also must modify the JSON secret file (the file you just
downloaded "client_secret_xxx…")
At the end of the file you will find “redirect_uris”, you need to
modify it like this:

Google Oauth

PART 2

We're going to break down a piece of code that allows your application to interact with Google APIs using OAuth2 for authentication. This script involves several important Node.js modules like googleapis, fs, path, and express. Let's get started!

First off, install the necessary packages.

npm init -y
npm install express googleapis
Enter fullscreen mode Exit fullscreen mode

now let's import some essential modules:

const { google } = require('googleapis');
const fs = require('fs');
const path = require('path');
Enter fullscreen mode Exit fullscreen mode

google is part of googleapis, which lets us interact with Google's services. fs is a built-in Node.js module to work with the file system, and path helps us with file paths.

Now, we define some constants:

const CREDENTIALS_FOLDER  = './'; // location of credentials
const SCOPES              = ['https://www.googleapis.com/auth/gmail.send'];
Enter fullscreen mode Exit fullscreen mode

CREDENTIALS_FOLDER is the location of our Google API credentials file. SCOPES is an array that specifies what permissions our application requests from users. Here, we're asking to send Gmail messages on behalf of the user.

The next part of the script involves loading our Google API credentials:

let credentials;
try {
  const files = fs.readdirSync(CREDENTIALS_FOLDER);
  const credentialsFile = files
    .find(file => file.startsWith('client_secret_') && file.endsWith('.json'));
  const credentialsPath = path.join(__dirname, CREDENTIALS_FOLDER, credentialsFile);
  const file = fs.readFileSync(credentialsPath, 'utf8');
  credentials = JSON.parse(file);
} catch (error) {
  console.log("unable to read file, can't continue");
}
Enter fullscreen mode Exit fullscreen mode

This script looks for a file that starts with client_secret_ and ends with .json (this is your Google API credentials file). It then reads the file and parses it as JSON. If something goes wrong (like if the file doesn't exist), it logs an error message.

Next, we validate our credentials:
But why? it is very common that we or somebody loads the incorrect credentials type, Google has different credentials, and validating we have the right ones will save hours of debugging.

const REQUIRED_CREDENTIALS_PROPERTIES = [
  'client_id', 'project_id', 'auth_uri', 'auth_provider_x509_cert_url', 'token_uri', 'client_secret', 'redirect_uris'
];
REQUIRED_CREDENTIALS_PROPERTIES.forEach(prop => {
  if (!(prop in credentials.installed)) 
    throw new Error(`Property ${prop} not found in credentials`);
});
Enter fullscreen mode Exit fullscreen mode

We have a list of all the properties that should be in our credentials object, and we make sure that each one is there. If not, we throw an error.

Now we create an OAuth2 client:

const { installed: { client_id, client_secret, redirect_uris } } = credentials;
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);
let authUrl = oAuth2Client.generateAuthUrl({
  access_type: 'offline',
  scope: SCOPES
});
Enter fullscreen mode Exit fullscreen mode

We pull out the necessary parts of our credentials and create an OAuth2 client. We then generate a URL (authUrl) where users will grant our application the permissions we requested.

Next, we set up an Express application:

const express = require('express');
const app = express();
Enter fullscreen mode Exit fullscreen mode

Express is a framework that makes it easier to handle HTTP requests and responses in Node.js.

We define three routes. The first ('/') sends users to the authUrl we generated earlier:

app.get('/', (req, res) => {
  res.send(`<a href="${authUrl}">Authorize</a>`);
});
Enter fullscreen mode Exit fullscreen mode

The second (/oauth2callback) is the "callback URL". After users grant permissions, Google redirects them here with a special "code". We exchange that code for access and refresh tokens, which allow us to make authorized requests on behalf of the user:

app.get('/oauth2callback', async (req, res) => {
  const { code } = req.query;
  const { tokens } = await oAuth2Client.getToken(code);
  oAuth2Client.setCredentials(tokens);
  res.redirect('/authenticated');
});
Enter fullscreen mode Exit fullscreen mode

The third route ('/authenticated') simply confirms that the user has been authenticated:

app.get('/authenticated', (req, res) => {
  res.send('You are authenticated now!');
});
Enter fullscreen mode Exit fullscreen mode

Next, we define two "middleware" functions to handle requests that don't match any routes and to handle errors:

function notFound(req, res, next) {
  res.status(404);
  const error = new Error('Not Found - ' + req.originalUrl);
  next(error);
}
function errorHandler(err, req, res, next) {
  res.status(res.statusCode || 500);
  res.json({
    message: err.message,
    stack: err.stack
  });
}
app.use(notFound);
app.use(errorHandler);
Enter fullscreen mode Exit fullscreen mode

These functions provide helpful error messages to users and developers.

Finally, we make our app listen on a specific port:

var interfaces = require('os').networkInterfaces(), localhostIP;
for (var k in interfaces) {
    for (var k2 in interfaces[k]) {
        let ipFamily = interfaces[k][k2].family;
       if ( ipFamily === 'IPv4' || ipFamily === 4 && !interfaces[k][k2].internal) {
          localhostIP = interfaces[k][k2].address;
       }
   }
}
const port = process.env.PORT || 5000;
app.listen(port, () => {
    console.log(`Listening on http://${localhostIP}:${port}`);
});
Enter fullscreen mode Exit fullscreen mode

This script starts our app listening on the port specified by the environment variable PORT, or 5000 if PORT isn't defined. The server's address is logged to the console when the server starts.

And that's it! You've just learned how to set up a simple OAuth2 authentication flow with Google APIs and Express. It's not so difficult when you break it down, right? Happy coding!

Full code

const { google }          = require('googleapis');
const fs                  = require('fs');
const path                = require('path');
const CREDENTIALS_FOLDER  = './';
const SCOPES              = ['https://www.googleapis.com/auth/gmail.send'];

/* GLOBAL VARIABLES*/
let credentials;


// ----------  OAUTH2 CLIENT ----------------
// Load client secrets into "credentials"
try {
  const files = fs.readdirSync(CREDENTIALS_FOLDER);
  const credentialsFile = files
    .find(file => file.startsWith('client_secret_') && file.endsWith('.json'));

  if (!credentialsFile) throw new Error('credentials file not found');

  // const credentialsPath = CREDENTIALS_FOLDER + credentialsFile;
  const credentialsPath = path.join(__dirname, CREDENTIALS_FOLDER, credentialsFile);
  const file = fs.readFileSync(credentialsPath, 'utf8');
  credentials = JSON.parse(file);
} catch (error) {
  console.log("unable to read file, can't continue");
}

// VALIDATE CREDENTIALS 
const REQUIRED_CREDENTIALS_PROPERTIES = [
  'client_id',
  'project_id',
  'auth_uri',
  'auth_provider_x509_cert_url',
  'token_uri',
  'client_secret',
  'redirect_uris'
];

// Ensure all required properties exist
if (!credentials || !credentials.installed) {
  throw new Error('credentials or installed not found in credentials');
}

REQUIRED_CREDENTIALS_PROPERTIES.forEach(prop => {
  if (!(prop in credentials.installed)) 
    throw new Error(`Property ${prop} not found in credentials`);
});

const { installed: { client_id, client_secret, redirect_uris } } = credentials;
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);

let authUrl = oAuth2Client.generateAuthUrl({
  access_type: 'offline',
  scope: SCOPES
});

// ----------  EXPRESS APPLICATION  ----------------
const express = require('express');
const app     = express();

app.get('/', (req, res) => {
  res.send(`<a href="${authUrl}">Authorize</a>`);
});

app.get('/oauth2callback', async (req, res) => {
  const { code } = req.query;
    const { tokens } = await oAuth2Client.getToken(code);
    oAuth2Client.setCredentials(tokens);
    res.redirect('/authenticated');
});

app.get('/authenticated', (req, res) => {
  res.send('You are authenticated now!');
});
// ----------  EXPRESS FALLBACKS  ----------------
function notFound(req, res, next) {
  res.status(404);
  const error = new Error('Not Found - ' + req.originalUrl);
  next(error);
}
function errorHandler(err, req, res, next) {
  res.status(res.statusCode || 500);
  res.json({
    message: err.message,
    stack: err.stack
  });
}
app.use(notFound);
app.use(errorHandler);

// gets the localhost IP address
var interfaces = require('os').networkInterfaces(), localhostIP;
for (var k in interfaces) {
    for (var k2 in interfaces[k]) {
        let ipFamily = interfaces[k][k2].family;
       if ( ipFamily === 'IPv4' || ipFamily === 4 && !interfaces[k][k2].internal) {
          localhostIP = interfaces[k][k2].address;
       }
   }
}

const port = process.env.PORT || 5000;

app.listen(port, () => {
    console.log(`Listening on http://${localhostIP}:${port}`);
});
Enter fullscreen mode Exit fullscreen mode
. . . . . . . . . . .