OAuth & social login
Status: 🟩 COMPLETE Last updated: 2026-06-19 Plain-English tagline: “Sign in with Google” — that whole flow. A protocol that lets you delegate authentication to a trusted provider, so users don’t make yet another password. Surprisingly intricate; mostly handled by libraries.
In plain English
When a website lets you click “Sign in with Google” instead of creating yet another password, that flow is OAuth under the hood. Specifically, OAuth 2.0 — the second major version, in widespread use since 2012.
The plain-English idea: instead of asking you for your Google password (which they shouldn’t have), the website redirects you to Google. You log into Google (or you’re already logged in). Google asks: “do you want to let app.com see your email and basic profile?” You click yes. Google sends you back to app.com with a special code. app.com exchanges that code for an access token that proves “Google verifies this is George with email george@example.com.”
The website never sees your Google password. Google never sees the website’s data. You get logged in without making a new account. Everyone wins.
This is the most common use of OAuth — “delegated authentication.” OAuth was originally designed for “delegated authorization” (let one app act on another app’s behalf — e.g. “let Buffer post to your Twitter”), but the auth use case is what most people encounter.
OAuth is famously complex if you look at the spec. The good news: in 2026 you almost never implement OAuth from scratch. Supabase Auth, NextAuth, Clerk, Auth0 — every auth service handles it. You configure the provider (paste a client ID and secret) and it works.
Why it matters
- Users hate passwords. Social login dramatically improves signup conversion. People will click “Sign in with Google” who would have abandoned an email/password signup.
- You don’t store passwords. The provider does. One less thing to leak. One less attack surface.
- Strong authentication for free. If your users have 2FA on Google, your app benefits indirectly.
- Standard protocol. OAuth is implemented identically by Google, GitHub, Apple, Microsoft, Discord, Slack, and a hundred others. Configure each; mechanism’s the same.
- Foundation of API authorization. Beyond user login, OAuth is how Stripe, GitHub Apps, Notion integrations, and most modern third-party APIs grant access.
The core flow — Authorization Code with PKCE
The modern, secure OAuth flow. Step by step:
1. User clicks “Sign in with Google” on your app
Browser navigates to:
https://accounts.google.com/o/oauth2/v2/auth?
client_id=YOUR_CLIENT_ID&
redirect_uri=https://yourapp.com/auth/callback&
response_type=code&
scope=openid+email+profile&
state=RANDOM_VALUE&
code_challenge=HASH_OF_VERIFIER&
code_challenge_method=S256
Parameters:
client_id— your app’s public identifier with Googleredirect_uri— where Google will send the user after authresponse_type=code— “give me a code I can exchange for a token”scope— what you’re asking permission for (email, profile, calendar, etc.)state— random value to prevent CSRF; you verify it on the callbackcode_challenge+code_challenge_method— PKCE (Proof Key for Code Exchange), prevents code-theft attacks
2. Google authenticates the user, asks for consent
User logs into Google if not already. Google shows: “yourapp.com would like to access: your email, profile picture, name. Allow / Deny.”
3. User clicks Allow
Google redirects back:
https://yourapp.com/auth/callback?
code=4/0AY0...&
state=RANDOM_VALUE
4. Your app verifies state and exchanges code for tokens
Your server (NOT the browser) makes a back-channel request to Google:
POST https://oauth2.googleapis.com/token
{
client_id, client_secret,
code, redirect_uri,
grant_type: "authorization_code",
code_verifier: ORIGINAL_VERIFIER
}
Google verifies the code + PKCE verifier and responds with:
{
"access_token": "ya29.a0...",
"id_token": "eyJhbGc...", // a JWT containing user info
"refresh_token": "1//04...",
"expires_in": 3600
}5. Your app reads user info from the ID token
The id_token is a JWT signed by Google containing the user’s email, name, sub (unique ID), etc.
6. Your app issues its own session
You don’t carry Google’s tokens around. You issue your own session cookie / JWT that says “this is George, authenticated via Google.”
That’s the whole flow. Six steps, mostly handled by libraries.
OAuth vs OpenID Connect
A common point of confusion:
- OAuth 2.0 is authorization. It produces an
access_tokenyou can use to call APIs on the user’s behalf. - OpenID Connect (OIDC) is an authentication layer on top of OAuth 2.0. It adds the
id_token(a JWT with user info) so you can identify the user.
For “Sign in with Google,” you’re using OIDC (OAuth + identity). The provider returns an id_token alongside the access token. Modern auth providers implement OIDC; “OAuth” and “OIDC” are often used interchangeably in casual speech.
A concrete example with Supabase
Supabase Auth handles OAuth providers with about three lines of code:
Configure in Supabase dashboard
Authentication → Providers → Google → enable, paste client_id and client_secret from Google Cloud Console.
Trigger sign-in from your app
"use client";
import { createClient } from "@/lib/supabase-client";
export function GoogleSignIn() {
const supabase = createClient();
async function signIn() {
await supabase.auth.signInWithOAuth({
provider: "google",
options: {
redirectTo: `${window.location.origin}/auth/callback`
}
});
}
return <button onClick={signIn}>Sign in with Google</button>;
}Handle the callback
// app/auth/callback/route.ts
import { NextResponse } from "next/server";
import { createClient } from "@/lib/supabase-server";
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const code = searchParams.get("code");
if (code) {
const supabase = await createClient();
await supabase.auth.exchangeCodeForSession(code);
}
return NextResponse.redirect(`${origin}/`);
}Supabase handles the PKCE, the token exchange, the session cookie. You write the button and the callback handler.
Scopes — what you’re asking for
A scope is a permission. Common scopes:
| Scope | Means |
|---|---|
openid | ”I want OIDC — give me an id_token” |
email | ”I want the user’s email” |
profile | ”I want basic profile info (name, picture)“ |
https://www.googleapis.com/auth/calendar.readonly | ”Read user’s calendar” |
repo (GitHub) | “Access user’s repositories” |
tweet.write (X) | “Post tweets as user” |
Ask for the minimum necessary. Users see the consent screen; asking for read my entire Drive when you just need email is suspicious and reduces conversion.
For simple “Sign in with X,” openid email profile is usually all you need.
Refresh tokens in OAuth
The access token usually expires in 1 hour. To avoid making the user re-authenticate, providers issue a refresh token alongside.
Your server uses the refresh token to silently get a new access token when the old one expires. Refresh tokens last weeks to months (or are long-lived).
Supabase handles refresh automatically.
Provider-specific quirks
| Provider | Notes |
|---|---|
Refresh tokens require access_type=offline and prompt=consent | |
| GitHub | Refresh tokens are opt-in (GitHub Apps); OAuth Apps don’t refresh |
| Apple | Email may be private relay; refresh tokens need separate config |
| Long-lived tokens via exchange, no traditional refresh | |
| Microsoft | Azure AD has its own quirks for tenant-restricted apps |
| Twitter/X | OAuth 2.0 is recent; OAuth 1.0a was the standard for a long time |
| Discord | Generally straightforward; PKCE supported |
Each provider has its own console, its own client ID/secret setup. The flow is identical.
Security considerations
PKCE (always)
Older OAuth flows could be attacked via authorization code interception. PKCE adds a per-request secret (the code verifier) that the attacker can’t guess. Always use PKCE.
Validate state parameter
The state parameter is random per request. If the value in the callback doesn’t match what you sent, the request was forged. Reject.
Validate redirect_uri exactly
Provider must redirect to a pre-registered URL. Don’t use wildcards. Don’t trust redirect_uri from query strings.
Validate id_token signature
If you’re handling tokens directly (not via Supabase et al.), verify the JWT signature against the provider’s public key.
Don’t expose client_secret to the browser
Client secret = signing key for token exchange. Server-side only.
Handle email privacy
Apple offers a private relay email — you might get abc123@privaterelay.appleid.com instead of the real email. Plan for this.
Account linking
A user signs in with Google one day, then GitHub the next. Are these the same user account? Decide your policy (link by email, prompt, or treat separately).
Common gotchas
-
Misconfigured redirect URI. Google rejects if the URI doesn’t exactly match what’s registered. Including/excluding trailing slashes matters.
-
CORS errors on token exchange. Token exchange is server-to-server, not client-side. Doing it in the browser breaks CORS AND exposes your client_secret.
-
Storing client_secret in client code. Catastrophic. Server-side only.
-
Forgetting to verify
state. Vulnerable to CSRF-like attacks. -
Not using PKCE. Older guides may omit it. Always use it now.
-
Ask for too many scopes. Users abandon the consent screen. Ask for minimum; request more later when needed.
-
Refresh token leak. Refresh tokens are very long-lived. Treat as super-secrets.
-
Account collision via email. Two providers report the same email → are they the same account? Usually yes, but verify the provider has verified the email.
-
Unverified emails. Some providers let users sign in with unverified emails. Attacker creates account with your verified email at provider X, signs into your app, you grant access. Always check
email_verifiedclaim. -
Locale / language redirect. Some providers redirect with
&hl=enetc. Your code shouldn’t depend on exact query string format. -
Mobile / PWA flow differences. Some providers don’t allow
localhostredirects from mobile apps; need custom URI schemes. -
Cookie SameSite issues on callback. If your auth cookies are
SameSite=Strict, the post-OAuth redirect from provider may not send them. UseLax. -
Token leak via referrer. If the auth callback URL has tokens in the query string and the page makes external requests, tokens leak via
Referer. Strip from referrer or process server-side immediately. -
Long auth flows breaking on slow connections. PKCE codes expire quickly (typically 10 min). Mobile users on bad networks can fail. Handle gracefully.
-
Pop-up vs redirect mode. Both work; popups have UX issues on mobile and can be blocked. Redirect is more reliable.
-
“Sign in with X” buttons that don’t match brand guidelines. Google, Apple etc. have specific design requirements for their buttons. Follow them or risk rejection / lawsuits.
-
Two users at same email. When a user changes their email at Provider, weirdness ensues. Use
sub(subject) as the stable ID, not email. -
Provider account compromise. If user’s Google account is hacked, the attacker can log into your app. Mitigation: support MFA, allow revocation, log unusual access.
-
Vendor lock-in. Switching auth providers later is painful. Choose deliberately. Open standards (Auth0, Supabase) are easier to migrate than proprietary ones.
-
Privacy / GDPR. Storing OAuth provider data has privacy implications. Have a privacy policy. Allow data deletion.
See also
- Authentication vs authorization đźź©
- Passwords & hashing 🟩 — OAuth skips passwords entirely
- Sessions & cookies 🟩 — where the session lands after OAuth
- JWT 🟩 —
id_tokenis a JWT - Magic links & passwordless 🟩 — another passwordless option
- CSRF 🟩 —
stateparameter prevents this - XSS 🟩 — protects OAuth tokens
- Secrets management 🟩 — client_secret is a secret
- OWASP top 10 🟩 🟦
- Supabase 🟩 🟦 — handles OAuth providers
- How-to: Add Supabase auth đźź©
- Glossary: OAuth