OAuth 2.0: Let Users Log In Without Storing Passwords
OAuth 2.0 is a delegation protocol. Instead of your app collecting and storing user passwords, you redirect them to Google/GitHub/etc., they authenticate there, and come back with a token. Your app never sees the password.
Key Terms
| Term | Meaning |
|---|---|
| Resource Owner | The user |
| Client | Your application |
| Authorization Server | Google, GitHub, Auth0 — who authenticates the user |
| Resource Server | The API that has the user's data |
| Access Token | Short-lived token to access APIs |
| Refresh Token | Long-lived token to get new access tokens |
| Scope | What permissions the token grants (e.g., read:email) |
Flow 1: Authorization Code + PKCE (Recommended)
For web apps, SPAs, mobile apps. The most secure flow for user-facing applications.
1. Your app redirects the user to the authorization server:
GET https://auth.example.com/authorize?
response_type=code&
client_id=YOUR_CLIENT_ID&
redirect_uri=https://yourapp.com/callback&
scope=openid email profile&
state=random_csrf_token&
code_challenge=SHA256(code_verifier)&
code_challenge_method=S256
2. User logs in and consents
3. Auth server redirects back with a code:
https://yourapp.com/callback?code=AUTH_CODE&state=random_csrf_token
4. Your server exchanges the code for tokens:
POST https://auth.example.com/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=AUTH_CODE&
redirect_uri=https://yourapp.com/callback&
client_id=YOUR_CLIENT_ID&
code_verifier=ORIGINAL_CODE_VERIFIER
5. Response:
{
"access_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "abc..."
}PKCE (Proof Key for Code Exchange) prevents authorization code interception. It's required for SPAs and mobile apps, recommended for all apps.
Flow 2: Client Credentials
For server-to-server communication. No user involved.
POST https://auth.example.com/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET&
scope=read:data
Response:
{
"access_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 3600
}Use this when your backend needs to call another API without user context (cron jobs, microservices).
Which Flow to Use
| Application Type | Flow | Notes |
|---|---|---|
| Web app (server-rendered) | Authorization Code + PKCE | Server can keep client_secret |
| SPA (React, Vue) | Authorization Code + PKCE | No client_secret (public client) |
| Mobile app | Authorization Code + PKCE | Use deep links for redirect |
| Server-to-server | Client Credentials | No user interaction |
| CLI tool | Device Code | User authorizes on another device |
Deprecated Flows (Don't Use)
- Implicit Flow: Tokens in the URL fragment. Deprecated — use Authorization Code + PKCE instead.
- Resource Owner Password Credentials: App collects username/password directly. Defeats the purpose of OAuth.
Common Implementation (Next.js + NextAuth)
// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth';
import GitHubProvider from 'next-auth/providers/github';
import GoogleProvider from 'next-auth/providers/google';
export const { handlers, auth } = NextAuth({
providers: [
GitHubProvider({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}),
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
],
});Decode your tokens: JWT Decoder — inspect OAuth access tokens and ID tokens.