Implementing OAuth 2.0: A Developer's Complete Guide
OAuth confused me for years. "Why are there so many redirects? What's the difference between tokens?" If you've asked these questions, this guide is for you.
OAuth In Simple Terms
OAuth lets users log into your app using their Google/GitHub/etc. account without sharing their password with you.
The flow:
- User clicks "Login with Google"
- User goes to Google, logs in there
- Google asks "Allow this app to see your email?"
- User says yes
- Google redirects back to your app with a special code
- Your app exchanges code for tokens
- User is logged in
The Authorization Code Flow
Most common for web apps:
User → Your App → Google Authorization
↓
User authorizes → Redirect to Your App (with code)
↓
Your Backend → Exchange code for tokens → Google Token Endpoint
↓
Got access_token + id_token → User authenticated
Implementation with Next.js
1. Setup Google OAuth
Go to Google Cloud Console:
- Create OAuth 2.0 credentials
- Add authorized redirect URI:
http://localhost:3000/api/auth/callback/google
2. Environment Variables
GOOGLE_CLIENT_ID=your-client-id
GOOGLE_CLIENT_SECRET=your-client-secret
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your-random-secret
3. NextAuth Configuration
// pages/api/auth/[...nextauth].js
import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
export default NextAuth({
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
callbacks: {
async session({ session, token }) {
session.user.id = token.sub;
return session;
},
},
});
4. Using in Components
import { useSession, signIn, signOut } from 'next-auth/react';
function LoginButton() {
const { data: session } = useSession();
if (session) {
return (
<>
<p>Signed in as {session.user.email}</p>
<button onClick={() => signOut()}>Sign out</button>
</>
);
}
return <button onClick={() => signIn('google')}>Sign in with Google</button>;
}
Understanding Tokens
Access Token
- Short-lived (minutes to hours)
- Used to call APIs
- Include in requests:
Authorization: Bearer <access_token>
Refresh Token
- Long-lived (days to months)
- Used to get new access tokens
- Store securely (server-side only)
ID Token (OpenID Connect)
- Contains user info
- JWT format
- Only for authentication, not API calls
Implementing Without NextAuth
// 1. Redirect to Google
app.get('/auth/google', (req, res) => {
const params = new URLSearchParams({
client_id: process.env.GOOGLE_CLIENT_ID,
redirect_uri: 'http://localhost:3000/auth/callback',
response_type: 'code',
scope: 'openid email profile',
state: generateRandomState() // CSRF protection
});
res.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params}`);
});
// 2. Handle callback
app.get('/auth/callback', async (req, res) => {
const { code, state } = req.query;
// Verify state matches
if (state !== req.session.oauthState) {
return res.status(400).send('Invalid state');
}
// Exchange code for tokens
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
code,
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
redirect_uri: 'http://localhost:3000/auth/callback',
grant_type: 'authorization_code'
})
});
const tokens = await tokenResponse.json();
// tokens.access_token, tokens.id_token, tokens.refresh_token
// Decode id_token to get user info
const payload = JSON.parse(Buffer.from(tokens.id_token.split('.')[1], 'base64').toString());
// payload.email, payload.name, payload.picture
});
Security Checklist
- Use HTTPS in production
- Validate state parameter (CSRF protection)
- Store refresh tokens server-side only
- Validate id_token signature
- Set proper token expiration
- Handle token refresh gracefully
OAuth is standard across platforms. Learn it once, use it everywhere.