belajarkoding Platform belajar web development Indonesia. Artikel, cheat sheets, roadmap, dan code challenges untuk developer Indonesia.
© 2026 BelajarKoding. All rights reserved.
Bagian dari ekosistem Galih Pratama
OAuth 2.0 & OpenID Connect Cheat Sheet 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.
JavaScript 9 min read 1.624 kata
Silakan
login atau
daftar untuk membaca cheat sheet ini.
Baca Cheat Sheet Lengkap Login atau daftar akun gratis untuk membaca cheat sheet ini.
OAuth 2.0 & OpenID Connect Cheat Sheet - BelajarKoding | BelajarKoding
# Konsep Dasar
# Roles di OAuth 2.0
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 vs OpenID Connect
OAuth 2.0: Authorization (bolehkan akses ke data)
OIDC: Authentication (siapa user ini) = OAuth 2.0 + ID Token
# Authorization Code Flow (Server-side Apps)Flow 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
# Authorization Code + PKCE (SPA & Mobile)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
# Client Credentials Flow (Machine-to-Machine)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
// 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
Token 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"
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.
# RSA / ECDSA (Asymmetric)import jwt from 'jsonwebtoken' ;
import { readFileSync } from 'fs' ;
const privateKey = readFileSync ( 'private.pem' );
const publicKey = readFileSync ( 'public.pem' );
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
// 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
// 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",
# Auth.js (NextAuth v5) di Next.js// auth.ts
import NextAuth from 'next-auth' ;
import Google from 'next-auth/providers/google' ;
import
// 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)
import passport from 'passport' ;
import { Strategy as GoogleStrategy } from 'passport-google-oauth20' ;
passport. use ( new GoogleStrategy ({
# JWT Middleware di Expressimport jwt from 'express-jwt' ;
import jwksRsa from 'jwks-rsa' ;
// Verify JWT dari Auth0/Keycloak/etc
const checkJwt = jwt ({
secret: jwksRsa. expressJwtSecret ({
cache: true
// 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 = [
Authorization Code : Kode sementara yang di-exchange untuk access token. Singkat umur (10 menit).
PKCE : Proof Key for Code Exchange. Ekstensi OAuth yang gantiin client_secret dengan code_verifier/challenge.
Access Token : Token untuk akses resource API. Biasanya pendek (15 menit - 1 jam).
Refresh Token : Token untuk dapat access token baru. Lebih panjang (hari - minggu).
ID Token : JWT yang berisi identity user. Spesifik OIDC.
Scope : Permission yang diminta client. Menentukan apa yang bisa dilakukan dengan token.
State Parameter : Random string untuk mencegah CSRF attack saat OAuth flow.
JWKS : JSON Web Key Set. Endpoint yang berisi public keys untuk verify JWT signature.
Bearer Token : Tipe token yang dikirim via Authorization header. Siapa pun yang pegang token bisa akses.
Consent : Halaman di authorization server di mana user approve atau deny akses.
Backchannel : Komunikasi server-to-server (tidak melalui browser). Contoh: token exchange.
Frontchannel : Komunikasi melalui browser. Contoh: redirect ke authorize endpoint.
&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"
}
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 , '' );
}
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 }` },
});
({
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 lama
:
1719000000
,
"exp" : 1719003600
}
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);
// 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' ] });
,
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);
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
// "email": "user@gmail.com",
// "email_verified": true,
// "name": "Galih Pratama",
// "picture": "https://..."
// }
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' ,
},
});
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 > ;
}
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
);
,
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 });
});
'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 ();
});