[{"data":1,"prerenderedAt":343},["ShallowReactive",2],{"blog-how-to/oauth-setup":3},{"id":4,"title":5,"body":6,"category":323,"date":324,"dateModified":325,"description":326,"draft":327,"extension":328,"faq":329,"featured":327,"headerVariant":330,"image":329,"keywords":329,"meta":331,"navigation":332,"ogDescription":333,"ogTitle":329,"path":334,"readTime":329,"schemaOrg":335,"schemaType":336,"seo":337,"sitemap":338,"stem":339,"tags":340,"twitterCard":341,"__hash__":342},"blog/blog/how-to/oauth-setup.md","How to Set Up OAuth Authentication Securely",{"type":7,"value":8,"toc":308},"minimark",[9,13,17,21,27,30,43,48,51,55,78,91,104,117,130,143,184,188,215,219,224,227,231,234,238,241,245,248,270,289],[10,11],"category-badge",{"category":12},"How-To Guide",[14,15,5],"h1",{"id":16},"how-to-set-up-oauth-authentication-securely",[18,19,20],"p",{},"Social login done right with Google, GitHub, and more",[22,23,24],"tldr",{},[18,25,26],{},"TL;DR (30 minutes):\nUse Authorization Code flow with PKCE (not implicit flow). Always validate the state parameter. Verify tokens on the server side, not client. Use exact redirect URI matching. Store OAuth tokens encrypted. Link accounts by verified email only after confirming email ownership.",[18,28,29],{},"Prerequisites:",[31,32,33,37,40],"ul",{},[34,35,36],"li",{},"OAuth provider account (Google, GitHub, etc.)",[34,38,39],{},"HTTPS-enabled domain",[34,41,42],{},"Database for storing user accounts",[44,45,47],"h2",{"id":46},"why-this-matters","Why This Matters",[18,49,50],{},"OAuth lets users sign in with existing accounts (Google, GitHub, etc.) instead of creating new passwords. But improper implementation leads to account takeover vulnerabilities. Common mistakes include missing state validation, improper token handling, and insecure account linking.",[44,52,54],{"id":53},"step-by-step-guide","Step-by-Step Guide",[56,57,59,64,67],"step",{"number":58},"1",[60,61,63],"h3",{"id":62},"register-your-oauth-application","Register your OAuth application",[18,65,66],{},"Create credentials with your OAuth provider:",[68,69,74],"pre",{"className":70,"code":72,"language":73},[71],"language-text","# Google Cloud Console\n1. Go to console.cloud.google.com\n2. Create project → APIs & Services → Credentials\n3. Create OAuth 2.0 Client ID\n4. Add authorized redirect URIs:\n   - https://yourapp.com/api/auth/callback/google\n   - http://localhost:3000/api/auth/callback/google (dev)\n\n# GitHub\n1. Go to github.com/settings/developers\n2. New OAuth App\n3. Set callback URL:\n   - https://yourapp.com/api/auth/callback/github\n\n# Store credentials securely\nGOOGLE_CLIENT_ID=xxx\nGOOGLE_CLIENT_SECRET=xxx\nGITHUB_CLIENT_ID=xxx\nGITHUB_CLIENT_SECRET=xxx\n","text",[75,76,72],"code",{"__ignoreMap":77},"",[56,79,81,85],{"number":80},"2",[60,82,84],{"id":83},"implement-authorization-with-pkce","Implement authorization with PKCE",[68,86,89],{"className":87,"code":88,"language":73},[71],"import crypto from 'crypto';\n\n// Generate PKCE challenge\nfunction generatePKCE() {\n  const verifier = crypto.randomBytes(32).toString('base64url');\n  const challenge = crypto\n    .createHash('sha256')\n    .update(verifier)\n    .digest('base64url');\n\n  return { verifier, challenge };\n}\n\n// Generate state parameter (prevents CSRF)\nfunction generateState() {\n  return crypto.randomBytes(32).toString('hex');\n}\n\n// Start OAuth flow\nasync function startOAuth(req, res, provider) {\n  const { verifier, challenge } = generatePKCE();\n  const state = generateState();\n\n  // Store in session for validation later\n  req.session.oauthState = state;\n  req.session.oauthVerifier = verifier;\n  req.session.oauthProvider = provider;\n\n  const params = new URLSearchParams({\n    client_id: process.env[`${provider.toUpperCase()}_CLIENT_ID`],\n    redirect_uri: `${process.env.APP_URL}/api/auth/callback/${provider}`,\n    response_type: 'code',\n    scope: getScopes(provider),\n    state,\n    code_challenge: challenge,\n    code_challenge_method: 'S256'\n  });\n\n  const authUrl = getAuthUrl(provider);\n  res.redirect(`${authUrl}?${params}`);\n}\n\nfunction getScopes(provider) {\n  switch (provider) {\n    case 'google': return 'openid email profile';\n    case 'github': return 'read:user user:email';\n    default: return 'openid email profile';\n  }\n}\n\nfunction getAuthUrl(provider) {\n  switch (provider) {\n    case 'google': return 'https://accounts.google.com/o/oauth2/v2/auth';\n    case 'github': return 'https://github.com/login/oauth/authorize';\n  }\n}\n",[75,90,88],{"__ignoreMap":77},[56,92,94,98],{"number":93},"3",[60,95,97],{"id":96},"handle-the-callback-securely","Handle the callback securely",[68,99,102],{"className":100,"code":101,"language":73},[71],"async function handleCallback(req, res) {\n  const { code, state, error } = req.query;\n  const provider = req.params.provider;\n\n  // Check for OAuth errors\n  if (error) {\n    return res.redirect('/login?error=oauth_denied');\n  }\n\n  // CRITICAL: Validate state parameter\n  if (!state || state !== req.session.oauthState) {\n    return res.redirect('/login?error=invalid_state');\n  }\n\n  // Validate provider matches\n  if (provider !== req.session.oauthProvider) {\n    return res.redirect('/login?error=provider_mismatch');\n  }\n\n  try {\n    // Exchange code for tokens\n    const tokens = await exchangeCodeForTokens(\n      provider,\n      code,\n      req.session.oauthVerifier\n    );\n\n    // Get user info from provider\n    const oauthUser = await getOAuthUserInfo(provider, tokens.access_token);\n\n    // Find or create user in your database\n    const user = await findOrCreateUser(oauthUser, provider, tokens);\n\n    // Clear OAuth session data\n    delete req.session.oauthState;\n    delete req.session.oauthVerifier;\n    delete req.session.oauthProvider;\n\n    // Create your app's session\n    await createUserSession(req, res, user);\n\n    res.redirect('/dashboard');\n  } catch (error) {\n    console.error('OAuth callback error:', error);\n    res.redirect('/login?error=oauth_failed');\n  }\n}\n\nasync function exchangeCodeForTokens(provider, code, verifier) {\n  const tokenUrl = provider === 'google'\n    ? 'https://oauth2.googleapis.com/token'\n    : 'https://github.com/login/oauth/access_token';\n\n  const response = await fetch(tokenUrl, {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/x-www-form-urlencoded',\n      'Accept': 'application/json'\n    },\n    body: new URLSearchParams({\n      client_id: process.env[`${provider.toUpperCase()}_CLIENT_ID`],\n      client_secret: process.env[`${provider.toUpperCase()}_CLIENT_SECRET`],\n      code,\n      grant_type: 'authorization_code',\n      redirect_uri: `${process.env.APP_URL}/api/auth/callback/${provider}`,\n      code_verifier: verifier\n    })\n  });\n\n  const data = await response.json();\n\n  if (data.error) {\n    throw new Error(data.error_description || data.error);\n  }\n\n  return data;\n}\n",[75,103,101],{"__ignoreMap":77},[56,105,107,111],{"number":106},"4",[60,108,110],{"id":109},"get-and-verify-user-info","Get and verify user info",[68,112,115],{"className":113,"code":114,"language":73},[71],"async function getOAuthUserInfo(provider, accessToken) {\n  let userInfo;\n\n  if (provider === 'google') {\n    const response = await fetch(\n      'https://www.googleapis.com/oauth2/v2/userinfo',\n      { headers: { Authorization: `Bearer ${accessToken}` } }\n    );\n    userInfo = await response.json();\n\n    return {\n      providerId: userInfo.id,\n      email: userInfo.email,\n      emailVerified: userInfo.verified_email,\n      name: userInfo.name,\n      avatar: userInfo.picture\n    };\n  }\n\n  if (provider === 'github') {\n    // Get user profile\n    const userResponse = await fetch('https://api.github.com/user', {\n      headers: { Authorization: `Bearer ${accessToken}` }\n    });\n    const user = await userResponse.json();\n\n    // Get user emails (GitHub may not include email in profile)\n    const emailsResponse = await fetch('https://api.github.com/user/emails', {\n      headers: { Authorization: `Bearer ${accessToken}` }\n    });\n    const emails = await emailsResponse.json();\n\n    // Find primary verified email\n    const primaryEmail = emails.find(e => e.primary && e.verified);\n\n    return {\n      providerId: user.id.toString(),\n      email: primaryEmail?.email,\n      emailVerified: primaryEmail?.verified || false,\n      name: user.name || user.login,\n      avatar: user.avatar_url\n    };\n  }\n}\n",[75,116,114],{"__ignoreMap":77},[56,118,120,124],{"number":119},"5",[60,121,123],{"id":122},"link-accounts-securely","Link accounts securely",[68,125,128],{"className":126,"code":127,"language":73},[71],"async function findOrCreateUser(oauthUser, provider, tokens) {\n  // Check if OAuth account already linked\n  let oauthAccount = await prisma.oAuthAccount.findUnique({\n    where: {\n      provider_providerId: {\n        provider,\n        providerId: oauthUser.providerId\n      }\n    },\n    include: { user: true }\n  });\n\n  if (oauthAccount) {\n    // Update tokens\n    await prisma.oAuthAccount.update({\n      where: { id: oauthAccount.id },\n      data: {\n        accessToken: encrypt(tokens.access_token),\n        refreshToken: tokens.refresh_token ? encrypt(tokens.refresh_token) : null\n      }\n    });\n    return oauthAccount.user;\n  }\n\n  // IMPORTANT: Only link to existing account if email is verified\n  if (oauthUser.email && oauthUser.emailVerified) {\n    const existingUser = await prisma.user.findUnique({\n      where: { email: oauthUser.email }\n    });\n\n    if (existingUser) {\n      // Link OAuth account to existing user\n      await prisma.oAuthAccount.create({\n        data: {\n          provider,\n          providerId: oauthUser.providerId,\n          userId: existingUser.id,\n          accessToken: encrypt(tokens.access_token),\n          refreshToken: tokens.refresh_token ? encrypt(tokens.refresh_token) : null\n        }\n      });\n      return existingUser;\n    }\n  }\n\n  // Create new user\n  const user = await prisma.user.create({\n    data: {\n      email: oauthUser.email,\n      emailVerified: oauthUser.emailVerified ? new Date() : null,\n      name: oauthUser.name,\n      avatar: oauthUser.avatar,\n      oauthAccounts: {\n        create: {\n          provider,\n          providerId: oauthUser.providerId,\n          accessToken: encrypt(tokens.access_token),\n          refreshToken: tokens.refresh_token ? encrypt(tokens.refresh_token) : null\n        }\n      }\n    }\n  });\n\n  return user;\n}\n",[75,129,127],{"__ignoreMap":77},[56,131,133,137],{"number":132},"6",[60,134,136],{"id":135},"schema-for-oauth-accounts","Schema for OAuth accounts",[68,138,141],{"className":139,"code":140,"language":73},[71],"// Prisma schema\nmodel User {\n  id            String    @id @default(cuid())\n  email         String?   @unique\n  emailVerified DateTime?\n  name          String?\n  avatar        String?\n  oauthAccounts OAuthAccount[]\n  sessions      Session[]\n}\n\nmodel OAuthAccount {\n  id           String  @id @default(cuid())\n  provider     String  // 'google', 'github', etc.\n  providerId   String  // ID from the OAuth provider\n  userId       String\n  user         User    @relation(fields: [userId], references: [id], onDelete: Cascade)\n  accessToken  String  // Encrypted\n  refreshToken String? // Encrypted\n\n  @@unique([provider, providerId])\n  @@index([userId])\n}\n",[75,142,140],{"__ignoreMap":77},[144,145,146,149],"warning-box",{},[18,147,148],{},"OAuth Security Mistakes to Avoid:",[31,150,151,158,163,168,173,178],{},[34,152,153,157],{},[154,155,156],"strong",{},"Never"," skip state validation - it prevents CSRF attacks",[34,159,160,162],{},[154,161,156],{}," use implicit flow for web apps - use authorization code with PKCE",[34,164,165,167],{},[154,166,156],{}," trust unverified emails for account linking - attackers can register with victim's email",[34,169,170,172],{},[154,171,156],{}," expose client secrets to the frontend",[34,174,175,177],{},[154,176,156],{}," use wildcard redirect URIs - exact match only",[34,179,180,183],{},[154,181,182],{},"Always"," verify tokens on your server, not the client",[44,185,187],{"id":186},"how-to-verify-it-worked","How to Verify It Worked",[189,190,191,197,203,209],"ol",{},[34,192,193,196],{},[154,194,195],{},"Test state validation:"," Modify the state parameter in callback URL - should fail",[34,198,199,202],{},[154,200,201],{},"Test redirect URI:"," Try different redirect URIs - should fail",[34,204,205,208],{},[154,206,207],{},"Test account linking:"," Create accounts with same email different providers",[34,210,211,214],{},[154,212,213],{},"Inspect tokens:"," Verify access tokens are encrypted in database",[44,216,218],{"id":217},"common-errors-troubleshooting","Common Errors & Troubleshooting",[220,221,223],"h4",{"id":222},"error-redirect_uri_mismatch","Error: \"redirect_uri_mismatch\"",[18,225,226],{},"The redirect URI doesn't match what's registered. Check exact match including trailing slashes.",[220,228,230],{"id":229},"error-invalid_grant","Error: \"invalid_grant\"",[18,232,233],{},"The authorization code was already used or expired. Codes are single-use.",[220,235,237],{"id":236},"state-mismatch","State mismatch",[18,239,240],{},"Session was lost between redirect and callback. Check session configuration and cookie settings.",[220,242,244],{"id":243},"email-not-returned","Email not returned",[18,246,247],{},"Some providers don't return email in profile. Request email scope and fetch from separate endpoint (like GitHub).",[249,250,251,258,264],"faq-section",{},[252,253,255],"faq-item",{"question":254},"Should I store OAuth refresh tokens?",[18,256,257],{},"Only if you need to access the provider's API on behalf of the user (like accessing their Google Drive). For simple authentication, you don't need to store tokens after creating the session.",[252,259,261],{"question":260},"How do I handle account linking for existing users?",[18,262,263],{},"If a user is logged in and wants to add another OAuth provider, link directly to their account. If not logged in, only link by email if the email is verified by the OAuth provider.",[252,265,267],{"question":266},"Why PKCE if I'm using server-side code?",[18,268,269],{},"PKCE adds defense in depth. It protects against authorization code interception, which can happen even in server-side flows. Modern OAuth best practices recommend PKCE for all clients.",[18,271,272,275,280,281,280,285],{},[154,273,274],{},"Related guides:",[276,277,279],"a",{"href":278},"/blog/how-to/nextauth-setup","NextAuth Setup"," ·\n",[276,282,284],{"href":283},"/blog/how-to/session-management","Session Management",[276,286,288],{"href":287},"/blog/how-to/magic-links","Magic Links",[290,291,292,298,303],"related-articles",{},[293,294],"related-card",{"description":295,"href":296,"title":297},"Step-by-step guide to securing your Drizzle ORM setup. Safe SQL queries, input validation, and access control patterns f","/blog/how-to/drizzle-security","How to Secure Drizzle ORM",[293,299],{"description":300,"href":301,"title":302},"Complete guide to environment variables for web apps. Learn how to set up .env files, access variables in code, and conf","/blog/how-to/environment-variables","How to Use Environment Variables - Complete Guide",[293,304],{"description":305,"href":306,"title":307},"Step-by-step guide to securing file uploads. File type validation, size limits, storage security, malware scanning, and ","/blog/how-to/file-upload-security","How to Secure File Uploads",{"title":77,"searchDepth":309,"depth":309,"links":310},2,[311,312,321,322],{"id":46,"depth":309,"text":47},{"id":53,"depth":309,"text":54,"children":313},[314,316,317,318,319,320],{"id":62,"depth":315,"text":63},3,{"id":83,"depth":315,"text":84},{"id":96,"depth":315,"text":97},{"id":109,"depth":315,"text":110},{"id":122,"depth":315,"text":123},{"id":135,"depth":315,"text":136},{"id":186,"depth":309,"text":187},{"id":217,"depth":309,"text":218},"how-to","2026-01-20","2026-01-28","Step-by-step guide to implementing OAuth 2.0 securely. Use PKCE, validate tokens properly, and avoid common OAuth vulnerabilities.",false,"md",null,"yellow",{},true,"Implement OAuth 2.0 with security best practices.","/blog/how-to/oauth-setup","[object Object]","HowTo",{"title":5,"description":326},{"loc":334},"blog/how-to/oauth-setup",[],"summary_large_image","bArO8izcNo_UG6hXFQoD6A5EeBj_XuDisRma7Kkp4-s",1775843928132]