# Google Login in Express with PassportJS & JWT

> Source: <https://dev.to/nhero/google-login-in-express-with-passportjs-jwt-1483>
> Published: 2026-05-23 10:12:28+00:00

## ⚡ Quick OAuth + JWT Architecture (For Fast Revision)

When handling social logins while maintaining a stateless JWT ecosystem, follow this flow:

``` php
[User] --- 1. GET /auth/google ---> [Passport Engine] ---> (Redirects to Google Sign-In)
[User] <--- 2. Grants Permission -- [Google Server]
[Backend Callback] <-- 3. Code/Profile Handshake <-- [Google Server] (Verifies & Upserts User Profile)
[User] <--- 4. Sets Secure Access & Refresh Cookies --- [Backend Controller] (Generates Custom JWTs)
```

### Core Strategy Rules

**No Server-Side Sessions:** We explicitly disable`passport`

session serialization (`session: false`

) because our app uses stateless JWT tokens.**User Accounts Linking:** If a user registers normally with an email address and later hits the "Sign In with Google" button, we automatically link the identity by pinning the`googleId`

onto the pre-existing document profile.

## Prerequisites & Dependencies

### 📂 Project Structure

```
└── src/
    ├── config/
    ├── controllers/
    ├── middlewares/
    ├── models/
    ├── routes/
    ├── utils/
    ├── app.ts
    └── index.ts
├── .env
```

### 📥 Install Required Packages

Execute the following installation string to fetch Passport.js, the Google OAuth2.0 strategy token extensions, and their respective type-hint definitions:

```
npm install passport passport-google-oauth20 jsonwebtoken cookie-parser bcrypt mongoose
npm install -D @types/passport-google-oauth20
```

## Step 1: Cloud Console Configurations

Before writing software, you need application credentials from the Google Cloud Dashboard.

Navigate to the

.**Google Cloud Console****Create a New Project** using the project selection drop-down layout.

Configure your

**OAuth Consent Screen** and designate the publishing status as*External*.

Head to the

**Clients** page, choose**Create Clients**, and click** OAuth Client ID**.

Set the application type to

*Web Application*and add your explicit callback mapping:

-
**Authorized Redirect URIs:**`http://localhost:3000/api/v1/auth/google/callback`

- Save your changes and copy your
**Client ID** and**Client Secret** tokens.

### 🌐 Environment Setup

Append these key-value configurations to your root `.env`

file environment block:

```
GOOGLE_CLIENT_ID=your_google_client_id_here
GOOGLE_CLIENT_SECRET=your_google_client_secret_here
GOOGLE_CALLBACK_URL=http://localhost:3000/api/v1/auth/google/callback
```

## Step 2: Adapting the Database Schema

To support alternative OAuth logins alongside traditional password profiles, update your Mongoose Model configuration.

🔒

Critical Security Rule

When integrating third-party OAuth provider chains, youmust make your schema's(`password`

string optional`required: false`

). This allows users who signed up with Google to create accounts without passwords.

Make sure these key fields are mapped out in your user schema definitions:

``` js
const userSchema = new Schema({
  username: { type: String, required: true, unique: true },
  email: { type: String, required: true, unique: true },

  // Make password optional for OAuth registrations!
  password: { type: String, required: false }, 

  avatarUrl: { type: String },
  refreshToken: { type: String },
  googleId: { type: String } // Keeps track of mapped Google profiles
});
```

To maintain security, place an evaluation guard inside your traditional login controllers so social-only accounts cannot be hijacked through brute-force attempts:

```
if (!user.password) {
  throw new ApiError(400, "This account was registered via Google Sign-In. Please log in using Google.");
}
```

## Step 3: Architecting the Passport Strategy

Now we configure Passport to handle Google authentication.

Here we:

- receive the Google profile
- check if the user already exists
- create a new account if needed
- link existing accounts using email

``` python
import passport from "passport";
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
import { User } from "../models/User.model.js";
import { generateUsername } from "../utils/usernameGen.js";

passport.use(
  new GoogleStrategy(
    {
      clientID: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
      callbackURL: process.env.GOOGLE_CALLBACK_URL,
    },
    async (_accessToken, _refreshToken, profile, done) => {
      try {
        const email = profile.emails?.[0]?.value;

        if (!email) {
          return done(new Error("Google account profile must yield a primary email address"));
        }

        // Look for an existing account matching either the googleId OR the email address
        let user = await User.findOne({
          $or: [{ googleId: profile.id }, { email }],
        });

        // Case 1: Account exists but lacks a googleId link (First-time social login for an existing user)
        if (user && !user.googleId) {
          user.googleId = profile.id;
          await user.save();
        }

        // Case 2: No account exists under this email - Create a brand new user profile
        if (!user) {
          const uniqueUsername = await generateUsername(profile.displayName);

          user = await User.create({
            username: uniqueUsername,
            fullName: profile.displayName,
            email,
            googleId: profile.id,
            avatarUrl: profile.photos?.[0]?.value || "",
          });
        }

        // Remove sensitive fields before returning the user
        const sanitizedUser = await User.findById(user._id).select("-password -refreshToken -googleId");
        if (!sanitizedUser) {
          return done(new Error("User not found after creation"));
        }

        return done(null, sanitizedUser);
      } catch (error) {
        return done(error as Error);
      }
    }
  )
);

export default passport;
```

### Dynamic Namespace Deduplication Utility

When creating users via OAuth, Google provides full display names, which aren't guaranteed to be unique, so we generate a fallback username if needed.:

`src/utils/usernameGen.ts`

``` js
import { User } from "../models/User.model.js";

export const generateUsername = async (
  displayName: string
): Promise<string> => {
  const cleaned = displayName
    .toLowerCase()
    .replace(/[^a-z0-9]/g, "");

  const baseUsername =
    cleaned.length > 0
      ? cleaned.slice(0, 15)
      : "user";

  let username = "";
  let exists = true;

  while (exists) {
    const suffix = Math.floor(
      1000 + Math.random() * 9000
    );

    username = `${baseUsername}${suffix}`;

    exists = !!(await User.exists({
      username,
    }));
  }

  return username;
};
```

## Step 4: Building the Callback Controller & Routes

Once Passport successfully authenticates the user, control moves to our controller.. Here, we generate our custom app cookies and pass down the response payload.

### The Controller Handlers

`src/controllers/auth.controller.ts`

``` js
import { type Request, type Response } from "express";
import { generateTokens } from "../utils/generateTokens.js";

export const googleAuthCallback = async (req: Request, res: Response) => {
  // Passport injects the sanitized profile info onto the req.user property
  const user = req.user!;

  // Generate our system's regular custom JWT tokens
  const { accessToken, refreshToken } = await generateTokens(user._id);

  const cookieOptions = {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    sameSite: "strict" as const,
  };

  return res
    .status(200)
    .cookie("accessToken", accessToken, cookieOptions)
    .cookie("refreshToken", refreshToken, cookieOptions)
    .json({
      success: true,
      message: "Google authentication handshake completed successfully",
      user,
    });
};
```

### Defining Route Registrations

`src/routes/auth.routes.ts`

``` python
import { Router } from "express";
import passport from "passport";
import { googleAuthCallback } from "../controllers/auth.controller.js";

const router = Router();

// Route 1: Initial redirect request loop trigger
router.route("/google").get(
  passport.authenticate("google", {
    scope: ["profile", "email"], // Target scope values required from Google Cloud console
    session: false, // Ensures stateless JWT operations
  })
);

// Route 2: Target route intercept landing zone for redirect returns from Google
router.route("/google/callback").get(
  passport.authenticate("google", {
    failureRedirect: "/login",
    session: false,
    failureMessage: "Failed to login with Google credentials",
  }),
  googleAuthCallback
);

export default router;
```

## Step 5: Mounting Initializations

Finally, register and load the Passport setup directly within your core runtime file (`app.ts`

) before mounting your routes.

`src/app.ts`

``` python
import express from "express";
import cookieParser from "cookie-parser";
import passport from "./config/passport.js"; // Loads strategy definitions
import authRouter from "./routes/auth.routes.js";

const app = express();

app.use(express.json());
app.use(cookieParser());

// Initialize Passport Engine
app.use(passport.initialize());

// App Routes
app.use("/api/v1/auth", authRouter);

export { app };
```

## 🛠️ Diagnostics & Troubleshooting Checkpoints

⚠️

Common Bug: Redirection URI Mismatch Errors

If Google dumps a configuration block error message on your display screen during testing, double-check that your callback strings match exactly across all three of these locations:

The Allowed Callback parameter mapped inside your

Cloud Dashboard Console.The

`GOOGLE_CALLBACK_URL`

literal configuration inside your`.env`

workspace variables.The

`callbackURL`

property parameter initialized inside your Passport strategy constructor instantiation block.

## Summary Checklist

Made backend password strings optional (

`required: false`

) on database models.Set

`session: false`

across all routing hooks to stay completely stateless.Bound fallback profile configurations matching

`profile.emails?.[0]?.value`

queries.Handled unique namespace fallback conflicts using clean alphanumeric deduplication utility logic.

Happy coding! 🚀
