Google OAuth (Passport)

The Google OAuth component provides a secure and standardized way to integrate Google authentication into your Servercn Express applications using the official passport, passport-google-oauth20.

It handles the complete OAuth 2.0 flow including authorization URL generation, token exchange, user information retrieval, and token refresh.

Official Docs

Features

  • Complete OAuth 2.0 flow - Authorization, token exchange, and user info retrieval
  • Secure by default - CSRF protection with state parameter
  • Token management - Access token, refresh token, and ID token verification
  • Express integration - Ready-to-use route handlers
  • Type-safe - Full TypeScript support
  • Flexible scopes - Customizable OAuth scopes

Installation Guide

This component requires additional Servercn components.

๐Ÿ‘‰ You do not need to install any Servercn components manually. Running this component installer will automatically install all required components. Manual installation is optional and only recommended if you prefer fine-grained components control

npx servercn-cli add oauth

You will be prompted to select a file upload provider:

? Select OAuth provider:  ยป - Use arrow-keys. Return to submit.
>   Google
    GitHub
    Google + GitHub

The CLI will then automatically configure the component based on your selected provider.

Prerequisites

  1. Go to the Google Cloud Console
  2. Create a new project or select an existing one
  3. Enable the Google+ API (or Google Identity API)
  4. Go to Credentials โ†’ Create Credentials โ†’ OAuth client ID
  5. Configure the OAuth consent screen:
    • Choose External (for testing) or Internal (for Google Workspace)
    • Fill in the required information
  6. Create OAuth 2.0 Client ID:
    • Application type: Web application
    • Authorized JavaScript origins: Add your origin URL (e.g., http://localhost:9000)
    • Authorized redirect URIs: Add your callback URL (e.g., http://localhost:9000/api/auth/google/callback)
  7. Copy the Client ID and Client Secret

Add the following to your .env file:

GOOGLE_CLIENT_ID="your-google-client-id.apps.googleusercontent.com"
GOOGLE_CLIENT_SECRET="your-google-client-secret"
GOOGLE_REDIRECT_URI="http://localhost:9000/api/auth/google/callback"
# replace all with your values

Ensure the following configuration are defined:

MVC: src/configs/env.ts

Feature: src/shared/configs/env.ts

import "dotenv-flow/config";
 
interface Config {
  PORT: number;
  NODE_ENV: string;
  LOG_LEVEL: string;
  CORS_ORIGIN: string;
 
  GOOGLE_CLIENT_ID: string;
  GOOGLE_CLIENT_SECRET: string;
  GOOGLE_REDIRECT_URI: string;
}
 
const env: Config = {
  PORT: Number(process.env.PORT) || 3000,
  NODE_ENV: process.env.NODE_ENV || "development",
  LOG_LEVEL: process.env.LOG_LEVEL || "info",
  CORS_ORIGIN: process.env.CORS_ORIGIN || "*",
 
  GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID!,
  GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET!,
  GOOGLE_REDIRECT_URI: process.env.GOOGLE_REDIRECT_URI!
};
 
export default env;

Basic Implementation

1. Configure passport in src/configs/passport.ts:

import passport from "passport";
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
import env from "./env";
 
const clientId = env.GOOGLE_CLIENT_ID;
const clientSecret = env.GOOGLE_CLIENT_SECRET;
const redirectUri = env.GOOGLE_REDIRECT_URI;
 
passport.use(
  new GoogleStrategy(
    {
      clientID: clientId,
      clientSecret,
      callbackURL: redirectUri
    },
    function (accessToken, refreshToken, profile, cb) {
      return cb(null, profile);
    }
  )
);

2. Create a Google OAuth controller in src/controllers/google-oauth.controller.ts or src/controllers/auth.controller.ts

import { NextFunction, Request, Response } from "express";
import { Profile } from "passport-google-oauth20";
 
//? import servercn components
import { ApiResponse } from "../utils/api-response";
import { AsyncHandler } from "../utils/async-handler";
import { ApiError } from "../utils/api-error";
 
//? login with google
export const googleOAuth = AsyncHandler(
  async (req: Request, res: Response, next: NextFunction) => {
    const data = req.user as Profile | undefined;
    const user = data?._json;
 
    if (!user || !data) {
      return next(ApiError.unauthorized("Authenticated failed!"));
    }
 
    const userInfo = {
      provider: data?.provider,
      providerId: data.id,
      name: data.displayName,
      email: data?.emails && data?.emails[0]?.value,
      isEmailVerified: data?.emails && data?.emails[0]?.verified,
      avatar: data.profileUrl || (data.photos && data.photos[0].value)
    };
 
    const userInfo2 = {
      provider: data?.provider,
      providerId: user.sub,
      name: user.name,
      email: user.email,
      isEmailVerified: user.email_verified,
      avatar: user.picture
    };
 
    //? save the data into your databases
 
    ApiResponse.ok(res, "Auth Successfull", {
      userInfo,
      userInfo2
    });
  }
);

3. Create a Google OAuth router in src/routes/google-oauth.routes.ts or src/routes/auth.routes.ts

import { Router } from "express";
import passport from "passport";
 
import { googleOAuth } from "../controllers/google-oauth.controller";
 
const router = Router();
 
router.get(
  "/google",
  passport.authenticate("google", {
    scope: ["email", "profile", "openid"],
    prompt: "consent"
  })
);
 
router.get(
  "/google/callback",
  passport.authenticate("google", {
    failureRedirect: "/login", //? redirect route if authenticated is failed
    session: false
  }),
  googleOAuth
);
 
export default router;

4. Create a server in src/app.ts

import express, { Express, Request, Response } from "express";
 
import { notFoundHandler } from "./middlewares/not-found-handler";
import { errorHandler } from "./middlewares/error-handler";
 
import AuthRoutes from "./routes/google-oauth.routes";
 
import "./configs/passport";
 
const app: Express = express();
 
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
 
// Routes
app.use("/api/auth", AuthRoutes);
 
// Not found handler (should be after routes)
app.use(notFoundHandler);
 
// Global error handler (should be last)
app.use(errorHandler);
 
export default app;

1. Configure passport in src/shared/configs/passport.ts:

import passport from "passport";
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
import env from "./env";
 
const clientId = env.GOOGLE_CLIENT_ID;
const clientSecret = env.GOOGLE_CLIENT_SECRET;
const redirectUri = env.GOOGLE_REDIRECT_URI;
 
passport.use(
  new GoogleStrategy(
    {
      clientID: clientId,
      clientSecret,
      callbackURL: redirectUri
    },
    function (accessToken, refreshToken, profile, cb) {
      return cb(null, profile);
    }
  )
);

2. Create a Google OAuth controller in src/modules/oauth/google-oauth.controller.ts or src/modules/auth/auth.controller.ts

import { NextFunction, Request, Response } from "express";
import { Profile } from "passport-google-oauth20";
 
import { AsyncHandler } from "../../shared/utils/async-handler";
import { ApiError } from "../../shared/errors/api-error";
import { ApiResponse } from "../../shared/utils/api-response";
 
//? login with google
export const googleOAuth = AsyncHandler(
  async (req: Request, res: Response, next: NextFunction) => {
    const data = req.user as Profile | undefined;
    const user = data?._json;
 
    if (!user || !data) {
      return next(ApiError.unauthorized("Authenticated failed!"));
    }
 
    const userInfo = {
      provider: data?.provider,
      providerId: data.id,
      name: data.displayName,
      email: data?.emails && data?.emails[0]?.value,
      isEmailVerified: data?.emails && data?.emails[0]?.verified,
      avatar: data.profileUrl || (data.photos && data.photos[0].value)
    };
 
    const userInfo2 = {
      provider: data?.provider,
      providerId: user.sub,
      name: user.name,
      email: user.email,
      isEmailVerified: user.email_verified,
      avatar: user.picture
    };
 
    //? save the data into your databases
 
    ApiResponse.ok(res, "Auth Successfull", {
      userInfo,
      userInfo2
    });
  }
);

3. Create a Google OAuth router in src/modules/oauth/google-oauth.routes.ts or src/modules/auth/auth.routes.ts

import { Router } from "express";
import passport from "passport";
 
import { googleOAuth } from "./google-oauth.controller";
 
const router = Router();
 
router.get(
  "/google",
  passport.authenticate("google", {
    scope: ["email", "profile", "openid"],
    prompt: "consent"
  })
);
 
router.get(
  "/google/callback",
  passport.authenticate("google", {
    failureRedirect: "/login", //? redirect route if authenticated is failed
    session: false
  }),
  googleOAuth
);
 
export default router;

4. Create a index route in src/routes/index.ts

import { Router } from "express";
import OAuthRoutes from "../modules/oauth/google-oauth.routes";
 
const router = Router();
 
router.use("/auth", OAuthRoutes);
 
export default router;

5. Create a server in src/app.ts

import express, { Express } from "express";
 
import { notFoundHandler } from "./shared/middlewares/not-found-handler";
import { errorHandler } from "./shared/middlewares/error-handler";
 
import Routes from "./routes/index";
 
import "./shared/configs/passport";
 
const app: Express = express();
 
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
 
// Routes
app.use("/api", Routes);
 
// Not found handler (should be after routes)
app.use(notFoundHandler);
 
// Global error handler (should be last)
app.use(errorHandler);
 
export default app;

Success Response

{
  "success": true,
  "message": "Auth Successfull",
  "statusCode": 200,
  "data": {
    "userInfo": {
      "provider": "google",
      "providerId": "1163181840105620881",
      "name": "Akkal Dhami",
      "email": "dhamiakkal21@gmail.com",
      "isEmailVerified": true,
      "avatar": "https://lh3.googleusercontent.com/a-/ALV-UjUYqyuQ4khZ_2DGd3G4iZPj8RXBtkTPv8avzN4L3ifb--QOXKomzOM6nOwG4Sau24uQmWCBfj6Id3DBdoyabVah8hPWFpZ9aIfyj6nBn4OTzSEYF0vxUXL-oPM4qszzCo7TXsJF2RySN45fuC8l-aZzzxl_qetw8-27Y9zZLh8MZytQXezVPRovI2VqZ1NJDQTfFBzF4VrK8XdkNC39pgO3exH65m1pM__I7zjB-wV5zN2AFrHfVT-ucllEQMGWoK4sXdRZXZBI3za8K-bqxVlaXFtqJyaBv13RKTuR32lfDMtqNp0q4tvH0FgTSV6bn1Yk80NIjxn8fsoPYHYAXgFDxY_wwuL9zhHRAl-niBC5acuSgSzN-JhOrSjYNAT3CxQ_UbtZ2X6Lw9NPalQDakHxahJTayJPzdvbU-fpDDpxIywz_-oBn_c44MrBbUX5ktY6-eioXigyZdeeRH4PTeXfAkhQBSnYD2cVDwACEjo0znVM5JJakmPsDInHBJGi3jQSJfQ52luqZGDXDF7CGmy7fU7-jZMmq1gRAPcxdhrS2m0srkuOwyopfxonXuJV59uYSEioRXe9nMjA-_wjkGy5jSxiCEaUnF10strkD51xGYvXzjUAYb4GH_iz4K0RjS8WJGfDHGG-wLfMB5qtIS2ZckYO38NT3FcEZ4v7hrFV78vXxdX9lotWX0S_IaVBv8siZVNQ2irvQHFcNDcfDVl4uCVIk9kkS4TBtBO9TosZI14IOnjFbMYua3z2ljQPLg_B1nT3CqC5UAOJ5GJG6Hl-M7dHXaJrKoOnJ5HeTbB84UbVOCB-4IXZoIpg6G1II73pCowX1eA8meWNLKHor6Eii-mOk3TTPSm_hgsArfTbv60uGGKyOzlTutY1Goi6yxN85iKkpzAJPUdkvPLbNbebm9XP8xIDKPae1U-okzm60l5lf-dmFlogBqiWKbDDgHeG0I-HRDGqSeTBv-C5sFSsczPmB5eVXHwV244nS0T-oFMy1OuzatYEM5-qo8YOy7tTSZv0swtMiA1MLhA128qV_ZB28LDC9BrB6v7ayH9C37vVzpCo8m0=s96-c"
    },
    "userInfo2": {
      "provider": "google",
      "providerId": "1163181840105620881",
      "name": "Akkal Dhami",
      "email": "dhamiakkal21@gmail.com",
      "isEmailVerified": true,
      "avatar": "https://lh3.googleusercontent.com/a-/ALV-UjUYqyuQ4khZ_2DGd3G4iZPj8RXBtkTPv8avzN4L3ifb--QOXKomzOM6nOwG4Sau24uQmWCBfj6Id3DBdoyabVah8hPWFpZ9aIfyj6nBn4OTzSEYF0vxUXL-oPM4qszzCo7TXsJF2RySN45fuC8l-aZzzxl_qetw8-27Y9zZLh8MZytQXezVPRovI2VqZ1NJDQTfFBzF4VrK8XdkNC39pgO3exH65m1pM__I7zjB-wV5zN2AFrHfVT-ucllEQMGWoK4sXdRZXZBI3za8K-bqxVlaXFtqJyaBv13RKTuR32lfDMtqNp0q4tvH0FgTSV6bn1Yk80NIjxn8fsoPYHYAXgFDxY_wwuL9zhHRAl-niBC5acuSgSzN-JhOrSjYNAT3CxQ_UbtZ2X6Lw9NPalQDakHxahJTayJPzdvbU-fpDDpxIywz_-oBn_c44MrBbUX5ktY6-eioXigyZdeeRH4PTeXfAkhQBSnYD2cVDwACEjo0znVM5JJakmPsDInHBJGi3jQSJfQ52luqZGDXDF7CGmy7fU7-jZMmq1gRAPcxdhrS2m0srkuOwyopfxonXuJV59uYSEioRXe9nMjA-_wjkGy5jSxiCEaUnF10strkD51xGYvXzjUAYb4GH_iz4K0RjS8WJGfDHGG-wLfMB5qtIS2ZckYO38NT3FcEZ4v7hrFV78vXxdX9lotWX0S_IaVBv8siZVNQ2irvQHFcNDcfDVl4uCVIk9kkS4TBtBO9TosZI14IOnjFbMYua3z2ljQPLg_B1nT3CqC5UAOJ5GJG6Hl-M7dHXaJrKoOnJ5HeTbB84UbVOCB-4IXZoIpg6G1II73pCowX1eA8meWNLKHor6Eii-mOk3TTPSm_hgsArfTbv60uGGKyOzlTutY1Goi6yxN85iKkpzAJPUdkvPLbNbebm9XP8xIDKPae1U-okzm60l5lf-dmFlogBqiWKbDDgHeG0I-HRDGqSeTBv-C5sFSsczPmB5eVXHwV244nS0T-oFMy1OuzatYEM5-qo8YOy7tTSZv0swtMiA1MLhA128qV_ZB28LDC9BrB6v7ayH9C37vVzpCo8m0=s96-c"
    }
  }
}

This response is formated by ApiResponse component.

Common Issues

Ensure your redirect URI in .env exactly matches the one configured in Google Cloud Console.

This usually means:

  • The authorization code has expired (codes expire after a few minutes)
  • The code has already been used
  • The redirect URI doesn't match

The user denied permission. Handle this gracefully in your UI.

File & Folder Structure

ServerCN

Select a file to view its contents

Installation

npx servercn-cli add oauth