{"slug": "google-login-in-express-with-passportjs-jwt", "title": "Google Login in Express with PassportJS & JWT", "summary": "This article provides a step-by-step guide on implementing Google OAuth 2.0 authentication in an Express.js application using PassportJS and JSON Web Tokens (JWT). It covers setting up the Google strategy in Passport, configuring the user model with required fields like `googleId` and `refreshToken`, and creating authentication routes with callback handling. The tutorial also includes instructions for generating JWT tokens, managing cookies, and deploying the solution in production with secure cookie settings.", "body_md": "## ⚡ Quick OAuth + JWT Architecture (For Fast Revision)\n\nWhen handling social logins while maintaining a stateless JWT ecosystem, follow this flow:\n\n``` php\n[User] --- 1. GET /auth/google ---> [Passport Engine] ---> (Redirects to Google Sign-In)\n[User] <--- 2. Grants Permission -- [Google Server]\n[Backend Callback] <-- 3. Code/Profile Handshake <-- [Google Server] (Verifies & Upserts User Profile)\n[User] <--- 4. Sets Secure Access & Refresh Cookies --- [Backend Controller] (Generates Custom JWTs)\n```\n\n### Core Strategy Rules\n\n**No Server-Side Sessions:** We explicitly disable`passport`\n\nsession serialization (`session: false`\n\n) 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`\n\nonto the pre-existing document profile.\n\n## Prerequisites & Dependencies\n\n### 📂 Project Structure\n\n```\n└── src/\n    ├── config/\n    ├── controllers/\n    ├── middlewares/\n    ├── models/\n    ├── routes/\n    ├── utils/\n    ├── app.ts\n    └── index.ts\n├── .env\n```\n\n### 📥 Install Required Packages\n\nExecute the following installation string to fetch Passport.js, the Google OAuth2.0 strategy token extensions, and their respective type-hint definitions:\n\n```\nnpm install passport passport-google-oauth20 jsonwebtoken cookie-parser bcrypt mongoose\nnpm install -D @types/passport-google-oauth20\n```\n\n## Step 1: Cloud Console Configurations\n\nBefore writing software, you need application credentials from the Google Cloud Dashboard.\n\nNavigate to the\n\n.**Google Cloud Console****Create a New Project** using the project selection drop-down layout.\n\nConfigure your\n\n**OAuth Consent Screen** and designate the publishing status as*External*.\n\nHead to the\n\n**Clients** page, choose**Create Clients**, and click** OAuth Client ID**.\n\nSet the application type to\n\n*Web Application*and add your explicit callback mapping:\n\n-\n**Authorized Redirect URIs:**`http://localhost:3000/api/v1/auth/google/callback`\n\n- Save your changes and copy your\n**Client ID** and**Client Secret** tokens.\n\n### 🌐 Environment Setup\n\nAppend these key-value configurations to your root `.env`\n\nfile environment block:\n\n```\nGOOGLE_CLIENT_ID=your_google_client_id_here\nGOOGLE_CLIENT_SECRET=your_google_client_secret_here\nGOOGLE_CALLBACK_URL=http://localhost:3000/api/v1/auth/google/callback\n```\n\n## Step 2: Adapting the Database Schema\n\nTo support alternative OAuth logins alongside traditional password profiles, update your Mongoose Model configuration.\n\n🔒\n\nCritical Security Rule\n\nWhen integrating third-party OAuth provider chains, youmust make your schema's(`password`\n\nstring optional`required: false`\n\n). This allows users who signed up with Google to create accounts without passwords.\n\nMake sure these key fields are mapped out in your user schema definitions:\n\n``` js\nconst userSchema = new Schema({\n  username: { type: String, required: true, unique: true },\n  email: { type: String, required: true, unique: true },\n\n  // Make password optional for OAuth registrations!\n  password: { type: String, required: false }, \n\n  avatarUrl: { type: String },\n  refreshToken: { type: String },\n  googleId: { type: String } // Keeps track of mapped Google profiles\n});\n```\n\nTo maintain security, place an evaluation guard inside your traditional login controllers so social-only accounts cannot be hijacked through brute-force attempts:\n\n```\nif (!user.password) {\n  throw new ApiError(400, \"This account was registered via Google Sign-In. Please log in using Google.\");\n}\n```\n\n## Step 3: Architecting the Passport Strategy\n\nNow we configure Passport to handle Google authentication.\n\nHere we:\n\n- receive the Google profile\n- check if the user already exists\n- create a new account if needed\n- link existing accounts using email\n\n``` python\nimport passport from \"passport\";\nimport { Strategy as GoogleStrategy } from \"passport-google-oauth20\";\nimport { User } from \"../models/User.model.js\";\nimport { generateUsername } from \"../utils/usernameGen.js\";\n\npassport.use(\n  new GoogleStrategy(\n    {\n      clientID: process.env.GOOGLE_CLIENT_ID!,\n      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,\n      callbackURL: process.env.GOOGLE_CALLBACK_URL,\n    },\n    async (_accessToken, _refreshToken, profile, done) => {\n      try {\n        const email = profile.emails?.[0]?.value;\n\n        if (!email) {\n          return done(new Error(\"Google account profile must yield a primary email address\"));\n        }\n\n        // Look for an existing account matching either the googleId OR the email address\n        let user = await User.findOne({\n          $or: [{ googleId: profile.id }, { email }],\n        });\n\n        // Case 1: Account exists but lacks a googleId link (First-time social login for an existing user)\n        if (user && !user.googleId) {\n          user.googleId = profile.id;\n          await user.save();\n        }\n\n        // Case 2: No account exists under this email - Create a brand new user profile\n        if (!user) {\n          const uniqueUsername = await generateUsername(profile.displayName);\n\n          user = await User.create({\n            username: uniqueUsername,\n            fullName: profile.displayName,\n            email,\n            googleId: profile.id,\n            avatarUrl: profile.photos?.[0]?.value || \"\",\n          });\n        }\n\n        // Remove sensitive fields before returning the user\n        const sanitizedUser = await User.findById(user._id).select(\"-password -refreshToken -googleId\");\n        if (!sanitizedUser) {\n          return done(new Error(\"User not found after creation\"));\n        }\n\n        return done(null, sanitizedUser);\n      } catch (error) {\n        return done(error as Error);\n      }\n    }\n  )\n);\n\nexport default passport;\n```\n\n### Dynamic Namespace Deduplication Utility\n\nWhen creating users via OAuth, Google provides full display names, which aren't guaranteed to be unique, so we generate a fallback username if needed.:\n\n`src/utils/usernameGen.ts`\n\n``` js\nimport { User } from \"../models/User.model.js\";\n\nexport const generateUsername = async (\n  displayName: string\n): Promise<string> => {\n  const cleaned = displayName\n    .toLowerCase()\n    .replace(/[^a-z0-9]/g, \"\");\n\n  const baseUsername =\n    cleaned.length > 0\n      ? cleaned.slice(0, 15)\n      : \"user\";\n\n  let username = \"\";\n  let exists = true;\n\n  while (exists) {\n    const suffix = Math.floor(\n      1000 + Math.random() * 9000\n    );\n\n    username = `${baseUsername}${suffix}`;\n\n    exists = !!(await User.exists({\n      username,\n    }));\n  }\n\n  return username;\n};\n```\n\n## Step 4: Building the Callback Controller & Routes\n\nOnce Passport successfully authenticates the user, control moves to our controller.. Here, we generate our custom app cookies and pass down the response payload.\n\n### The Controller Handlers\n\n`src/controllers/auth.controller.ts`\n\n``` js\nimport { type Request, type Response } from \"express\";\nimport { generateTokens } from \"../utils/generateTokens.js\";\n\nexport const googleAuthCallback = async (req: Request, res: Response) => {\n  // Passport injects the sanitized profile info onto the req.user property\n  const user = req.user!;\n\n  // Generate our system's regular custom JWT tokens\n  const { accessToken, refreshToken } = await generateTokens(user._id);\n\n  const cookieOptions = {\n    httpOnly: true,\n    secure: process.env.NODE_ENV === \"production\",\n    sameSite: \"strict\" as const,\n  };\n\n  return res\n    .status(200)\n    .cookie(\"accessToken\", accessToken, cookieOptions)\n    .cookie(\"refreshToken\", refreshToken, cookieOptions)\n    .json({\n      success: true,\n      message: \"Google authentication handshake completed successfully\",\n      user,\n    });\n};\n```\n\n### Defining Route Registrations\n\n`src/routes/auth.routes.ts`\n\n``` python\nimport { Router } from \"express\";\nimport passport from \"passport\";\nimport { googleAuthCallback } from \"../controllers/auth.controller.js\";\n\nconst router = Router();\n\n// Route 1: Initial redirect request loop trigger\nrouter.route(\"/google\").get(\n  passport.authenticate(\"google\", {\n    scope: [\"profile\", \"email\"], // Target scope values required from Google Cloud console\n    session: false, // Ensures stateless JWT operations\n  })\n);\n\n// Route 2: Target route intercept landing zone for redirect returns from Google\nrouter.route(\"/google/callback\").get(\n  passport.authenticate(\"google\", {\n    failureRedirect: \"/login\",\n    session: false,\n    failureMessage: \"Failed to login with Google credentials\",\n  }),\n  googleAuthCallback\n);\n\nexport default router;\n```\n\n## Step 5: Mounting Initializations\n\nFinally, register and load the Passport setup directly within your core runtime file (`app.ts`\n\n) before mounting your routes.\n\n`src/app.ts`\n\n``` python\nimport express from \"express\";\nimport cookieParser from \"cookie-parser\";\nimport passport from \"./config/passport.js\"; // Loads strategy definitions\nimport authRouter from \"./routes/auth.routes.js\";\n\nconst app = express();\n\napp.use(express.json());\napp.use(cookieParser());\n\n// Initialize Passport Engine\napp.use(passport.initialize());\n\n// App Routes\napp.use(\"/api/v1/auth\", authRouter);\n\nexport { app };\n```\n\n## 🛠️ Diagnostics & Troubleshooting Checkpoints\n\n⚠️\n\nCommon Bug: Redirection URI Mismatch Errors\n\nIf 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:\n\nThe Allowed Callback parameter mapped inside your\n\nCloud Dashboard Console.The\n\n`GOOGLE_CALLBACK_URL`\n\nliteral configuration inside your`.env`\n\nworkspace variables.The\n\n`callbackURL`\n\nproperty parameter initialized inside your Passport strategy constructor instantiation block.\n\n## Summary Checklist\n\nMade backend password strings optional (\n\n`required: false`\n\n) on database models.Set\n\n`session: false`\n\nacross all routing hooks to stay completely stateless.Bound fallback profile configurations matching\n\n`profile.emails?.[0]?.value`\n\nqueries.Handled unique namespace fallback conflicts using clean alphanumeric deduplication utility logic.\n\nHappy coding! 🚀", "url": "https://wpnews.pro/news/google-login-in-express-with-passportjs-jwt", "canonical_source": "https://dev.to/nhero/google-login-in-express-with-passportjs-jwt-1483", "published_at": "2026-05-23 10:12:28+00:00", "updated_at": "2026-05-23 10:33:15.349138+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Google", "PassportJS", "JWT", "Express.js", "Google Cloud Console", "Mongoose", "bcrypt"], "alternates": {"html": "https://wpnews.pro/news/google-login-in-express-with-passportjs-jwt", "markdown": "https://wpnews.pro/news/google-login-in-express-with-passportjs-jwt.md", "text": "https://wpnews.pro/news/google-login-in-express-with-passportjs-jwt.txt", "jsonld": "https://wpnews.pro/news/google-login-in-express-with-passportjs-jwt.jsonld"}}