BelajarKoding Logobelajarkoding

Platform belajar web development Indonesia. Artikel, cheat sheets, roadmap, dan code challenges untuk developer Indonesia.

Navigasi

  • Artikel
  • Cheat Sheets
  • Roadmap
  • Challenges
  • Pricing
  • Search

Produk Lain

  • JagoHermes
  • KelasClaude
  • KilatKoding
  • BelajarVibeCoding
  • JualanKoding

Support

  • Privacy Policy
  • Terms of Service
  • Email

© 2026 BelajarKoding. All rights reserved.

Galih PratamaBagian dari ekosistem Galih Pratama
belajarkoding LogobyGalih Pratama
RoadmapArtikelCheat SheetsChallengesUpgrade
belajarkoding LogobyGalih Pratama
RoadmapArtikelCheat SheetsChallengesUpgrade
belajarkoding LogobyGalih Pratama
RoadmapArtikelCheat SheetsChallengesUpgrade

Daftar Isi

Konsep DasarRoles di OAuth 2.0OAuth 2.0 vs OpenID ConnectAuthorization FlowsAuthorization Code Flow (Server-side Apps)Authorization Code + PKCE (SPA & Mobile)Client Credentials Flow (Machine-to-Machine)Refresh Token FlowToken TypesAccess TokenID Token (OIDC)Refresh TokenJWT (JSON Web Token)Struktur JWTDecode dan Verify JWTRSA / ECDSA (Asymmetric)ScopesScope yang UmumScope di Auth ServerOIDC EndpointsDiscovery (Well-known)UserInfo EndpointIntegrasi LibraryAuth.js (NextAuth v5) di Next.jsPassport.js di ExpressJWT Middleware di ExpressSecurity Best PracticesGlossary
OAuthAuthenticationSecurityJWT

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.

JavaScript9 min read1.624 kata
Silakan login atau daftar untuk membaca cheat sheet ini.

#Konsep Dasar

#Roles di OAuth 2.0

RoleDeskripsiContoh
Resource OwnerUser yang punya dataKamu
ClientApp yang minta aksesWeb app kamu
Authorization ServerServer yang issue tokenGoogle, Auth0, Keycloak
Resource ServerAPI yang punya dataGoogle Drive API

#OAuth 2.0 vs OpenID Connect

text
OAuth 2.0: Authorization (bolehkan akses ke data)
OIDC: Authentication (siapa user ini) = OAuth 2.0 + ID Token

#Authorization Flows

#Authorization Code Flow (Server-side Apps)

Flow paling aman dan paling umum untuk web apps dengan backend.

text
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"
   }

#Authorization Code + PKCE (SPA & Mobile)

PKCE (Proof Key for Code Exchange) untuk apps yang ngga bisa simpan client_secret (SPA, mobile).

javascript
// 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, '');
}

#Client Credentials Flow (Machine-to-Machine)

Untuk service-to-service communication tanpa user.

bash
# 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
javascript
// 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}` },
});

#Refresh Token Flow

javascript
// 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 lama

#Token Types

#Access Token

Token yang dipake buat akses API. Biasanya pendek (1 jam).

http
# Bearer token di Authorization header
GET /api/users/me
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...

#ID Token (OIDC)

JWT yang berisi user identity info. Khusus OIDC, bukan OAuth murni.

json
{
  "iss": "https://accounts.google.com",
  "sub": "123456789",
  "email": "user@gmail.com",
  "email_verified": true,
  "name": "Galih Pratama",
  "picture": "https://...",
  "iat": 1719000000,
  "exp": 1719003600
}

#Refresh Token

Token buat dapat access token baru. Lebih panjang umurnya (minggu/bulan).

#JWT (JSON Web Token)

#Struktur JWT

text
header.payload.signature
 
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJlbWFpbCI6ImhpQGdhbGlo...

#Decode dan Verify JWT

javascript
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);

#RSA / ECDSA (Asymmetric)

javascript
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'] });

#Scopes

#Scope yang Umum

text
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)

#Scope di Auth Server

javascript
// 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);

#OIDC Endpoints

#Discovery (Well-known)

javascript
// 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

#UserInfo Endpoint

javascript
// 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://..."
// }

#Integrasi Library

#Auth.js (NextAuth v5) di Next.js

typescript
// 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',
  },
});
typescript
// app/api/auth/[...nextauth]/route.ts
import { handlers } from '@/auth';
export const { GET, POST } = handlers;
typescript
// 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>;
}

#Passport.js di Express

javascript
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
);

#JWT Middleware di Express

javascript
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 });
});

#Security Best Practices

javascript
// 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();
});

#Glossary

  • 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.

Baca Cheat Sheet Lengkap

Login atau daftar akun gratis untuk membaca cheat sheet ini.

LoginDaftar Gratis
Share: