OAuth 2.0 implementation in Node.js

OAuth 2.0 implementation in Node.js

Today, so many applications’ functionality requires accessing resources hosted on other applications or platforms on behalf of a user, given the user’s consent or approval.

These applications could be managing resources on another platform, such as a scheduling or social media management application that requires access to a user’s Google Calendar or Facebook account. OAuth 2.0 makes it possible to achieve such functionality.

In this tutorial, we’ll be exploring OAuth 2.0 by implementing Google Sign-In in NodeJS Express project.

The same process or principle applies to other OAuth 2.0 providers, so feel free to adapt the steps to an OAuth 2.0 provider of your choice.

What is OAuth 2.0

OAuth  2.0 is a standard that allows an application to access resources hosted by another platform on behalf of the user. It’s an acronym for Open Authorization.

The user (i.e. resource owner) must provide authorization to the application before it can access the user’s resources on another platform.

The granting of consent to access resources usually happens first. OAuth2.0 includes a mechanism (i.e. scope) to control the type of resources and actions the application can perform.

OAuth 2.0 is an Authorization Protocol and NOT an Authentication Protocol. In other words, it’s mainly concerned with access and not user identification. However, because it provides access to a set of resources, specifically user data, the protocol was further developed to create another protocol called OpenID Connect (OIDC).

OIDC is the authentication protocol built by adding an identity layer on top of OAuth2.0 and is the protocol that is often used to implement user authentication in applications. The addition of an identity layer on top of the OAuth2.0 framework is the reason why we have “Sign in with X” or “Sign up with X” ubiquitous.

The cornerstone or essential framework of third-party integration is OAuth 2.0. Its application cases fall under User Authentication (implemented using OIDC) and Access to data via APIs.

The importance of OAuth 2.0 becomes evident when you consider a scenario where your scheduling software couldn’t connect with your Google Calendars.

Think of all the usernames and passwords you would have needed to commit to memory if it weren’t for Google Sign-in or other sign-in services offered by platforms.

Let’s dive in with the implementation!

Implementing Login with Google

The first step in implementing OAuth 2.0 is obtaining credentials for the client i.e. Client ID and Secrets from the authorization server. In this tutorial, the client is your application and the authorization server is Google.

Step 1: Creating a Project on the Google Cloud Platform

Go to the Google Cloud console.

Click on select project on the top left of the screen and select New project to create a new project.

Step 2: Create credentials to access your enabled APIs

Click on credentials on the left navigation menu

Click on Create credentials, select OAuth client id from the dropdown options

Select the application type as web
Enter “http://localhost:8000” as authorized origin and “http://localhost:8000/google/callback” as authorized redirect URLs. These URLs would be created on our server.
Click on Create to obtain the client ID and client secrets
Copy and keep the credentials generated. They will be used in the next section.

Step 3: Configuring the OAuth2.0 Consent screen

Click on API & services on the left navigation menu and select the OAuth consent screen.

Select the app type – internal or external, then click on the Create button

Enter the app name and app logo. It’s important to know that these are shown to the user when asking for consent.

Select the Scopes. In the image above, I have selected three scopes which include “userinfo.email”, “userinfo.profile”, “openid”. It’s important to note that OpenID Connect requests MUST contain the openid scope value.

If the openid scope value is not present, an id_token will not be returned.
Scopes are permissions you request users to authorize for your application. They determine the resources and actions the client app can perform on behalf of the user.

Add the email address to test OAuth during development.

Setting Up Express Project

# bash

npm init

npm i -D nodemon

npm i express dotenv node-fetch

Update package.json so npm start command uses nodemon and change js files to ES modules.

type: module,

main: index.js,

scripts: {

start: nodemon app.js

},

Start a  basic server with two URL Paths using the code snippet below

import * as dotenv from dotenv;

dotenv.config();

import express from express;

import fetch from node-fetch;

const app = express();

app.use(express.json());

app.get(/, async (req, res) => {
res.send(Sign in with Google);
});

app.get(/google/callback, async (req, res) => {

res.send(Google OAuth Callback Url!);

});

const PORT = process.env.PORT || 3000;

const start = async (port) => {
app.listen(port, () => {
console.log(`Server running on port: http://localhost:${port}`);
});
};

start(PORT);

Create a  .env file and add the following to it. Update the client ID and client secret with the credentials obtained in the previous section.

PORT=8000

GOOGLE_CLIENT_ID=

GOOGLE_CLIENT_SECRET=

GOOGLE_OAUTH_URL=https://accounts.google.com/o/oauth2/v2/auth

GOOGLE_ACCESS_TOKEN_URL=https://oauth2.googleapis.com/token

GOOGLE_TOKEN_INFO_URL=https://oauth2.googleapis.com/tokeninfo

Before we get started with the implementation of Google OAuth 2.0, I want you to know that there are npm packages (like Passport) that simplify or handle that but I have refrained from using one of those packages in this tutorial so you can understand the flow of OAuth 2.0 and how it works.

STEP 1: Redirect the user to the Google OAuth Consent Screen

Typically, this is the first step in the OAuth process. When a user clicks on the Sign-in with Google button, the user will be taken to the Google OAuth server for the client to request the user’s authorization. Let’s do that in our code.

Update the app.js file.

const GOOGLE_OAUTH_URL = process.env.GOOGLE_OAUTH_URL;

const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;

const GOOGLE_CALLBACK_URL = http%3A//localhost:8000/google/callback;

const GOOGLE_OAUTH_SCOPES = [

https%3A//www.googleapis.com/auth/userinfo.email,

https%3A//www.googleapis.com/auth/userinfo.profile,

];

app.get(/, async (req, res) => {
const state = some_state;
const scopes = GOOGLE_OAUTH_SCOPES.join( );
const GOOGLE_OAUTH_CONSENT_SCREEN_URL = `${GOOGLE_OAUTH_URL}?client_id=${GOOGLE_CLIENT_ID}&redirect_uri=${GOOGLE_CALLBACK_URL}&access_type=offline&response_type=code&state=${state}&scope=${scopes}`;
res.redirect(GOOGLE_OAUTH_CONSENT_SCREEN_URL);
});

In the above code, we constructed the URL for the consent screen by setting the following parameters:

client_id: The client ID for your application.
redirect_uri: The URL the authorization server redirects the user after the user completes the authorization request. The value must match one of the authorized redirect URIs for the OAuth 2.0 client, which you configured while creating credentials.
response_type: Determines whether the Google OAuth 2.0 endpoint returns an authorization code. We have set the value as “code” since we’re doing the Authorization Code flow.
state: This is a value passed to the request that must be included in the response of the OAuth 2.0 server response.
Scope: This is the list of scopes that you need to request to access Google APIs.
access_type: Determines whether a refresh token will be returned alongside an access token for offline use during the initial exchange of an authorization code for tokens. It’s noteworthy that Google has slightly diverged from the OpenID specification concerning offline access. In OpenID Connect, there exists a scope value named “offline_access” specifically designed to request offline access. However, Google follows the approach used here, and for other authorization servers, they might explicitly request you to include the scope “offline_access” in your overall scope value, as detailed in their documentation.

And then redirects the user to the new Google OAuth consent page.

Google OAuth consent page will prompt the user for consent on the permissions needed by the client application as requested in the scope value.

If you’re wondering why certain information, like your language preference or profile picture, appeared without us asking for it, it’s because of how OpenID works.

Even though we only asked for your email and basic profile info, OpenID has its own set of rules called scope claims. These scope claims cover more than what we specifically ask for.

For example, if we ask for your email, OpenID Providers are supposed to give us both your email and let us know if it’s verified. And when we ask for your profile, they should give us a whole bunch of details like your name, nickname, picture, and more.

It appears that Google may not have precisely adhered to these rules. They call the email and profile scope “userinfo.email” and “userinfo.profile” instead of just “email” and “profile” like they’re supposed to.

It’s a small deviation, but it’s worth noting because other services might follow the rules correctly. So, if you’re not using Google, make sure to request “openid email profile” (or check the provider documentation) to get the same kind of information.

STEP 2: Handle the OAuth 2.0 server response

Update the app.js file

const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;

const GOOGLE_ACCESS_TOKEN_URL = process.env.GOOGLE_ACCESS_TOKEN_URL;

app.get(/google/callback, async (req, res) => {
console.log(req.query);

const { code } = req.query;

const data = {
code,

client_id: GOOGLE_CLIENT_ID,

client_secret: GOOGLE_CLIENT_SECRET,

redirect_uri: http://localhost:8000/google/callback,

grant_type: authorization_code,
};

console.log(data);

// exchange authorization code for access token & id_token

const response = await fetch(GOOGLE_ACCESS_TOKEN_URL, {
method: POST,

body: JSON.stringify(data),
});

const access_token_data = await response.json();
});

This is the controller for the redirect URL specified while constructing the authorization request URL. The logic in this controller is to handle the server response from the Google OAuth server.

So this is what happens when a user completes the OAuth consent screen (i.e. authorization request). The Google OAuth server will redirect the user to the redirect URL in both the success and error cases.

What distinguishes the success and error case is the name of the query parameter with which the redirect URL is called. Hence, why we are extracting the query parameters from the URL req.query.

An authorization code response:

http://localhost:8000/google/callback?state=some_state&code=4%2F0AfJohXkceFxp-XI0AbtY4A0wxbTxIOVqQel7-axZ1G5XdPFLID5ldMX2gXkyNfy1uPBUHg&scope=email+profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+openid&authuser=0&prompt=none

We exchange the code for an access and ID token by making a post request to Google’s access token endpoint.

STEP 3: Verify and extract information in the Google ID token

Update the “google/callback”  controller in the app.js file

app.get(/google/callback, async (req, res) => {

const { id_token } = access_token_data;

console.log(id_token);

// verify and extract the information in the id token

const token_info_response = await fetch(
`${process.env.GOOGLE_TOKEN_INFO_URL}?id_token=${id_token}`
);
res.status(token_info_response.status).json(await token_info_response.json());
});

Extract the id token returned from the access token endpoint
Make a GET request to the token info endpoint to verify the token’s validity and obtain the user’s profile information.

Note: Google advises against calling the tokeninfo validation endpoint to validate a token in a production environment. Google recommends using a Google API client library for your platform, or a general-purpose JWT library. You can read this to learn more

STEP 4: Use the information in the Google ID token to manage user authentication

To manage user auth we need to create a user account – identification. So, let’s install mongoose and jsonwebtoken to handle JWT authentication – an alternative to creating a user session when a user logs in

Update the .env file with the following

MONGO_DB_URI=

JWT_SECRET=secret

JWT_LIFETIME=2d

Set up user schema and create the user model

import mongoose from mongoose;

import jwt from jsonwebtoken;

const mongoDBURI = process.env.MONGO_DB_URI;

mongoose.connect(mongoDBURI);

const UserSchema = new mongoose.Schema({
name: {
type: String,
unique: true,
trim: true,
required: [true, Please provide a name],
minlength: 3,
maxlength: 56,
},
email: {
type: String,
match: [
/^(([^<>()[]\.,;:s@”]+(.[^<>()[]\.,;:s@”]+)*)|(“.+”))@(([[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}])|(([a-zA-Z0-9]+.)+[a-zA-Z]{2,}))$/,
Please provide a valid email.,
],
unique: true,
},
password: {
type: String,
minlength: 6,
required: false,
},
});

UserSchema.methods.generateToken = function () {
const token = jwt.sign({ id: this._id }, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_LIFETIME,
});
return token;
};

const User = mongoose.model(User, UserSchema);

Update the logic in the Google callback controller to create a user if the user does not exist and return the JWT token

app.get(/google/callback, async (req, res) => {

const { email, name } = token_info_data;
let user = await User.findOne({ email }).select(-password);
if (!user) {
user = await User.create({ email, name});
}
const token = user.generateToken();
res.status(token_info_response.status).json({ user, token });
});

Note: In case you run into install reference error: fetch isn’t defined, ensure you install node-fetch

OAuth 2.0 Best Practices

Certain security procedures and error-handling scenarios were disregarded to keep the tutorial straightforward and simple to follow. This section will teach you the recommended practices to follow when implementing OAuth 2.0.

Make use of state: Use anti-forgery tokens to strengthen security and avoid the risk of Google imposter sending data or calling the callback URL. The purpose of the state is to ensure that the response from the authorization server is from a request initiated by the application and the same user.

Incremental Authorization and Separation of Authentication and Authorization use cases – This practice is similar to the programming rule – You Aren’t Gonna Need It (YAGNI). It requires that your application must not request permissions to access what your application does not need. If at a later time, your application requires a scope a user has not approved before you could prompt the user for approval again. Also, if the sole purpose of making use of OAuth 2.0 is to delegate your application’s user authentication then there’s no need to store or keep users’ access token and/or refresh token because it’s only needed if you want to access the resource server.
Don’t expose your client’s secrets.
Make use of the appropriate grant type or flow that suits your use case or environment. The flow type used in this tutorial is called authorization code flow and we used it because we are building a web application. The following are other flow types available:

Device authorization grant – used with devices with limited input devices or without access to browser.
Implicit grant
Token grant

Summing Up

OAuth 2.0 is the bedrock or fundamental component of third-party integration. The benefits accrued from the seamless integration of multiple services or various applications make OAuth 2.0 an important protocol and so widely adopted or used.

This tutorial has provided a hands-on guide to implementing Google Sign-In through OAuth 2.0, emphasizing key security practices. By following these steps, developers can seamlessly integrate authentication and authorization features, enhancing user experiences in a world where interconnectedness is paramount.

Leave a Reply

Your email address will not be published. Required fields are marked *