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

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 CredentialsCreate CredentialsOAuth 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

Loading files...

Installation

npx servercn-cli add oauth