Referensi cepat OAuth 2.0 dan OIDC. Authorization flows, PKCE, token types, JWT, scopes, dan integrasi dengan Auth.js/Passport. Perfect buat developer yang bangun authentication.
| Role | Deskripsi | Contoh |
|---|---|---|
| Resource Owner | User yang punya data | Kamu |
| Client | App yang minta akses | Web app kamu |
| Authorization Server | Server yang issue token | Google, Auth0, Keycloak |
| Resource Server | API yang punya data | Google Drive API |
OAuth 2.0: Authorization (bolehkan akses ke data)
OIDC: Authentication (siapa user ini) = OAuth 2.0 + ID TokenFlow paling aman dan paling umum untuk web apps dengan backend.
1. User click "Login with Google"
2. Redirect ke Google authorize endpoint
GET https://accounts.google.com/o/oauth2/v2/auth?
response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://app.com/callback
&scope=openid email profile
&state=RANDOM_STRING
3. User consent di Google
4. Google redirect balik ke app dengan code
GET https://app.com/callback?code=AUTH_CODE&state=RANDOM_STRING
5. App exchange code untuk token (server-to-server)
POST https://oauth2.googleapis.com/token
grant_type=authorization_code
&code=AUTH_CODE
&redirect_uri=https://app.com/callback
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
6. Token response
{
"access_token": "...",
"refresh_token": "...",
"id_token": "...",
"expires_in": 3600,
"token_type": "Bearer"
}PKCE (Proof Key for Code Exchange) untuk apps yang ngga bisa simpan client_secret (SPA, mobile).
// Step 1: Generate code_verifier dan code_challenge
const codeVerifier = generateRandomString(64); // random string
const codeChallenge = base64url(sha256(codeVerifier));
// Step 2: Redirect ke authorize dengan code_challenge
const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?` +
`response_type=code` +
`&client_id=${CLIENT_ID}` +
`&redirect_uri=${REDIRECT_URI}` +
`&scope=openid email profile` +
`&state=${state}` +
`&code_challenge=${codeChallenge}` +
`&code_challenge_method=S256`;
// Step 3: Exchange code + verifier untuk token
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: authCode,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
code_verifier: codeVerifier, // Tidak ada client_secret!
}),
});
// Helper functions
function generateRandomString(length) {
const array = new Uint8Array(length);
crypto.getRandomValues(array);
return base64url(array);
}
async function sha256(buffer) {
const hash = await crypto.subtle.digest('SHA-256', buffer);
return new Uint8Array(hash);
}
function base64url(input) {
const bytes = typeof input === 'string'
? new TextEncoder().encode(input)
: input;
return btoa(String.fromCharCode(...bytes))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}Untuk service-to-service communication tanpa user.
# Token request (no user involved)
POST https://oauth.example.com/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id=SERVICE_ID
&client_secret=SERVICE_SECRET
&scope=read:users write:users// Service A calling Service B
const tokenResponse = await fetch(`${AUTH_SERVER}/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
scope: 'read:users',
}),
});
const { access_token } = await tokenResponse.json();
// Use token to call API
const apiResponse = await fetch('https://api.service-b.com/users', {
headers: { Authorization: `Bearer ${access_token}` },
});// Access token expired, gunakan refresh token buat dapat token baru
const response = await fetch(`${AUTH_SERVER}/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: storedRefreshToken,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET, // atau PKCE
}),
});
const { access_token, refresh_token, expires_in } = await response.json();
// Simpan token baru, replace refresh_token lamaToken yang dipake buat akses API. Biasanya pendek (1 jam).
# Bearer token di Authorization header
GET /api/users/me
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...JWT yang berisi user identity info. Khusus OIDC, bukan OAuth murni.
{
"iss": "https://accounts.google.com",
"sub": "123456789",
"email": "user@gmail.com",
"email_verified": true,
"name": "Galih Pratama",
"picture": "https://...",
"iat": 1719000000,
"exp": 1719003600
}Token buat dapat access token baru. Lebih panjang umurnya (minggu/bulan).
header.payload.signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJlbWFpbCI6ImhpQGdhbGlo...import jwt from 'jsonwebtoken';
// Sign JWT (server side)
const token = jwt.sign(
{ userId: 123, email: 'hi@galihpratama.com', role: 'admin' },
process.env.JWT_SECRET,
{
expiresIn: '1h',
issuer: 'myapp.com',
audience: 'myapp.com',
}
);
// Verify JWT
try {
const payload = jwt.verify(token, process.env.JWT_SECRET, {
issuer: 'myapp.com',
audience: 'myapp.com',
});
console.log(payload.userId); // 123
} catch (err) {
if (err.name === 'TokenExpiredError') {
// Token expired
} else if (err.name === 'JsonWebTokenError') {
// Invalid token
}
}
// Decode tanpa verify (untuk debugging, JANGAN untuk auth)
const decoded = jwt.decode(token);import jwt from 'jsonwebtoken';
import { readFileSync } from 'fs';
const privateKey = readFileSync('private.pem');
const publicKey = readFileSync('public.pem');
// Sign dengan private key
const token = jwt.sign({ userId: 123 }, privateKey, {
algorithm: 'RS256',
expiresIn: '1h',
});
// Verify dengan public key (bisa didistribusikan)
const payload = jwt.verify(token, publicKey, { algorithms: ['RS256'] });openid - OIDC authentication
profile - nama, foto profil
email - alamat email
email_verified - status verifikasi email
offline_access - minta refresh token
read:users - baca data user (custom scope)
write:users - tulis data user (custom scope)// Keycloak / Auth0: define scopes di admin panel
// Request multiple scopes (space-separated)
const authUrl = `...&scope=openid profile email read:posts write:posts`;
// Check scope di API middleware
function requireScope(requiredScope) {
return (req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '');
const payload = jwt.verify(token, PUBLIC_KEY);
const scopes = payload.scope?.split(' ') || [];
if (!scopes.includes(requiredScope)) {
return res.status(403).json({ error: 'Insufficient scope' });
}
req.user = payload;
next();
};
}
// Usage
app.get('/api/posts', requireScope('read:posts'), getPosts);
app.post('/api/posts', requireScope('write:posts'), createPost);// Auto-discover OIDC endpoints
const config = await fetch(
'https://accounts.google.com/.well-known/openid-configuration'
);
const oidcConfig = await config.json();
console.log(oidcConfig.authorization_endpoint); // authorize URL
console.log(oidcConfig.token_endpoint); // token URL
console.log(oidcConfig.userinfo_endpoint); // userinfo URL
console.log(oidcConfig.jwks_uri); // public keys URL
console.log(oidcConfig.end_session_endpoint); // logout URL// Setelah dapat access_token, fetch user info
const userInfoResponse = await fetch(oidcConfig.userinfo_endpoint, {
headers: { Authorization: `Bearer ${access_token}` },
});
const userInfo = await userInfoResponse.json();
// {
// "sub": "123456789",
// "email": "user@gmail.com",
// "email_verified": true,
// "name": "Galih Pratama",
// "picture": "https://..."
// }// auth.ts
import NextAuth from 'next-auth';
import Google from 'next-auth/providers/google';
import GitHub from 'next-auth/providers/github';
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
authorization: {
params: {
scope: 'openid email profile',
prompt: 'select_account',
},
},
}),
GitHub({
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
}),
],
session: {
strategy: 'jwt', // atau 'database'
maxAge: 30 * 24 * 60 * 60, // 30 hari
},
callbacks: {
async jwt({ token, account, user }) {
// Pertama kali login
if (account && user) {
token.accessToken = account.access_token;
token.refreshToken = account.refresh_token;
token.provider = account.provider;
}
return token;
},
async session({ session, token }) {
// Kirim token ke client session
session.accessToken = token.accessToken;
return session;
},
},
pages: {
signIn: '/login',
error: '/auth/error',
},
});// app/api/auth/[...nextauth]/route.ts
import { handlers } from '@/auth';
export const { GET, POST } = handlers;// Server component: get session
import { auth } from '@/auth';
export default async function ProfilePage() {
const session = await auth();
if (!session) redirect('/login');
return <div>Hello, {session.user?.name}</div>;
}
// Client component: useSession hook
'use client';
import { useSession } from 'next-auth/react';
function Header() {
const { data: session, status } = useSession();
if (status === 'loading') return <Loading />;
if (!session) return <LoginButton />;
return <div>Welcome, {session.user?.name}</div>;
}import passport from 'passport';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: '/auth/google/callback',
}, (accessToken, refreshToken, profile, done) => {
// Find or create user
User.findOrCreate({ googleId: profile.id }, (err, user) => {
return done(err, user);
});
}));
// Routes
app.get('/auth/google',
passport.authenticate('google', { scope: ['profile', 'email'] })
);
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(req, res) => {
res.redirect('/dashboard');
}
);
// Protect routes
app.get('/dashboard',
(req, res, next) => {
if (!req.isAuthenticated()) return res.redirect('/login');
next();
},
dashboardHandler
);import jwt from 'express-jwt';
import jwksRsa from 'jwks-rsa';
// Verify JWT dari Auth0/Keycloak/etc
const checkJwt = jwt({
secret: jwksRsa.expressJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: 'https://your-auth-server/.well-known/jwks.json',
}),
audience: 'your-api-identifier',
issuer: 'https://your-auth-server/',
algorithms: ['RS256'],
});
app.get('/api/protected', checkJwt, (req, res) => {
res.json({ message: 'Authenticated!', user: req.auth.sub });
});// 1. Selalu pakai HTTPS
// 2. Simpan state parameter untuk CSRF protection
const state = crypto.randomUUID();
// Simpan state di session, verify saat callback
// 3. Validate redirect_uri
const allowedRedirects = [
'https://app.com/callback',
'http://localhost:3000/callback', // dev only
];
// 4. Use HttpOnly cookies for tokens
res.cookie('access_token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 3600000, // 1 hour
});
// 5. Short-lived access tokens
jwt.sign(payload, secret, { expiresIn: '15m' }); // 15 menit
// 6. Long-lived refresh tokens with rotation
// Setiap refresh = generate refresh token baru, invalidate yang lama
// 7. Scope principle of least privilege
// Minta scope minimal yang dibutuhkan
// 8. Implement token revocation
app.post('/api/revoke', async (req, res) => {
await tokenStore.revoke(req.body.token);
res.status(200).send();
});Login atau daftar akun gratis untuk membaca cheat sheet ini.