OAuth 2.0 Is Not Authentication
OAuth 2.0 is an authorization framework. It answers: "Can application X access resource Y on behalf of user Z?"
OpenID Connect (OIDC) layers authentication on top: "Who is this user?"
Most developers use both without realizing it.
The Four Flows
1. Authorization Code Flow (Web Apps)
The standard flow for web applications with a backend.
Browser → Your App → GitHub/Google ("Allow access?") → Your App (with code) → Exchange code for token
// Step 1: Redirect user to provider
app.get('/auth/github', (req, res) => {
const state = generateRandomString(16); // CSRF protection
req.session.oauthState = state;
const params = new URLSearchParams({
client_id: process.env.GITHUB_CLIENT_ID!,
redirect_uri: `${process.env.APP_URL}/auth/github/callback`,
scope: 'read:user user:email',
state,
});
res.redirect(`https://github.com/login/oauth/authorize?${params}`);
});
// Step 2: Handle callback with code
app.get('/auth/github/callback', async (req, res) => {
const { code, state } = req.query;
// Verify state to prevent CSRF
if (state !== req.session.oauthState) {
return res.status(400).send('Invalid state');
}
// Exchange code for token (server-to-server)
const tokenResponse = await fetch('https://github.com/login/oauth/access_token', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify({
client_id: process.env.GITHUB_CLIENT_ID,
client_secret: process.env.GITHUB_CLIENT_SECRET, // never exposed to browser
code,
}),
}).then(r => r.json());
const { access_token } = tokenResponse;
// Fetch user info
const user = await fetch('https://api.github.com/user', {
headers: { Authorization: `Bearer ${access_token}` },
}).then(r => r.json());
// Create or find user in your DB
const dbUser = await upsertUser({ githubId: user.id, email: user.email, name: user.name });
// Create your own session
req.session.userId = dbUser.id;
res.redirect('/dashboard');
});
2. PKCE Flow (SPAs and Mobile)
Same as Authorization Code but without a client secret (safe for public clients).
// Generate code verifier and challenge
function generatePKCE() {
const verifier = generateRandomString(64);
const challenge = base64url(sha256(verifier));
return { verifier, challenge };
}
// Step 1: Redirect with challenge
const { verifier, challenge } = generatePKCE();
sessionStorage.setItem('pkce_verifier', verifier);
const params = new URLSearchParams({
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: 'openid profile email',
code_challenge: challenge,
code_challenge_method: 'S256',
});
window.location.href = `https://provider.com/auth?${params}`;
// Step 2: Exchange code + verifier (no secret needed)
const response = await fetch('https://provider.com/token', {
method: 'POST',
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: CLIENT_ID,
code,
code_verifier: sessionStorage.getItem('pkce_verifier')!,
redirect_uri: REDIRECT_URI,
}),
});
3. Client Credentials (Machine-to-Machine)
No user involved. Service authenticates as itself.
async function getServiceToken(): Promise<string> {
const response = await fetch('https://auth.example.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.SERVICE_CLIENT_ID!,
client_secret: process.env.SERVICE_CLIENT_SECRET!,
audience: 'https://api.example.com',
}),
}).then(r => r.json());
return response.access_token;
}
// Use in service-to-service calls
const token = await getServiceToken();
const data = await fetch('https://api.example.com/data', {
headers: { Authorization: `Bearer ${token}` },
}).then(r => r.json());
Using NextAuth.js (The Easy Path)
// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth';
import GitHub from 'next-auth/providers/github';
import Google from 'next-auth/providers/google';
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
GitHub({ clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET! }),
Google({ clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET! }),
],
callbacks: {
async signIn({ user, account }) {
// Custom logic: block certain domains, etc.
return true;
},
async session({ session, token }) {
session.user.id = token.sub!;
return session;
},
},
});
NextAuth handles the code exchange, token storage, session management, and CSRF protection automatically. Use it unless you have a specific reason to implement OAuth yourself.
The Key Security Rules
-
Always validate
state— prevents CSRF attacks - Never expose client secrets to browsers — use Authorization Code + PKCE for SPAs
- Use short-lived access tokens — 15 minutes to 1 hour
- Store refresh tokens securely — httpOnly cookies, not localStorage
- Always use HTTPS — tokens in query params are logged by servers
NextAuth.js with GitHub, Google, and email magic links pre-configured: Whoff Agents AI SaaS Starter Kit.
Top comments (0)