Best Practices

AI Rules for Social Authentication

AI implements social login by storing provider tokens in localStorage with no account linking strategy. Rules for NextAuth/Auth.js provider setup, account linking, profile data sync, token refresh, and login button UX.

7 min read·March 19, 2025

Custom OAuth implementation, localStorage tokens, auto-merge on email — account takeover in 3 clicks

NextAuth providers, verified account linking, profile sync, token management, branded buttons

AI Implements Social Login from Scratch

AI generates social authentication with: custom OAuth implementation (reimplements the entire OAuth flow instead of using a library), provider tokens stored in localStorage (XSS-readable, no refresh mechanism), no account linking strategy (user signs up with email, later tries Google login with the same email — creates a duplicate account), no profile sync (user changes their Google profile picture — your app still shows the old one), and generic buttons (a plain blue button saying "Login" with no provider branding). Each of these is a solved problem with standard libraries.

Modern social authentication is: library-managed (NextAuth/Auth.js, Passport.js, Lucia — handle the entire OAuth flow), securely stored (session cookies or httpOnly tokens, never localStorage), account-linked (multiple providers can be linked to one account with verification), profile-synced (name, email, avatar updated on each login from the provider), and branded (Google sign-in button follows Google branding guidelines, GitHub button is dark with the Octocat). AI generates none of these.

These rules cover: NextAuth provider configuration, safe account linking, profile data sync, provider token management, multi-provider support, and branded login button UX.

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
💡 15 Lines vs 300 Lines

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.

⚠️ Auto-Merge = Account Takeover

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
ℹ️ Scary Permissions = Lower Conversion

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.

Rule 5: Branded Login Button UX

The rule: 'Follow each provider branding guidelines for login buttons. Google: white button with Google logo and "Sign in with Google" text (Google Sign-In Branding Guidelines). GitHub: dark button with Octocat logo. Apple: black or white button with Apple logo and "Sign in with Apple" (required by App Store guidelines). Generic buttons ("Login with Social" or unbranded blue buttons) violate provider terms and confuse users who do not recognize the button as a familiar OAuth flow.'

For the login page layout: 'Social login buttons above the email/password form: users see the easiest option first (one click vs filling a form). Separator: "or" divider between social buttons and email form. Button order: most popular provider first (typically Google, then GitHub for developer tools, then Apple). Each button on its own line (not inline — easier to tap on mobile). Loading state on button click: show a spinner inside the button while redirecting to the provider (prevents double-clicks and confirms something is happening).'

AI generates: three identical blue buttons labeled "Google", "GitHub", "Apple" with no logos, no branding, and no loading state. The user does not trust unbranded buttons (is this a phishing attempt?). Branded buttons with provider logos: the user recognizes the familiar Google/GitHub/Apple sign-in flow, trusts the button, and clicks with confidence. Provider branding is trust signaling, not just aesthetics.

Complete Social Authentication Rules Template

Consolidated rules for social authentication.

  • NextAuth/Auth.js: 15 lines of config vs 300 lines of custom OAuth — use the library
  • No auto-merge on email: verify existing account ownership before linking providers
  • Profile sync on login: name, avatar updated automatically — selective sync rules
  • Server-side token storage: refresh automatically, never expose tokens to client
  • Minimal scopes on login, incremental scope requests for additional features
  • Branded buttons: Google/GitHub/Apple logos and text per provider guidelines
  • Social buttons above email form: easiest option first, 'or' separator, loading state
  • Provider-specific branding is trust signaling: users recognize familiar OAuth flows