Rule 1: NextAuth/Auth.js Provider Configuration
The rule: 'Use NextAuth (Auth.js) for social login instead of implementing OAuth from scratch. Configuration: providers: [Google({ clientId, clientSecret }), GitHub({ clientId, clientSecret }), Discord({ clientId, clientSecret })]. NextAuth handles: the entire OAuth Authorization Code + PKCE flow, session management (JWT or database sessions), CSRF protection (built-in state parameter), and callback URL validation. One configuration file replaces hundreds of lines of OAuth implementation.'
For database adapter: 'Use the Drizzle adapter for NextAuth: adapter: DrizzleAdapter(db). NextAuth automatically manages: users table (created on first login), accounts table (links provider accounts to users), sessions table (for database session strategy). The adapter handles the persistence — your code handles the business logic. For JWT strategy: sessions are stateless (no database lookup per request). For database strategy: sessions are server-side (revocable, queryable).'
AI generates: 300 lines of custom OAuth implementation: authorization URL construction, callback handling, token exchange, user info fetching, session creation, CSRF protection, and error handling. Each line is a potential bug. NextAuth: 15 lines of configuration achieve the same result, tested by thousands of applications, maintained by the community. Reimplementing OAuth is reimplementing solved problems with new bugs.
- NextAuth/Auth.js: 15 lines of config vs 300 lines of custom OAuth implementation
- Built-in: Authorization Code + PKCE, state parameter, callback validation
- Drizzle adapter: automatic users, accounts, sessions table management
- JWT strategy (stateless) or database strategy (revocable) — choose per use case
- Providers: Google, GitHub, Discord, Apple, Twitter, and 60+ more built-in
Custom OAuth: 300 lines of authorization URL construction, callback handling, token exchange, user info, session creation, CSRF protection. NextAuth: 15 lines of provider configuration. Same result, battle-tested by thousands of apps, maintained by community.
Rule 2: Safe Account Linking with Verification
The rule: 'When a user signs in with a social provider and an account with that email already exists, do not auto-merge. Scenarios: (1) Same user, new provider: user signed up with email/password, now wants to add Google login. Require: enter existing password to confirm ownership, then link the Google account. (2) Different user, same email: someone else has a Google account with your email. Auto-merge = account takeover. Require: verification of existing account ownership before linking any new provider.'
For NextAuth account linking: 'NextAuth events and callbacks control linking behavior: callbacks: { async signIn({ user, account, profile }) { const existingUser = await db.query.users.findFirst({ where: eq(users.email, user.email) }); if (existingUser && !existingUser.emailVerified) return "/auth/verify-to-link"; return true; } }. Custom logic at the signIn callback determines whether to allow the login, redirect to a verification step, or block the attempt. The default NextAuth behavior creates a new account per provider — customize for linking.'
AI generates: const user = await db.users.findOne({ email: profile.email }); if (user) linkAccount(user, providerAccount); — auto-merge on email match. Attacker creates a Google account with the victim email, signs in via OAuth, gains access to the victim account. One line of auto-merge = account takeover vulnerability. Verification before linking: one extra step for legitimate users, complete prevention of this attack vector.
Attacker creates Google account with victim email, signs in via OAuth, app auto-merges on email match. Attacker owns the victim account. One line of auto-merge = one account takeover vulnerability. Verify existing account ownership before linking any provider.
Rule 3: Profile Data Sync on Login
The rule: 'Update the user profile with the latest provider data on each login: name, avatar, and email (if changed and verified). NextAuth events: events: { async signIn({ user, profile }) { await db.update(users).set({ name: profile.name, image: profile.picture, updatedAt: new Date() }).where(eq(users.id, user.id)); } }. The user changes their Google profile picture: next login, your app shows the new picture automatically. No manual profile update needed.'
For selective sync: 'Not all provider data should overwrite local data. Rules: (1) Name: sync from provider unless the user has manually set a custom name in your app. (2) Avatar: always sync from provider (users expect their Google/GitHub avatar). (3) Email: sync only if the provider email is verified AND the user has not set a different email in your app. (4) Never sync: role, permissions, subscription status, or any business-logic fields. Provider data informs the profile; it does not control access.'
AI generates: profile data fetched once on first login, never updated. The user changes their name on Google: your app shows the old name forever. The avatar URL expires after 30 days: your app shows a broken image. With sync on login: the profile stays current automatically. The user never has to manually update their name or re-upload their avatar in your app.
Rule 4: Provider Token Management for API Access
The rule: 'If your app needs to access provider APIs on behalf of the user (read GitHub repos, access Google Calendar), store the OAuth access token and refresh token securely. NextAuth: the account record stores access_token and refresh_token. Refresh expired tokens: callbacks: { async session({ session, token }) { if (token.accessTokenExpires < Date.now()) { const refreshed = await refreshAccessToken(token); } return session; } }. Never expose provider tokens to the client — make API calls from your server using the stored tokens.'
For scope management: 'Request only the scopes you need: Google sign-in needs openid, email, profile (default). Accessing Google Calendar additionally needs https://www.googleapis.com/auth/calendar.readonly. Do not request calendar scope for users who do not use the calendar feature — the permission dialog is scarier with more scopes (lower conversion). Request additional scopes incrementally: sign-in with basic scopes, request calendar scope when the user enables the calendar integration.'
AI generates: access_token stored in localStorage, no refresh mechanism. The token expires in 1 hour; the user must re-authenticate. Or: all scopes requested on initial login (read + write access to repos, email, profile, organizations) when the app only needs to read the username. Over-scoping: lower sign-up conversion and higher security risk if tokens are compromised. Minimal scopes + incremental requests: higher conversion, lower risk.
- Store tokens server-side: NextAuth account record, never localStorage
- Refresh expired tokens automatically: callback checks expiry, refreshes transparently
- Minimal scopes: openid email profile for login, additional scopes incrementally
- Incremental scope: request calendar access only when user enables calendar feature
- Server-side API calls: use stored tokens from your backend, never expose to client
Requesting repo write + email + org access for a login button: the permission dialog is terrifying. Users abandon. Minimal scopes (openid email profile) for login, request calendar/repo access only when the user enables that specific feature. Higher conversion, lower risk.
Complete Social Authentication Rules Template
Consolidated rules for social authentication.