In this article, I’ll walk you through the process of setting up Google OAuth2 in a Node.js application, including creating the OAuth project on the Google Cloud Console, configuring the OAuth Client ID and secret, and implementing the necessary code in the Node.js project.

What is Google OAuth? Google OAuth is a token-based system that allows third-party applications to access a user’s Google account information, such as their email address, profile information, and contacts, without the user sharing their login credentials.

With Google OAuth, the user can grant access to their data without giving away their password. Also, the user can easily revoke access to the third-party application if necessary.

Related articles:

How to Implement Google OAuth2 in Node.js

Run the Node.js Google OAuth2 Project

  1. Download or clone the Node.js Google OAuth project from https://github.com/wpcodevo/google-github-oauth2-nodejs and open the source code in an IDE.
  2. Run git checkout google-oauth2-nodejs in the console of the root directory to change the Git branch. Nonetheless, feel free to use the master branch.
  3. Install the project’s dependencies by running yarn or yarn install.
  4. Duplicate the example.env file and rename the copied one to .env .
  5. Follow the “Get the Google OAuth2 Credentials” section to obtain the OAuth2 client ID and secret from the Google API Console.
  6. Add the Google OAuth2 credentials to the .env file.
  7. Push the Prisma schema to the SQLite database by running npx prisma db push.
  8. Run yarn start to start the Express HTTP server.
  9. Set up the React app to interact with the Node.js API.

Run the Node.js API with a React.js App

For a complete guide on how to set up the Google OAuth2 flow in a React application, see the post “How to Implement Google OAuth2 in React.js“. However, you can follow the steps below to spin up the app within minutes.

  1. Download or clone the React Google OAuth2 project from https://github.com/wpcodevo/google-github-oath2-reactjs and open the project in a code editor.
  2. Run yarn or yarn install to install the necessary dependencies.
  3. Duplicate the example.env file and rename the copied one to .env.local .
  4. Add the OAuth2 client ID and client secret to the .env.local file.
  5. Run yarn dev to start the Vite development server.
  6. Interact with the Node.js API from the React app.

Setup the Node.js Project

First things first, navigate to a convenient location on your system and create the project directory. For the seek of this tutorial, you can name the project google-oauth2-nodejs . Once that is done, navigate into the folder and initialize the Node.js project using Yarn.


mkdir google-oauth2-nodejs
cd google-oauth2-nodejs
yarn init -y

This will create a package.json file with an initial setup for the Node.js app. With that out of the way, run the commands below to install the dependencies we’ll need for the project.


yarn add @prisma/client axios cookie-parser cors dotenv express jsonwebtoken qs zod
yarn add -D typescript ts-node-dev prisma morgan @types/qs @types/node @types/morgan @types/jsonwebtoken @types/express @types/cors @types/cookie-parser

  • @prisma/client – An auto-generated query builder that enables type-safe database access.
  • axios – A promise-based HTTP client for the browser and Node.js.
  • cookie-parser – A middleware for parsing HTTP request cookies.
  • cors – A Node.js CORS middleware.
  • dotenv – Load environment variables from a .env file.
  • express – A web framework for Node.js.
  • jsonwebtoken – JSON Web Token implementation for JavaScript projects.
  • qs – A library for parsing and stringifying query parameters.
  • zod – A schema validation library.
  • typescript – A language for JavaScript development.
  • ts-node-dev – Compiles the TypeScript files and restart the server when required files change.
  • prisma – A CLI tool that allows you to interact with your Prisma project from the command line.
  • morgan – An HTTP request middleware logger for Node.js

After the installation is complete, open the project in an IDE or text editor. Next, create a tsconfig.json file in the root directory and add the following TypeScript configurations.

tsconfig.json


{
  "compilerOptions": {
    "target": "es2016",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "module": "commonjs",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "strictPropertyInitialization": false,
    "skipLibCheck": true
  }
}

Now let’s get our hands dirty by building a basic Express.js server to respond with a simple JSON object. To do this, create an src folder in the root directory. Within the src folder, create a app.ts file and add the code below.

src/app.ts


import express, { Request, Response } from "express";

const app = express();

app.get("/api/healthChecker", (req: Request, res: Response) => {
  res.status(200).json({
    status: "success",
    message: "Implement Google OAuth2 in Node.js",
  });
});

const port = 8000;
app.listen(port, () => {
  console.log(`✅ Server started on port: ${port}`);
});

In the above, we created an Express server that listens on port 8000 and has one route, /api/healthChecker , which returns a 200 OK status code and a JSON object.

Open the package.json file and add the following scripts:

package.json


{
  "scripts": {
    "start": "ts-node-dev --respawn --transpile-only src/app.ts",
    "db:migrate": "npx prisma migrate dev --name 'users' --create-only",
    "db:generate": "npx prisma generate",
    "db:push": "npx prisma db push"
  }
}

  • start – This script will start the Express HTTP server and hot-reload the server when required files change.
  • db:migrate – This script will create the Prisma migration file without applying it.
  • db:generate – This script will generate the Prisma Client in the node_modules folder.
  • db:push – This script will push the Prisma migration file to the database.

Now run yarn start to start the Express server and make a GET request to http://localhost:8000/api/healthchecker. Within a few milliseconds, you should see the JSON object.

testing the health checker route of the Node.js Google OAuth2 project

Get the Google OAuth2 Credentials

  1. Go to the Google Cloud Console https://console.cloud.google.com/ and select an existing project or create a new one.

    If you haven’t created a project yet, you can create one by clicking on the “NEW PROJECT” button.
    select a project or create a new one on the Google Cloud API dashboard
  2. On the next screen, enter the project name and click on the “CREATE” button.
    create a new project on the google console api dashboard
  3. In a short period, the project will be created and you’ll be prompted to select the newly-created project from the notification.
    click on the newly created project from the notification
  4. After selecting the project, click on the “OAuth consent screen” menu in the left sidebar and select “External” under the “User Type” on the next screen.
    select external under the user type and click on create
    Then, click on the “CREATE” button.
  5. On the “OAuth consent screen” tab, enter the App information and scroll down to the “App domain” section.
    provide the consent screen credentials part 1
    In the “App domain” section, enter the application homepage, privacy policy, and the terms of service URLs.
    provide the consent screen credentials part 2
    After that, enter your email in the “Developer contact information” section, and click on the “SAVE AND CONTINUE” button.
  6. On the “Scopes” tab, click on the “ADD OR REMOVE SCOPES” button, and select both the userinfo.email and userinfo.profile . Once you are done, scroll to the bottom and click on the “UPDATE” button.
    select the scopes
  7. Click on the “ADD USERS” button on the “Test users” tab. While the application is in sandbox mode, only these users will be allowed to test it using their Google accounts.

    Provide the emails of the test users and click on the “ADD” button.
    add the test user
    Click on the “SAVE AND CONTINUE” button to persist the changes. On the “Summary” tab, go through the provided information and click on the “BACK TO DASHBOARD” button.
  8. At this point, we’ve provided the information needed for the consent screen. Now let’s create the OAuth client ID and secret. To do this, click on the “Credentials” menu in the left sidebar and click on the “CREATE CREDENTIALS” button.

    Select “OAuth client ID” from the available options.
    select oauth client ID
  9. On the “Create OAuth client ID” screen, select “Web application” as the Application type, and provide a name for the client ID.

    After that, enter http://localhost:8000/api/sessions/oauth/google as the Authorised redirect URI and click on the “Create” button at the bottom.
    provide the oauth credentials
  10. Once the OAuth client ID and secret have been generated, open the .env file and add them.

Open the .env file and add the following environment variables. Don’t forget to add the OAuth client ID and client secret to the placeholders.

.env


DATABASE_URL="file:./dev.db"

GOOGLE_OAUTH_CLIENT_ID=
GOOGLE_OAUTH_CLIENT_SECRET=
GOOGLE_OAUTH_REDIRECT=http://localhost:8000/api/sessions/oauth/google

NODE_ENV=development
JWT_SECRET=my_ultra_secure_secret
TOKEN_EXPIRES_IN=60
FRONTEND_ORIGIN=http://localhost:3000

Setup the Database with Prisma

Now let’s set up a database to store the user’s account information. To do this, we’ll use the Prisma ORM to access and mutate data in an SQLite database.

Feel free to use any Prisma-supported database. Run the Prisma init command to initialize Prisma in the project.


npx prisma init --datasource-provider sqlite

This will create a new prisma directory with a Prisma schema file. Open the prisma/schema.prisma file and the following Prisma model.

prisma/schema.prisma


model User {
  id       String  @id @default(uuid())
  name     String
  email    String  @unique
  password String
  role     String  @default("user")
  photo    String  @default("default.png")
  verified Boolean @default(false)
  provider String  @default("local")

  createdAt DateTime
  updatedAt DateTime @updatedAt

  @@map(name: "users")
}

The Prisma model has two main purposes:

  • It represents the table in the underlying database
  • Serves as a foundation for the generated Prisma Client.

Run the Prisma migrate command to create the SQL migration file, generate the Prisma Client, and push the migration to the database.


npx prisma migrate dev --name init

Now let’s create an instance of the Prisma Client and a function to check if the database connection pool was established successfully. Create a utils folder in the src directory. In the src/utils folder, create a prisma.ts file and add the following code snippets.

src/utils/prisma.ts


import { PrismaClient } from "@prisma/client";

export const prisma = new PrismaClient();

async function connectDB() {
  try {
    await prisma.$connect();
    console.log("🚀 Database connected successfully");
  } catch (error) {
    console.log(error);
    process.exit(1);
  } finally {
    await prisma.$disconnect();
  }
}

export default connectDB;

Create the Validation Schemas

To prevent users from sending junk values in the request body, let’s create validation schemas and a middleware to validate the request bodies against the rules defined in the schemas.

Create a schema folder in the src directory. Within the src/schema/ folder, create a user.schema.ts file and add the following schema definitions.

src/schema/user.schema.ts


import { object, string, TypeOf } from 'zod';

export const createUserSchema = object({
  body: object({
    name: string({ required_error: 'Name is required' }),
    email: string({ required_error: 'Email is required' }).email(
      'Invalid email'
    ),
    password: string({ required_error: 'Password is required' })
      .min(8, 'Password must be more than 8 characters')
      .max(32, 'Password must be less than 32 characters'),
    passwordConfirm: string({ required_error: 'Please confirm your password' }),
  }).refine((data) => data.password === data.passwordConfirm, {
    path: ['passwordConfirm'],
    message: 'Passwords do not match',
  }),
});

export const loginUserSchema = object({
  body: object({
    email: string({ required_error: 'Email is required' }).email(
      'Invalid email or password'
    ),
    password: string({ required_error: 'Password is required' }).min(
      8,
      'Invalid email or password'
    ),
  }),
});

export type CreateUserInput = TypeOf<typeof createUserSchema>['body'];
export type LoginUserInput = TypeOf<typeof loginUserSchema>['body'];

Now let’s create a validation middleware that we’ll add to the request middleware pipeline to validate the incoming request body and return appropriate validation errors to the client.

To do this, create a src/middleware/validate.ts file and add the code below:

src/middleware/validate.ts


import { NextFunction, Request, Response } from "express";
import { AnyZodObject, ZodError } from "zod";

export const validate =
  (schema: AnyZodObject) =>
  (req: Request, res: Response, next: NextFunction) => {
    try {
      schema.parse({
        params: req.params,
        query: req.query,
        body: req.body,
      });

      next();
    } catch (err: any) {
      if (err instanceof ZodError) {
        return res.status(400).json({
          status: "fail",
          error: err.errors,
        });
      }
      next(err);
    }
  };

This route middleware will accept a Zod schema as an argument, parse the request body and validate the fields based on the rules defined in the schema, and return validation errors if any of the rules were violated.

Get the Google OAuth Access Token and User’s Info

If you made it this far, am proud of you. Now it’s time to use the OAuth client ID and client secret to obtain an access token from the Google OAuth2 API. To begin, create a src/services/session.service.ts file and add the following imports and constants.

src/services/session.service.ts


import axios from "axios";
import qs from "qs";

const GOOGLE_OAUTH_CLIENT_ID = process.env.GOOGLE_OAUTH_CLIENT_ID as unknown as string;
const GOOGLE_OAUTH_CLIENT_SECRET = process.env.GOOGLE_OAUTH_CLIENT_SECRET as unknown as string;
const GOOGLE_OAUTH_REDIRECT = process.env.GOOGLE_OAUTH_REDIRECT as unknown as string;

Get the OAuth Access Token

To obtain the OAuth access token, we’ll create a function that will use the axios package to make a POST request to the Google OAuth token endpoint and retrieve the access token using the OAuth client ID and secret.

src/services/session.service.ts


interface GoogleOauthToken {
  access_token: string;
  id_token: string;
  expires_in: number;
  refresh_token: string;
  token_type: string;
  scope: string;
}

export const getGoogleOauthToken = async ({
  code,
}: {
  code: string;
}): Promise<GoogleOauthToken> => {
  const rootURl = "https://oauth2.googleapis.com/token";

  const options = {
    code,
    client_id: GOOGLE_OAUTH_CLIENT_ID,
    client_secret: GOOGLE_OAUTH_CLIENT_SECRET,
    redirect_uri: GOOGLE_OAUTH_REDIRECT,
    grant_type: "authorization_code",
  };
  try {
    const { data } = await axios.post<GoogleOauthToken>(
      rootURl,
      qs.stringify(options),
      {
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
        },
      }
    );

    return data;
  } catch (err: any) {
    console.log("Failed to fetch Google Oauth Tokens");
    throw new Error(err);
  }
};

  • client_id – The client ID is a unique identifier that was issued to us when we created the project on the Google Cloud Console.
  • client_secret – The Client secret will be used by the Google OAuth2 API to prove the identity of the application.
  • redirect_uri – The redirect URI, also known as the callback URL, is the URL the OAuth2 server will redirect the user to after they’ve granted or denied access to the requested permissions.
  • grant_type – The code, also known as the authorization code, is a short-lived code that is issued by the OAuth2 server when the user grants access to their resources. We’ll use this code to obtain the access token from the OAuth2 server.

Get the Google Account User

Now that we’re able to obtain the access token, let’s create a function to retrieve the user’s account information using the access token.

src/services/session.service.ts


interface GoogleUserResult {
  id: string;
  email: string;
  verified_email: boolean;
  name: string;
  given_name: string;
  family_name: string;
  picture: string;
  locale: string;
}

export async function getGoogleUser({
  id_token,
  access_token,
}: {
  id_token: string;
  access_token: string;
}): Promise<GoogleUserResult> {
  try {
    const { data } = await axios.get<GoogleUserResult>(
      `https://www.googleapis.com/oauth2/v1/userinfo?alt=json&access_token=${access_token}`,
      {
        headers: {
          Authorization: `Bearer ${id_token}`,
        },
      }
    );

    return data;
  } catch (err: any) {
    console.log(err);
    throw Error(err);
  }
}

This function will make a GET request with Axios to Google OAuth2’s /v1/userinfo endpoint to retrieve the user’s public profile information. For this to work, the function will add the access token to the URL as a query parameter and id_token to the Authorization header as Bearer.

Implement the Google OAuth2 in Node.js

Oops, quite a lot of configurations and code. At this point, we’re now ready to implement the authentication aspect of the Node.js API. To do this, we’ll create middleware functions that Express will delegate requests to when a client or frontend application makes requests to the server.

First, create a controllers folder in the src directory. Then, create a auth.controller.ts file in the controllers folder and add the following code.

src/controllers/auth.controller.ts


import { NextFunction, Request, Response } from "express";
import { CreateUserInput, LoginUserInput } from "../schema/user.schema";
import {
  getGoogleOauthToken,
  getGoogleUser,
} from "../services/session.service";
import { prisma } from "../utils/prisma";
import jwt from "jsonwebtoken";

export function exclude<User, Key extends keyof User>(
  user: User,
  keys: Key[]
): Omit<User, Key> {
  for (let key of keys) {
    delete user[key];
  }
  return user;
}

The exclude function will allow us to omit sensitive fields like passwords from the records returned by Prisma.

Register User Route Handler

This route middleware function will be evoked by the Express router to register new users when a POST request is made to the /api/auth/register endpoint.

When Express forwards the request to this function, it will create a new user object with the data provided in the request body, and save the user to the database using the Prisma client.

If a user with that email already exists in the database, a 409 Conflict response will be sent to the client. However, if the operation is successful, the function will return a 201 status code and the newly-created user in the response.

src/controllers/auth.controller.ts


export const registerHandler = async (
  req: Request<{}, {}, CreateUserInput>,
  res: Response,
  next: NextFunction
) => {
  try {
    const user = await prisma.user.create({
      data: {
        name: req.body.name,
        email: req.body.email,
        password: req.body.password,
        createdAt: new Date(),
      },
    });

    res.status(201).json({
      status: "success",
      data: {
        user: exclude(user, ["password"]),
      },
    });
  } catch (err: any) {
    if (err.code === "P2002") {
      return res.status(409).json({
        status: "fail",
        message: "Email already exist",
      });
    }
    next(err);
  }
};

Login User Route Handler

This middleware function will be called to sign users into the API when a POST request is made to the /api/auth/login endpoint. When the request reaches this route handler, it will use the Prisma client to check if a user with that email exists in the database.

If a user exists, a JSON Web token will be generated and sent to the client as an HTTP Only cookie. Note: To make the project simple, we’ll skip other authentication methods like password hashing, session storage, etc.

src/controllers/auth.controller.ts


export const loginHandler = async (
  req: Request<{}, {}, LoginUserInput>,
  res: Response,
  next: NextFunction
) => {
  try {
    const user = await prisma.user.findUnique({
      where: { email: req.body.email },
    });

    if (!user) {
      return res.status(401).json({
        status: "fail",
        message: "Invalid email or password",
      });
    }

    if (user.provider === "Google") {
      return res.status(401).json({
        status: "fail",
        message: `Use ${user.provider} OAuth2 instead`,
      });
    }

    const TOKEN_EXPIRES_IN = process.env.TOKEN_EXPIRES_IN as unknown as number;
    const TOKEN_SECRET = process.env.JWT_SECRET as unknown as string;
    const token = jwt.sign({ sub: user.id }, TOKEN_SECRET, {
      expiresIn: `${TOKEN_EXPIRES_IN}m`,
    });

    res.cookie("token", token, {
      expires: new Date(Date.now() + TOKEN_EXPIRES_IN * 60 * 1000),
    });

    res.status(200).json({
      status: "success",
    });
  } catch (err: any) {
    next(err);
  }
};

Logout User Route Handler

Now that we’re able to sign up and sign in users, let’s create a route function that Express will use to sign out users. To achieve this, we’ll send an expired cookie to delete the existing one in the user’s browser or API client.

src/controllers/auth.controller.ts


export const logoutHandler = async (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  try {
    res.cookie("token", "", { maxAge: -1 });
    res.status(200).json({ status: "success" });
  } catch (err: any) {
    next(err);
  }
};

Authenticate with Google OAuth2 Route Handler

src/controllers/auth.controller.ts


export const googleOauthHandler = async (req: Request, res: Response) => {
  const FRONTEND_ORIGIN = process.env.FRONTEND_ORIGIN as unknown as string;

  try {
    const code = req.query.code as string;
    const pathUrl = (req.query.state as string) || "/";

    if (!code) {
      return res.status(401).json({
        status: "fail",
        message: "Authorization code not provided!",
      });
    }

    const { id_token, access_token } = await getGoogleOauthToken({ code });

    const { name, verified_email, email, picture } = await getGoogleUser({
      id_token,
      access_token,
    });

    if (!verified_email) {
      return res.status(403).json({
        status: "fail",
        message: "Google account not verified",
      });
    }

    const user = await prisma.user.upsert({
      where: { email },
      create: {
        createdAt: new Date(),
        name,
        email,
        photo: picture,
        password: "",
        verified: true,
        provider: "Google",
      },
      update: { name, email, photo: picture, provider: "Google" },
    });

    if (!user) return res.redirect(`${FRONTEND_ORIGIN}/oauth/error`);

    const TOKEN_EXPIRES_IN = process.env.TOKEN_EXPIRES_IN as unknown as number;
    const TOKEN_SECRET = process.env.JWT_SECRET as unknown as string;
    const token = jwt.sign({ sub: user.id }, TOKEN_SECRET, {
      expiresIn: `${TOKEN_EXPIRES_IN}m`,
    });

    res.cookie("token", token, {
      expires: new Date(Date.now() + TOKEN_EXPIRES_IN * 60 * 1000),
    });

    res.redirect(`${FRONTEND_ORIGIN}${pathUrl}`);
  } catch (err: any) {
    console.log("Failed to authorize Google User", err);
    return res.redirect(`${FRONTEND_ORIGIN}/oauth/error`);
  }
};

Complete Route Handlers

src/controllers/auth.controller.ts


import { NextFunction, Request, Response } from "express";
import { CreateUserInput, LoginUserInput } from "../schema/user.schema";
import {
  getGoogleOauthToken,
  getGoogleUser,
} from "../services/session.service";
import { prisma } from "../utils/prisma";
import jwt from "jsonwebtoken";

export function exclude<User, Key extends keyof User>(
  user: User,
  keys: Key[]
): Omit<User, Key> {
  for (let key of keys) {
    delete user[key];
  }
  return user;
}

export const registerHandler = async (
  req: Request<{}, {}, CreateUserInput>,
  res: Response,
  next: NextFunction
) => {
  try {
    const user = await prisma.user.create({
      data: {
        name: req.body.name,
        email: req.body.email,
        password: req.body.password,
        createdAt: new Date(),
      },
    });

    res.status(201).json({
      status: "success",
      data: {
        user: exclude(user, ["password"]),
      },
    });
  } catch (err: any) {
    if (err.code === "P2002") {
      return res.status(409).json({
        status: "fail",
        message: "Email already exist",
      });
    }
    next(err);
  }
};

export const loginHandler = async (
  req: Request<{}, {}, LoginUserInput>,
  res: Response,
  next: NextFunction
) => {
  try {
    const user = await prisma.user.findUnique({
      where: { email: req.body.email },
    });

    if (!user) {
      return res.status(401).json({
        status: "fail",
        message: "Invalid email or password",
      });
    }

    if (user.provider === "Google") {
      return res.status(401).json({
        status: "fail",
        message: `Use ${user.provider} OAuth2 instead`,
      });
    }

    const TOKEN_EXPIRES_IN = process.env.TOKEN_EXPIRES_IN as unknown as number;
    const TOKEN_SECRET = process.env.JWT_SECRET as unknown as string;
    const token = jwt.sign({ sub: user.id }, TOKEN_SECRET, {
      expiresIn: `${TOKEN_EXPIRES_IN}m`,
    });

    res.cookie("token", token, {
      expires: new Date(Date.now() + TOKEN_EXPIRES_IN * 60 * 1000),
    });

    res.status(200).json({
      status: "success",
    });
  } catch (err: any) {
    next(err);
  }
};

export const logoutHandler = async (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  try {
    res.cookie("token", "", { maxAge: -1 });
    res.status(200).json({ status: "success" });
  } catch (err: any) {
    next(err);
  }
};

export const googleOauthHandler = async (req: Request, res: Response) => {
  const FRONTEND_ORIGIN = process.env.FRONTEND_ORIGIN as unknown as string;

  try {
    const code = req.query.code as string;
    const pathUrl = (req.query.state as string) || "/";

    if (!code) {
      return res.status(401).json({
        status: "fail",
        message: "Authorization code not provided!",
      });
    }

    const { id_token, access_token } = await getGoogleOauthToken({ code });

    const { name, verified_email, email, picture } = await getGoogleUser({
      id_token,
      access_token,
    });

    if (!verified_email) {
      return res.status(403).json({
        status: "fail",
        message: "Google account not verified",
      });
    }

    const user = await prisma.user.upsert({
      where: { email },
      create: {
        createdAt: new Date(),
        name,
        email,
        photo: picture,
        password: "",
        verified: true,
        provider: "Google",
      },
      update: { name, email, photo: picture, provider: "Google" },
    });

    if (!user) return res.redirect(`${FRONTEND_ORIGIN}/oauth/error`);

    const TOKEN_EXPIRES_IN = process.env.TOKEN_EXPIRES_IN as unknown as number;
    const TOKEN_SECRET = process.env.JWT_SECRET as unknown as string;
    const token = jwt.sign({ sub: user.id }, TOKEN_SECRET, {
      expiresIn: `${TOKEN_EXPIRES_IN}m`,
    });

    res.cookie("token", token, {
      expires: new Date(Date.now() + TOKEN_EXPIRES_IN * 60 * 1000),
    });

    res.redirect(`${FRONTEND_ORIGIN}${pathUrl}`);
  } catch (err: any) {
    console.log("Failed to authorize Google User", err);
    return res.redirect(`${FRONTEND_ORIGIN}/oauth/error`);
  }
};

Create a User Route Handler

Let’s create a route handler that will be called to return the authenticated user’s information when a GET request is made to the /api/users/me endpoint. This route middleware will be protected and only users with valid JWT can access it.

src/controllers/user.controller.ts


import { NextFunction, Request, Response } from "express";

export const getMeHandler = (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  try {
    const user = res.locals.user;
    res.status(200).json({
      status: "success",
      data: {
        user,
      },
    });
  } catch (err: any) {
    next(err);
  }
};

Create the Authentication Guards

Here, you’ll create an Express middleware function that will authenticate users by checking for a valid token in the request headers or Cookies object.

Authentication Middleware

The middleware function will check for the presence of an “Authorization” header in the request, which should contain a JSON Web Token (JWT). If the header is not present, it will check the Cookies object and if the token was not included in the request, a 401 Unauthorized response will be sent to the client.

However, if the token is present in either the Authorization header or Cookies object, the function will verify the token by calling the jwt.verify() method provided by the jsonwebtoken library.

src/middleware/deserializeUser.ts


import { NextFunction, Request, Response } from "express";
import jwt from "jsonwebtoken";
import { exclude } from "../controllers/auth.controller";
import { prisma } from "../utils/prisma";

export const deserializeUser = async (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  try {
    let token;
    if (
      req.headers.authorization &&
      req.headers.authorization.startsWith("Bearer")
    ) {
      token = req.headers.authorization.split(" ")[1];
    } else if (req.cookies.token) {
      token = req.cookies.token;
    }

    if (!token) {
      return res.status(401).json({
        status: "fail",
        message: "You are not logged in",
      });
    }

    const JWT_SECRET = process.env.JWT_SECRET as unknown as string;
    const decoded = jwt.verify(token, JWT_SECRET);

    if (!decoded) {
      return res.status(401).json({
        status: "fail",
        message: "Invalid token or user doesn't exist",
      });
    }

    const user = await prisma.user.findUnique({
      where: { id: String(decoded.sub) },
    });

    if (!user) {
      return res.status(401).json({
        status: "fail",
        message: "User with that token no longer exist",
      });
    }

    res.locals.user = exclude(user, ["password"]);

    next();
  } catch (err: any) {
    next(err);
  }
};

If the token is valid, the payload will be extracted and a query will be made to check if the user belonging to the token still exists in the database. If the user exists, the record returned by the database will be added to the response object as res.locals.user and the request will be passed to the next middleware.

Require User Middleware

This middleware will then check the response to see if the res.locals object has the user record. If the res.locals.user property is undefined, a 401 Unauthorized response will be sent to the client.

src/middleware/requireUser.ts


import { NextFunction, Request, Response } from "express";

export const requireUser = (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  try {
    const user = res.locals.user;
    if (!user) {
      return res.status(401).json({
        status: "fail",
        message: "Invalid token or session has expired",
      });
    }

    next();
  } catch (err: any) {
    next(err);
  }
};

Create the API Routes

Now let’s create Express routers to evoke the route middleware functions. The first router will have the following endpoints:

  • /register – This route will evoke the registerHandler function when a POST request is made to /api/auth/register .
  • /login – This route will evoke the loginHandler function when a POST request is made to the /api/auth/login endpoint.
  • /logout – This route will evoke the logoutHandler function when a GET request is made to the /api/auth/logout endpoint.

Create a routes folder in the src directory. In the src/routes/ folder, create a auth.route.ts file and add the code below.

src/routes/auth.route.ts


import express from "express";
import {
  loginHandler,
  logoutHandler,
  registerHandler,
} from "../controllers/auth.controller";
import { deserializeUser } from "../middleware/deserializeUser";
import { requireUser } from "../middleware/requireUser";
import { validate } from "../middleware/validate";
import { createUserSchema, loginUserSchema } from "../schema/user.schema";

const router = express.Router();

router.post("/register", validate(createUserSchema), registerHandler);
router.post("/login", validate(loginUserSchema), loginHandler);
router.get("/logout", deserializeUser, requireUser, logoutHandler);

export default router;

The second router will have only one endpoint, /oauth/google , which will be evoked when the Oauth2 server redirects the user to the Express server. So create a session.route.ts file and add the following code.

src/routes/session.route.ts


import express from "express";
import { googleOauthHandler } from "../controllers/auth.controller";

const router = express.Router();

router.get("/oauth/google", googleOauthHandler);

export default router;

The last router will also have one endpoint, /me , which will be evoked when a GET request is made to the /api/users/me endpoint.

src/routes/user.route.ts


import express from "express";
import { getMeHandler } from "../controllers/user.controller";
import { deserializeUser } from "../middleware/deserializeUser";
import { requireUser } from "../middleware/requireUser";

const router = express.Router();

router.use(deserializeUser, requireUser);

router.get("/me", getMeHandler);

export default router;

Setup CORS and Register the API Routers

Finally, let’s add the routers to the Express app and configure the server to accept cross-origin requests from specific origins. To do this, open the src/app.ts file, and replace its content with the following code.

src/app.ts


require("dotenv").config();
import path from "path";
import express, { NextFunction, Request, Response } from "express";
import morgan from "morgan";
import cors from "cors";
import cookieParser from "cookie-parser";
import userRouter from "./routes/user.route";
import authRouter from "./routes/auth.route";
import sessionRouter from "./routes/session.route";
import connectDB from "./utils/prisma";

const app = express();

app.use(express.json({ limit: "10kb" }));
app.use(cookieParser());
if (process.env.NODE_ENV === "development") app.use(morgan("dev"));
app.use("/api/images", express.static(path.join(__dirname, "../public")));

const FRONTEND_ORIGIN = process.env.FRONTEND_ORIGIN as unknown as string;
app.use(
  cors({
    credentials: true,
    origin: [FRONTEND_ORIGIN],
  })
);

app.use("/api/users", userRouter);
app.use("/api/auth", authRouter);
app.use("/api/sessions", sessionRouter);

app.get("/api/healthChecker", (req: Request, res: Response) => {
  res.status(200).json({
    status: "success",
    message: "Implement OAuth in Node.js",
  });
});

// UnKnown Routes
app.all("*", (req: Request, res: Response, next: NextFunction) => {
  const err = new Error(`Route ${req.originalUrl} not found`) as any;
  err.statusCode = 404;
  next(err);
});

app.use((err: any, req: Request, res: Response, next: NextFunction) => {
  err.status = err.status || "error";
  err.statusCode = err.statusCode || 500;

  res.status(err.statusCode).json({
    status: err.status,
    message: err.message,
  });
});

const port = 8000;
app.listen(port, () => {
  console.log(`✅ Server started on port: ${port}`);
  connectDB();
});

Now you can start the Express HTTP server again by running yarn start.

Conclusion

And we are done! You can find the source code of the Node.js Google OAuth2 project on GitHub.

In this article, we implemented Google OAuth flow from scratch using TypeScript and Node.js. The API has all the required functionalities, for example, signing up users, signing users into the API, authenticating with Google OAuth, and logging users out of the application.