β‘ Quick OAuth + JWT Architecture (For Fast Revision) #
When handling social logins while maintaining a stateless JWT ecosystem, follow this flow:
[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 disablepassport
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 thegoogleId
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 asExternal.
Head to the
Clients page, chooseCreate Clients, and click** OAuth Client ID**.
Set the application type to
Web Applicationand add your explicit callback mapping:
Authorized Redirect URIs:http://localhost:3000/api/v1/auth/google/callback
- Save your changes and copy your Client ID andClient 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 optionalrequired: 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:
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
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
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
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
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
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! π