A detailed backend reference

Autentikasi, pertanyaan siapa Anda.

Penelusuran dari prinsip dasar tentang dua gagasan yang disentuh setiap backend tiap hari, siapa Anda (autentikasi) dan apa yang bisa Anda lakukan (otorisasi). Dari segel lilin dan rahasia bersama sampai sesi, JWT, cookie, OAuth 2.0, OpenID Connect, dan RBAC. Ditulis bukan hanya untuk menyebut apa fungsi tiap bagian, tapi mengapa ia ada dan bagaimana ia bekerja di dalamnya. Ada implementasi di Go dan Python.

Identity / Access Sessions / JWT / OAuth Go 1.22+ / Python 3.11+ 18 sections Versi Sederhana
Part I / Foundations

01 Ringkasan Dua Kalimat

Seluruh topik menyusut menjadi dua pertanyaan. Autentikasi adalah mekanisme menetapkan identitas pada subjek di dalam konteks tertentu; ia menjawab pertanyaan "siapa Anda?". Konteksnya bisa platform, sistem operasi, ponsel Anda, apa pun. Proses menemukan jawaban pertanyaan itu adalah autentikasi.

Otorisasi menjawab pertanyaan yang sama sekali berbeda: "apa yang bisa Anda lakukan?" dalam konteks itu. Kemampuan Anda, izin Anda. Proses menemukan jawaban apa yang bisa Anda lakukan di sini adalah otorisasi. Autentikasi menetapkan identitas lebih dulu; otorisasi memutuskan apa yang boleh dilakukan identitas itu setelahnya.

AUTHENTICATION Who are you? Assign an identity to a subject within a given context. -> login, signup, identity proof AUTHORIZATION What can you do? Your capabilities and permissions within that same context. -> roles, scopes, access control
FIG 1, Identity precedes permission. Authenticate first, authorize second.

02 Sejarah Autentikasi

Auth modern tidak muncul dari ketiadaan, setiap mekanisme yang kita pakai hari ini adalah keturunan gagasan yang jauh lebih tua. Menelusuri garis keturunannya membuat kompromi desainnya tampak jelas.

Masyarakat pra-industri: kepercayaan intrinsik

Autentikasi bersifat implisit. Identitas subjek sama dengan pengakuan subjek: seseorang yang sudah dihormati komunitas, tetua desa, bisa menjamin seseorang, dan kesepakatan dimeteraikan dengan jabat tangan, tindakan yang melambangkan pengakuan dan kesepakatan bersama. Elementer, tapi ia memakai bentuk autentikasi yang dibangun di atas kepercayaan kontekstual manusia. Saat populasi tumbuh dan interaksi melampaui lingkaran yang dikenal, kepercayaan implisit gagal berskala, tetua satu desa tidak dipercaya lintas negara atau benua. Ini menandai pencarian autentikasi eksplisit: bukti identitas yang berfungsi tanpa kenalan pribadi.

Abad pertengahan: segel & era kriptografis

Masyarakat butuh sistem yang bisa berskala melampaui pengenalan pribadi. Jawabannya segel lilin: pola unik yang ditekan ke dokumen dan perjanjian, bertindak sebagai tanda tangan awal. Segel adalah token autentikasi pertama yang diadopsi luas: representasi fisik identitas yang mengandalkan kepemilikan ("sesuatu yang Anda punya"). Jika Anda memegang segel, Anda terautentikasi. Tapi segel bisa dipalsukan, menandai kasus tercatat pertama serangan bypass autentikasi: melewati autentikasi dengan niat jahat. Ini mendorong mekanisme yang lebih canggih: watermark dan kode terenkripsi di dokumentasi dagang, meletakkan fondasi pemikiran kriptografis.

Revolusi industri: passphrase & rahasia bersama

Saat mesin mengubah dunia, sistem komunikasi ikut berevolusi. Telegraf menjadi infrastruktur kritis, dan bersamanya muncul kebutuhan validasi pesan yang aman. Operator memakai passphrase yang disepakati di awal: bentuk awal rahasia bersama, praktisnya kata sandi statis. Prinsipnya bergeser dari "sesuatu yang Anda miliki" (segel) ke "sesuatu yang Anda ketahui", lompatan keamanan yang berarti dan leluhur langsung kata sandi modern.

Mainframe: fase digital pertama

Pada 1961, peneliti di Project MAC MIT menggarap Compatible Time-Sharing System (CTSS) dan memperkenalkan kata sandi untuk sistem multi-pengguna, supaya banyak pengguna bisa berbagi satu komputer tanpa saling membocorkan data. Kata sandi disimpan sebagai teks polos: kerentanan kritis, yang jadi terlihat pada hari seseorang mencetak file kata sandi. Insiden itu menandai kelahiran penyimpanan kata sandi yang aman dan memotivasi filosofi yang masih kita pegang: jangan pernah menyimpan kata sandi sebagai teks polos. Ia berujung pada hashing.

How hashing works

Algoritma hashing menerima string teks polos dengan panjang apa pun lalu menjalankan komputasi satu arah yang mengembalikan string berpanjang tetap. Panjangnya tidak pernah berubah entah Anda memberi 3 atau 100 karakter, dan input yang sama selalu menghasilkan output yang sama. Ia tidak bisa dibalik, Anda tidak bisa memulihkan aslinya dari hash, persis itulah kenapa yang disimpan adalah hash, bukan kata sandi.

1970-an: kriptografi asimetris

Ledakan riset kriptografi, didorong Whitfield Diffie dan Martin Hellman, menghasilkan pertukaran key Diffie-Hellman dan konsep kriptografi asimetris: membiarkan dua pihak membangun rahasia bersama lewat medium yang tidak tepercaya. Kripto asimetris menjadi tulang punggung protokol auth modern (juga disebut PKI: public key infrastructure). Era ini juga memberi Kerberos, yang memperkenalkan autentikasi berbasis tiket memakai pihak ketiga tepercaya untuk menerbitkan tiket yang memverifikasi pengguna dan layanan, pendahulu langsung sistem berbasis token zaman sekarang.

1990-an: MFA & biometrik

Saat internet tumbuh, sistem username/password sederhana tidak cukup kuat melawan brute-force dan serangan kamus. Ini melahirkan Multi-Factor Authentication (MFA), yang menggabungkan tiga kategori bukti:

FACTOR 1 Something you know passwords / PINs FACTOR 2 Something you have smart cards / OTP generators FACTOR 3 Something you are fingerprints / retina scans
FIG 2, MFA layers independent factors so one compromise is not enough.

Autentikasi biometrik muncul di sini, memakai pengenalan pola dan model statistik untuk mengidentifikasi pengguna lewat ciri fisik unik. Tapi biometrik membawa tantangannya sendiri: false positive/negative dan keamanan template: jadi ia tidak pernah menjadi solusi satu langkah untuk semua masalah.

Abad ke-21: dan masa depan

Cloud computing, perangkat mobile, dan arsitektur berbasis API menuntut kerangka yang lebih maju. Tak satu pun metode lama cukup sendirian, dan dari sanalah lahir komponen yang masih kita pakai: OAuth dan OAuth 2.0, JWT, arsitektur zero-trust, dan sistem passwordless seperti WebAuthn (mengandalkan public/private key yang disimpan di perangkat keras). Ke depan, kandidat masa depannya adalah identitas terdesentralisasi (dibangun di atas blockchain), biometrik perilaku, dan kriptografi pasca-kuantum.

Post-quantum cryptography

Begitu komputer kuantum umum, mereka cukup cepat untuk memecahkan algoritma (RSA dan skema public/private-key lain) yang hari ini membuat data kita terasa aman; algoritma itu memang tidak pernah dirancang untuk daya komputasi setinggi itu. Kriptografi pasca-kuantum adalah keluarga teknik yang dirancang tetap aman bahkan melawan mesin kuantum. Beberapa algoritma semacam itu sudah ada.

03 Tiga Komponen Inti

Sebelum bagian teknis, tiga komponen muncul berulang di setiap alur auth dan authz. Mengenalinya lebih dulu memberi Anda kosakata untuk semua yang menyusul: sesi, JWT, dan cookie.

  • Sesi: konteks sementara di sisi server yang memberi web stateless sedikit memori tentang pengguna di antara request.
  • JWT (JSON Web Token): token bertanda tangan yang mandiri, membawa klaim pengguna sehingga server tidak perlu state tersimpan untuk memverifikasinya.
  • Cookie: mekanisme penyimpanan sisi browser yang dikendalikan server, otomatis menempel di setiap request berikutnya.

Tiga bagian berikutnya membahas masing-masing secara bergiliran, secara mendalam.

Part II / The Building Blocks

04 Sesi

Saat web lahir, semua komunikasi klien-server berjalan di atas HTTP, dan sejak desain HTTP stateless: setiap request diperlakukan sebagai interaksi terisolasi. Server tidak mengingat request Anda sebelumnya; tiap request harus membawa semua informasi yang dibutuhkan server untuk menjalankan logika bisnisnya. HTTP tidak punya memori tentang pertukaran masa lalu.

Itu ideal untuk web awal, kebanyakan halaman statis, gambar statis, data yang bisa dibaca. Tidak ada yang butuh kontinuitas antar request. Tapi saat web beralih ke konten dinamis, statelessness menjadi hambatan. Situs e-commerce harus mengingat isi keranjang Anda; pengguna harus tetap login saat berpindah antar halaman. Kebutuhan inilah yang menandai dimulainya interaksi ber-state, dan sesi adalah jawabannya, cara membangun konteks sisi server yang sementara untuk tiap pengguna.

Cara kerja sesi

  1. Pembuatan sesi. Saat pengguna login, server membuat session ID unik dan menyimpannya bersama data pengguna yang relevan (role, nama, isi keranjang, apakah pengguna terautentikasi) di penyimpanan persisten: database, atau store di memori seperti Redis.
  2. Pengiriman. Session ID, cuma sebuah key, dikirim ke klien (browser) sebagai cookie. Setiap request berikutnya menyertakan cookie itu, jadi server bisa memakai session ID untuk mengambil semua data pengguna kembali dari store.
  3. Kedaluwarsa. Sesi berumur pendek. Jika kedaluwarsanya 15 menit, setelah itu sesi tidak valid; server membuat sesi baru dan menerbitkan session ID baru. Lingkaran ini berulang.
Client Server Store (Redis/DB) 1 / login (user + password) 2 / store {sessionID -> userData} 3 / Set-Cookie: sid=... (HttpOnly) 4 / request + Cookie: sid 5 / lookup sid -> userData 6 / authorized response
FIG 3, The session loop. The cookie carries only a key; the data lives server-side.

Evolusi penyimpanannya

Penyimpanan sesi matang dalam tiga tahap, masing-masing memecahkan masalah skalabilitas tahap sebelumnya:

  • Sesi berbasis file: pendekatan paling awal, menyimpan data dalam file di server. Sederhana, tapi menderita masalah skalabilitas saat jumlah pengguna bertambah.
  • Sesi berbasis database: memindahkan data sesi ke database untuk pencarian lebih cepat dan ketahanan di atas restart server (sesi selamat dari restart).
  • Store terdistribusi: akhirnya data sesi pindah ke store terdistribusi dalam memori seperti Redis atau Memcached (data di RAM, bukan disk), yang jauh lebih cepat daripada pencarian database.

Kita masih memakai sesi sampai hari ini, untuk tujuan yang sama persis: memberi server sedikit memori tentang pengguna.

Kode: hashing kata sandi & login sesi

Login sesi pertama memverifikasi kredensial terhadap hash tersimpan (tak pernah teks polos), lalu mencetak session ID acak dan menyimpan payload pengguna di Redis dengan TTL.

Go
package main

import (
    "context"
    "crypto/rand"
    "encoding/hex"
    "encoding/json"
    "net/http"
    "time"

    "github.com/redis/go-redis/v9"
    "golang.org/x/crypto/bcrypt"
)

var rdb = redis.NewClient(&redis.Options{Addr: "localhost:6379"})
var ctx = context.Background()

type User struct {
    ID   string `json:"id"`
    Role string `json:"role"`
}

// hash once at signup; store hash, never the password
func HashPassword(pw string) (string, error) {
    b, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
    return string(b), err
}

// bcrypt.CompareHashAndPassword is constant-time internally
func CheckPassword(hash, pw string) bool {
    return bcrypt.CompareHashAndPassword([]byte(hash), []byte(pw)) == nil
}

func newSessionID() string {
    b := make([]byte, 32)
    rand.Read(b) // cryptographically random
    return hex.EncodeToString(b)
}

func Login(w http.ResponseWriter, u User) error {
    sid := newSessionID()
    data, _ := json.Marshal(u)
    // store {sessionID -> userData} with a 15-minute TTL
    if err := rdb.Set(ctx, "sess:"+sid, data, 15*time.Minute).Err(); err != nil {
        return err
    }
    http.SetCookie(w, &http.Cookie{
        Name:     "sid",
        Value:    sid,
        HttpOnly: true,            // JS cannot read it
        Secure:   true,            // HTTPS only
        SameSite: http.SameSiteLaxMode,
        Path:     "/",
        Expires:  time.Now().Add(15 * time.Minute),
    })
    return nil
}
Python
import json, secrets
import bcrypt
import redis
from flask import Flask, request, make_response

app = Flask(__name__)
rdb = redis.Redis(host="localhost", port=6379)
TTL = 15 * 60  # 15 minutes

# hash once at signup; store hash, never the password
def hash_password(pw: str) -> bytes:
    return bcrypt.hashpw(pw.encode(), bcrypt.gensalt())

# bcrypt.checkpw is constant-time internally
def check_password(hash_: bytes, pw: str) -> bool:
    return bcrypt.checkpw(pw.encode(), hash_)

def new_session_id() -> str:
    return secrets.token_hex(32)  # cryptographically random

@app.post("/login")
def login():
    body = request.get_json()
    # ... look up user, check_password(stored_hash, body["password"]) ...
    user = {"id": "u_42", "role": "admin"}

    sid = new_session_id()
    # store {sessionID -> userData} with a 15-minute TTL
    rdb.setex(f"sess:{sid}", TTL, json.dumps(user))

    resp = make_response({"ok": True})
    resp.set_cookie(
        "sid", sid,
        httponly=True,   # JS cannot read it
        secure=True,     # HTTPS only
        samesite="Lax",
        max_age=TTL,
        path="/",
    )
    return resp

Selami: cara menyimpan kata sandi yang benar

"Simpan hash-nya, bukan kata sandinya" hanya setengah aturan. Hash yang mana, dan apa yang Anda campurkan ke dalamnya, menentukan apakah database yang bocor adalah insiden kecil atau bencana.

Kenapa hash serbaguna adalah alat yang salah

SHA-256 dan MD5 dibuat untuk cepat: persis yang tidak Anda inginkan untuk kata sandi. GPU atau ASIC modern menghitung miliaran hash SHA-256 per detik, jadi tabel hash cepat yang dicuri bisa di-brute-force hampir gratis. Penyimpanan kata sandi butuh fungsi hashing kata sandi (KDF lambat yang bisa disetel) yang sengaja dibuat mahal dan, idealnya, memory-hard supaya penyerang tidak bisa memparalelkan murah di GPU. Pilihan yang diterima adalah Argon2id (preferensi saat ini), scrypt, dan bcrypt.

Salt: mengalahkan prekomputasi

Salt adalah nilai acak unik yang dibuat per kata sandi dan disimpan sebagai teks polos tepat di samping hash. Ia melakukan dua hal: kata sandi identik tidak lagi menghasilkan hash identik (jadi penyerang tidak bisa melihat kata sandi yang dipakai ulang atau memecahkan banyak sekaligus), dan rainbow table hasil prekomputasi jadi sia-sia karena penyerang butuh satu tabel terpisah per salt. Salt bukan rahasia: satu-satunya tugasnya unik.

Pepper: rahasia yang tidak pernah dipegang database

Pepper adalah satu rahasia tingkat situs yang disimpan di luar database, di environment variable, secret manager, atau HSM. Anda HMAC kata sandi dengan pepper sebelum memberinya ke KDF. Hasilnya: jika hanya database yang bocor (salt + hash) tapi rahasia aplikasi tidak, hash-nya tetap tak terpecahkan, karena penyerang kekurangan pepper. Salt melindungi dari prekomputasi; pepper melindungi dari breach yang hanya menyentuh DB.

Work factor / biaya

Setiap KDF mengekspos parameter biaya yang Anda setel ke perangkat keras: biaya bcrypt (putaran logaritmik), N/r/p scrypt, dan time, memory, serta parallelism Argon2. Pilih nilai supaya satu hash memakan kira-kira 250 ms di server Anda, cukup lambat untuk membatasi penyerang, cukup cepat untuk login sungguhan, dan naikkan seiring waktu begitu perangkat keras membaik. Parameter terpilih disimpan di dalam string hash sehingga Anda bisa memverifikasi hash lama dan men-hash ulang secara transparan saat login berikutnya ketika biaya dinaikkan.

password HMAC(pw, pepper) secret, not in DB Argon2id(/, salt, cost) slow / memory-hard store salt+params+hash
FIG 4, Pepper (secret) -> KDF with per-password salt + cost -> store salt, params and hash together.
KDFMemory-hard?Catatan
Argon2idYaBawaan pilihan; menahan GPU/ASIC; setel memory + time + parallelism.
scryptYaPilihan memory-hard yang baik; setel N/r/p.
bcryptTidakMasih bisa diterima; membatasi input di 72 byte, pre-hash input panjang.
SHA-256 / MD5TidakJangan untuk kata sandi, terlalu cepat.

Kode: Argon2id dengan pepper

Go
import (
    "crypto/hmac"
    "crypto/rand"
    "crypto/sha256"
    "crypto/subtle"
    "encoding/base64"
    "fmt"
    "golang.org/x/crypto/argon2"
)

var pepper = []byte(os.Getenv("PASSWORD_PEPPER")) // secret, NOT in DB

// cost parameters, tune so one hash ~250ms
const (
    aTime    = 3         // iterations
    aMemory  = 64 * 1024 // 64 MB, memory-hard
    aThreads = 4
    aKeyLen  = 32
)

func peppered(pw string) []byte {
    m := hmac.New(sha256.New, pepper)
    m.Write([]byte(pw))
    return m.Sum(nil)
}

// returns an encoded string: salt + params + hash
func HashArgon(pw string) string {
    salt := make([]byte, 16)
    rand.Read(salt) // unique per password
    h := argon2.IDKey(peppered(pw), salt, aTime, aMemory, aThreads, aKeyLen)
    return fmt.Sprintf("argon2id$%d$%d$%d$%s$%s", aTime, aMemory, aThreads,
        base64.RawStdEncoding.EncodeToString(salt),
        base64.RawStdEncoding.EncodeToString(h))
}

// re-derive with the SAME salt+params, then constant-time compare
func VerifyArgon(encoded, pw string) bool {
    var t, mem, th uint32
    var b64salt, b64hash string
    fmt.Sscanf(encoded, "argon2id$%d$%d$%d$%s$%s", &t, &mem, &th, &b64salt, &b64hash)
    salt, _ := base64.RawStdEncoding.DecodeString(b64salt)
    want, _ := base64.RawStdEncoding.DecodeString(b64hash)
    got := argon2.IDKey(peppered(pw), salt, t, mem, uint8(th), uint32(len(want)))
    return subtle.ConstantTimeCompare(got, want) == 1
}
Python
import os, hmac, hashlib
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

PEPPER = os.environ["PASSWORD_PEPPER"].encode()  # secret, NOT in DB

# cost parameters, tune so one hash ~250ms
ph = PasswordHasher(
    time_cost=3,            # iterations
    memory_cost=64 * 1024,  # 64 MB, memory-hard
    parallelism=4,
    hash_len=32,
    salt_len=16,            # random salt baked into the output
)

def peppered(pw: str) -> bytes:
    return hmac.new(PEPPER, pw.encode(), hashlib.sha256).digest()

# argon2-cffi embeds salt + params inside the returned string
def hash_argon(pw: str) -> str:
    return ph.hash(peppered(pw))

def verify_argon(encoded: str, pw: str) -> bool:
    try:
        ph.verify(encoded, peppered(pw))   # constant-time internally
    except VerifyMismatchError:
        return False
    # transparently upgrade old hashes when cost is raised
    if ph.check_needs_rehash(encoded):
        rehash_and_store(hash_argon(pw))
    return True

05 JSON Web Token (JWT)

Menjelang pertengahan 2000-an, aplikasi web telah tumbuh menjadi sistem terdistribusi global, dan sistem ber-state (sesi), meski efektif, menimbulkan hambatan. Tiga masalah mendorong industri ke pendekatan baru:

  • Memori. Menjaga data sesi untuk jutaan pengguna menjadi biaya yang mahal.
  • Replikasi. Dalam arsitektur terdistribusi, menyinkronkan data sesi lintas server dan region (satu server di satu belahan dunia, yang lain di sisi berlawanan) memperkenalkan latensi dalam alur auth.
  • Konsistensi. Menjaga state yang tersinkron itu tetap benar di mana-mana itu sulit.

Developer ingin cara menggeser state keluar dari server sambil menjaga keamanan dan integritas. Jawabannya, diformalkan pada 2015, adalah JWT: mekanisme stateless untuk memindahkan klaim antara dua pihak. Inovasi kuncinya: token yang mandiri. Sebuah JWT membawa data pengguna (user ID, role) dan tanda tangan kriptografis di dalam token itu sendiri, semuanya di-encode Base64.

Anatomi: tiga bagian

JWT adalah satu string ber-encode Base64URL dengan tiga bagian yang dipisah titik: header.payload.signature.

HEADER Metadata about the token itself { "alg": "HS256", "typ": "JWT" } signing algorithm PAYLOAD The claims / data { "sub": "u_42", "iat": 1716998400, "name": "Asha", "role": "admin" } sub = user id / iat = issued-at SIGNATURE Proves integrity & authenticity HMACSHA256( b64(header)+"."+ b64(payload), SECRET) tamper -> verify fails
FIG 5, header / payload / signature. Change any byte of header or payload and the signature no longer matches.

Header memegang metadata seperti algoritma penandatanganan. Payload memegang klaim: sub adalah konvensi untuk user ID (dari DB atau auth provider Anda, konteks apa pun), iat berarti "issued at", plus field opsional seperti name dan role (admin / member / editor / viewer). Signature dihitung dengan secret key yang hanya Anda pegang. Jika ada yang mengubah JWT, verifikasi dengan secret Anda gagal, Anda langsung tahu token itu tidak lagi valid. Begitulah cara memverifikasi data pengguna secara stateless: tanpa pencarian sesi, cukup cek tanda tangan di tiap request. Ringan dan menghemat biaya penyimpanan.

Kelebihannya

  • Stateless: tanpa biaya penyimpanan sisi server.
  • Skalabilitas: dalam arsitektur microservice dan terdistribusi, cookie yang diterbitkan satu server tidak bisa dipakai domain lain, tapi JWT bisa dikirim ke banyak server; masing-masing memegang secret bersama, men-decode token, dan memverifikasi identitas. Server berskala ke banyak instance dan semuanya mengautentikasi serentak memakai secret bersama.
  • Portabilitas: ringan, ramah URL, dan ber-encode Base64, JWT mudah bepergian: di header, di cookie, bahkan di nilai URL (dan di local storage, meski umumnya sebaiknya jangan).

Tantangannya

Token theft

Karena JWT stateless, server tidak punya catatan tersimpan untuk divalidasi. Jika seseorang mendapatkan JWT Anda, mereka bisa menyamar sebagai Anda dan bertindak atas nama Anda sampai token kedaluwarsa, dan tidak ada cara bawaan untuk mencabutnya lebih awal kecuali server mengganti secret-nya. Tapi memutar secret mencabut token semua pengguna, memaksa semua orang login ulang hanya untuk mematikan satu token.

Revocation

Untuk alasan yang sama, Anda tidak bisa mencabut akses satu pengguna sesuai permintaan. Karena JWT stateless, tidak ada cara melacak status token sampai ia kedaluwarsa.

Pendekatan hibrida (dan paradoksnya)

Untuk mengalahkan kedua masalah itu, tim menggabungkan statelessness dengan sedikit statefulness. Pengguna login, mendapat JWT, dan mengirimnya di tiap request (biasanya di header Authorization). Server memverifikasi tanda tangan dengan secret-nya, tanpa pencarian penyimpanan. Lalu, setelah verifikasi lewat, server memeriksa blacklist JWT yang dicabut yang disimpan di store persisten (Redis/DB). Ini memungkinkan Anda memblokir atau mencabut pengguna untuk sementara (mis. akun yang diretas) tanpa memutar secret global.

The honest question

Seluruh alasan JWT adalah statelessness. Jika Anda toh kini melakukan pencarian store persisten untuk memeriksa validitas, kenapa tidak langsung memakai pendekatan ber-state sejak awal, yang umumnya dianggap lebih aman dan memberi kendali ini gratis? Ini perdebatan industri yang sering terjadi.

Practical advice

Untuk sistem menengah-ke-besar, jangan bersusah payah soal hashing, salting, algoritma, dan strategi pencabutan sendiri, pakai auth provider (Auth0, Clerk, dan semacamnya). Tugas merekalah memikirkan komponen mana yang paling aman. Latihan yang bagus untuk mengimplementasikan auth sendiri saat belajar backend, demi memahami bagian-bagian dan komprominya, tapi di produksi, ambil provider kecuali Anda sangat yakin dengan alur auth Anda.

Kode: menandatangani & memverifikasi JWT

Go
package auth

import (
    "time"
    "github.com/golang-jwt/jwt/v5"
)

var secret = []byte("keep-this-very-secret")

// mint a self-contained token carrying the claims
func Sign(userID, role string) (string, error) {
    claims := jwt.MapClaims{
        "sub":  userID,                              // user id
        "role": role,                                // for authorization
        "iat":  time.Now().Unix(),                   // issued at
        "exp":  time.Now().Add(time.Hour).Unix(),    // expiry
    }
    tok := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return tok.SignedString(secret)
}

// verify signature + expiry; returns the claims if valid
func Verify(tokenStr string) (jwt.MapClaims, error) {
    tok, err := jwt.Parse(tokenStr, func(t *jwt.Token) (any, error) {
        // pin the algorithm to stop "alg: none" attacks
        if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, jwt.ErrSignatureInvalid
        }
        return secret, nil
    })
    if err != nil || !tok.Valid {
        return nil, err
    }
    return tok.Claims.(jwt.MapClaims), nil
}
Python
import jwt   # PyJWT
from datetime import datetime, timedelta, timezone

SECRET = "keep-this-very-secret"

# mint a self-contained token carrying the claims
def sign(user_id: str, role: str) -> str:
    now = datetime.now(timezone.utc)
    payload = {
        "sub":  user_id,                 # user id
        "role": role,                    # for authorization
        "iat":  now,                     # issued at
        "exp":  now + timedelta(hours=1) # expiry
    }
    return jwt.encode(payload, SECRET, algorithm="HS256")

# verify signature + expiry; returns the claims if valid
def verify(token: str) -> dict:
    # algorithms is a whitelist -> stops "alg: none" attacks
    return jwt.decode(token, SECRET, algorithms=["HS256"])

# hybrid: after verify(), also check a revocation blacklist
def is_revoked(rdb, jti_or_token: str) -> bool:
    return rdb.sismember("jwt:blacklist", jti_or_token)

Selami: penandatanganan, serangan, dan refresh token

HS256 vs RS256: penandatanganan simetris vs asimetris

Alg di header menentukan siapa yang bisa membuat token valid. Dengan HS256 (HMAC) ada satu secret bersama yang dipakai untuk menandatangani dan memverifikasi, jadi setiap layanan yang bisa memverifikasi juga bisa memalsukan. Itu baik di dalam satu batas kepercayaan, berbahaya lintas banyak. Dengan RS256 / ES256 (asimetris), server auth menandatangani dengan private key dan yang lain memverifikasi dengan public key. Verifier tidak bisa mencetak token, dan Anda bisa memublikasikan public key (endpoint JWKS) sehingga microservice yang tak berhubungan dan pihak ketiga memvalidasi secara independen. Inilah alasan sebenarnya JWT berskala lintas sistem terdistribusi: verifikasi tidak butuh berbagi kekuatan penandatanganan.

HS256 (simetris)RS256 / ES256 (asimetris)
KeySatu secret bersamaPrivate (tanda tangan) + public (verifikasi)
Siapa yang bisa memalsukanSiapa pun yang bisa memverifikasiHanya pemegang private key
Terbaik untukSatu layanan / monolitMicroservice, verifier pihak ketiga, OIDC

Serangan alg:none dan key-confusion

Dua kerentanan JWT klasik sama-sama berasal dari mempercayai header token itu sendiri. Pada serangan alg:none, penyerang meng-set "alg":"none" dan membuang signature; verifier naif yang "menuruti kata header" menerima token tanpa tanda tangan. Pada serangan key-confusion RS256->HS256, penyerang mengganti algoritma menjadi HS256 dan menandatangani dengan public key (yang jelas-jelas publik) seolah itu secret HMAC; verifier yang memilih algoritma dari header akan memvalidasinya. Pertahanan: jangan pernah membiarkan token yang memilih, sematkan algoritma yang diharapkan di sisi verifier, seperti yang sudah dilakukan cek SigningMethodHMAC dan whitelist algorithms=[...] pada kode bagian 05.

Access token + refresh token

Pola ini menyelesaikan kelemahan pencabutan tanpa membuang statelessness. Terbitkan access token berumur pendek (±5-15 menit) yang dipakai di tiap panggilan API, dan refresh token berumur panjang (hari-minggu) yang disimpan aman (cookie HttpOnly, atau penyimpanan aman di mobile). Saat access token kedaluwarsa, klien menukar refresh token dengan pasangan baru. Karena access token berumur pendek, token yang dicuri tak berguna dalam hitungan menit; karena hanya refresh token yang menyentuh store, jalur cepat per-request tetap stateless.

Rotation + reuse detection

Di tiap refresh, putar: terbitkan refresh token baru dan cabut yang lama. Jika refresh token bekas (yang sudah diputar) muncul lagi, itu sinyal kuat bahwa ia dicuri, cabut seluruh keluarga token dan paksa login ulang. Ini memberi pencabutan praktis sambil menjaga jalur access token bebas pencarian.

Access token ~10 min / sent every call Refresh token days / HttpOnly / stored mints new access token on expiry rotate: old refresh invalidated; reuse -> revoke family
FIG 6, Short access token for speed, rotating refresh token for control.

Kode: verifikasi RS256 + rotasi refresh

Go
// Verify with a PUBLIC key, pinning RS256 (blocks alg:none & key confusion)
func VerifyRS256(tokenStr string, pub *rsa.PublicKey) (jwt.MapClaims, error) {
    tok, err := jwt.Parse(tokenStr, func(t *jwt.Token) (any, error) {
        if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok { // pin family
            return nil, jwt.ErrSignatureInvalid
        }
        return pub, nil
    }, jwt.WithValidMethods([]string{"RS256"}))
    if err != nil || !tok.Valid {
        return nil, err
    }
    return tok.Claims.(jwt.MapClaims), nil
}

// Rotate a refresh token; detect reuse of an already-spent one.
func Refresh(rdb *redis.Client, presented string) (string, error) {
    family, err := rdb.Get(ctx, "rt:"+presented).Result()
    if err != nil {
        // not a live token, was it a previously-spent one? -> theft
        if fam, e := rdb.Get(ctx, "spent:"+presented).Result(); e == nil {
            rdb.Del(ctx, "family:"+fam)            // revoke the whole family
            return "", errors.New("refresh reuse detected")
        }
        return "", errors.New("invalid refresh token")
    }
    rdb.Del(ctx, "rt:"+presented)                  // consume the live token
    rdb.Set(ctx, "spent:"+presented, family, 14*24*time.Hour) // remember it
    newRefresh := newSessionID()
    rdb.Set(ctx, "rt:"+newRefresh, family, 14*24*time.Hour)
    return newRefresh, nil
}
Python
# Verify with a PUBLIC key, pinning RS256 (blocks alg:none & key confusion)
def verify_rs256(token: str, public_key) -> dict:
    return jwt.decode(token, public_key, algorithms=["RS256"])  # whitelist

# Rotate a refresh token; detect reuse of an already-spent one.
def refresh(rdb, presented: str) -> str:
    family = rdb.get(f"rt:{presented}")
    if family is None:
        # not a live token, was it previously spent? -> theft
        fam = rdb.get(f"spent:{presented}")
        if fam is not None:
            rdb.delete(f"family:{fam.decode()}")    # revoke the whole family
            raise PermissionError("refresh reuse detected")
        raise PermissionError("invalid refresh token")
    rdb.delete(f"rt:{presented}")                   # consume the live token
    rdb.setex(f"spent:{presented}", 14 * 24 * 3600, family)  # remember it
    new_refresh = secrets.token_hex(32)
    rdb.setex(f"rt:{new_refresh}", 14 * 24 * 3600, family)
    return new_refresh

06 Cookie

Cookie adalah cara menyimpan sepotong informasi, nilai string apa pun, di browser pengguna, dari sisi server. Bagian terakhir itu yang penting: dengan cookie, server bisa menaruh data ke browser klien lewat alur normal request/response HTTP, tunduk pada dua jaminan yang ditegakkan browser:

  • Cookie hanya bisa diakses server (domain) yang men-set-nya; satu server tidak bisa melihat cookie server lain. Pembatasan ini fitur keamanan bawaan browser.
  • Cookie yang di-set server otomatis menempel di setiap request berikutnya ke server yang sama.

Round-trip otomatis itulah yang membuat cookie begitu berguna untuk auth. Server menyimpan token otorisasi (JWT atau session ID, tergantung implementasi) di cookie; browser (Chrome, Firefox, Safari) mengirimnya kembali di tiap request; server memvalidasinya, mengidentifikasi dan mengotorisasi pengguna. Seluruh tarian "server menyerahkan token ke klien, klien mengembalikannya tiap kali" diotomasi mekanisme cookie, digerakkan sepenuhnya dari sisi server, karena server bisa menulis ke cookie jar klien.

HttpOnly

Menandai cookie HttpOnly berarti JavaScript tidak bisa membaca nilainya. Untuk session ID ini penting: ia memblokir satu kelas serangan pencurian token yang bekerja dengan membaca cookie dari skrip halaman. Pasangkan dengan Secure (khusus HTTPS) dan kebijakan SameSite yang masuk akal.

Selami: CSRF dan session fixation

Fitur terbaik cookie, penempelan otomatis ke tiap request, juga akar dari dua serangan khasnya. Keduanya relevan khusus untuk auth berbasis cookie.

Cross-Site Request Forgery (CSRF)

Karena browser mengirim session cookie Anda pada request apa pun ke sebuah domain, termasuk request yang dipicu situs lain, halaman penyerang bisa diam-diam menembakkan request pengubah state ke bank atau aplikasi Anda, dan browser dengan patuhnya menempelkan cookie Anda. Server melihat request terautentikasi penuh yang tak pernah ia maksud untuk dihormati. Perhatikan asimetrinya: auth token-di-header (Bearer JWT) kebal secara alami, karena halaman penyerang tidak bisa membaca atau men-set header Authorization Anda; hanya kredensial yang dibawa cookie yang terkirim otomatis. Pertahanan:

  • Cookie SameSite: Lax atau Strict menghentikan cookie ikut pada request lintas-situs. Garis pertahanan pertama.
  • Synchronizer token: server menyematkan token acak di halaman/form; request pengubah state wajib menggema-nya kembali. Penyerang tidak bisa membacanya (same-origin policy), jadi mereka tidak bisa memalsukan request valid.
  • Double-submit cookie: kirim token CSRF sebagai cookie yang bisa dibaca dan sebagai header/field; server memeriksa keduanya cocok. Stateless dan sederhana.
evil.com attacker page your browser holds bank cookie bank.com trusts the cookie hidden POST + cookie (auto!) SameSite / CSRF token blocks the forged request
FIG 7, The cookie rides along on a cross-site request; SameSite + a CSRF token break the chain.

Session fixation

Di sini penyerang menanam session ID yang sudah ia ketahui, mis. dengan memberi korban tautan berisi sid tetap, atau men-set satu, lalu menunggu korban login dengan ID yang sama itu. Jika server mempertahankan session ID pra-login setelah autentikasi, penyerang kini berbagi sesi terautentikasi. Perbaikannya sederhana dan mutlak: regenerasi session ID di setiap perubahan hak istimewa, terutama tepat setelah login, supaya nilai yang diketahui penyerang jadi tak berguna.

Kode: double-submit CSRF + regenerasi sesi

Go
// Double-submit: compare the CSRF cookie against the header.
func CheckCSRF(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        if r.Method == "GET" || r.Method == "HEAD" {
            next(w, r); return
        }
        c, err := r.Cookie("csrf")
        hdr := r.Header.Get("X-CSRF-Token")
        if err != nil || subtle.ConstantTimeCompare(
            []byte(c.Value), []byte(hdr)) != 1 {
            http.Error(w, "forbidden", http.StatusForbidden)
            return
        }
        next(w, r)
    }
}

// Regenerate the session ID right after a successful login.
func RegenerateSession(oldSID string, u User) (string, error) {
    raw, _ := rdb.Get(ctx, "sess:"+oldSID).Result()
    rdb.Del(ctx, "sess:"+oldSID)            // kill the pre-login ID
    newSID := newSessionID()                // brand-new value
    rdb.Set(ctx, "sess:"+newSID, raw, 15*time.Minute)
    return newSID, nil
}
Python
import hmac

# Double-submit: compare the CSRF cookie against the header.
def check_csrf(view):
    @wraps(view)
    def wrapper(*args, **kwargs):
        if request.method in ("GET", "HEAD"):
            return view(*args, **kwargs)
        cookie = request.cookies.get("csrf", "")
        header = request.headers.get("X-CSRF-Token", "")
        if not cookie or not hmac.compare_digest(cookie, header):
            return jsonify(error="forbidden"), 403
        return view(*args, **kwargs)
    return wrapper

# Regenerate the session ID right after a successful login.
def regenerate_session(old_sid: str) -> str:
    raw = rdb.get(f"sess:{old_sid}")
    rdb.delete(f"sess:{old_sid}")            # kill the pre-login ID
    new_sid = secrets.token_hex(32)          # brand-new value
    rdb.setex(f"sess:{new_sid}", 15 * 60, raw)
    return new_sid
Part III / Types of Authentication

07 Autentikasi Ber-State

Tipe utama pertama. Klien (browser) mengirim username/email dan kata sandi ke server. Server memeriksa validitas dan kebenarannya; jika pengguna memenuhi syarat, ia membuat session ID, menggabungkannya dengan data pengguna, dan menyimpan bundelnya di Redis (DB juga bisa, tapi kebanyakan platform memilih Redis karena aksesnya cepat). Session ID dikirim balik ke klien di dalam cookie HttpOnly: jadi JavaScript tidak bisa membaca session ID. Berkat perilaku auto-attach cookie, setiap request berikutnya membawa cookie itu. Server membaca session ID, memeriksa keberadaan, kedaluwarsa, dan data penggunanya di Redis, mengidentifikasi dan mengotorisasi pengguna, lalu meloloskan request. Session ID bisa berupa string acak kriptografis apa pun, bahkan JWT, tergantung implementasi.

Client browser Server business logic Redis sid -> userData user+pass store bundle Set-Cookie sid (HttpOnly) lookup on each req
FIG 8, Stateful: the proof lives in the store; the cookie only points to it.

Kode: middleware yang memvalidasi sesi

Go
// RequireSession reads the sid cookie and looks it up in Redis.
func RequireSession(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        c, err := r.Cookie("sid")
        if err != nil {
            http.Error(w, "authentication failed", http.StatusUnauthorized)
            return
        }
        raw, err := rdb.Get(ctx, "sess:"+c.Value).Result()
        if err != nil { // missing or expired
            http.Error(w, "authentication failed", http.StatusUnauthorized)
            return
        }
        var u User
        json.Unmarshal([]byte(raw), &u)
        // attach the user to the request context for later handlers
        ctxWithUser := context.WithValue(r.Context(), "user", u)
        next(w, r.WithContext(ctxWithUser))
    }
}
Python
from functools import wraps
from flask import request, g, jsonify

def require_session(view):
    @wraps(view)
    def wrapper(*args, **kwargs):
        sid = request.cookies.get("sid")
        if not sid:
            return jsonify(error="authentication failed"), 401
        raw = rdb.get(f"sess:{sid}")     # None if missing/expired
        if raw is None:
            return jsonify(error="authentication failed"), 401
        g.user = json.loads(raw)          # attach for later handlers
        return view(*args, **kwargs)
    return wrapper

@app.get("/notes")
@require_session
def notes():
    return jsonify(owner=g.user["id"])

08 Autentikasi Stateless

Klien mengirim email/username dan kata sandi pada request login. Server memeriksa keasliannya; jika benar, ia membuat JWT bertanda tangan memakai secret key miliknya sendiri (disimpan aman sehingga bisa menandatangani dan memverifikasi). Token menyematkan info pengguna, user ID, role, dll., dan dikembalikan ke klien. Pada semua request berikutnya, klien mengidentifikasi diri dengan mengirim JWT, sesuai konvensi di header Authorization. Server mengekstrak token, memverifikasinya dengan secret, memulihkan user ID, dan setelah sukses menyelesaikan request; kalau tidak, ia mengembalikan 401 Unauthorized atau 403 Forbidden.

Ia disebut stateless justru karena, tidak seperti auth ber-state, tidak ada pencarian store persisten. Semua yang dibutuhkan server tentang pengguna hidup di dalam token bertanda tangan yang mandiri; secret key sendirilah yang membuktikan keaslian token.

Client browser / mobile Server holds SECRET key 1 / login -> server signs JWT with secret 2 / JWT returned 3 / every request: Authorization: Bearer <JWT>, verified with secret, no DB
FIG 9, Stateless: the proof travels inside the token; the server stores nothing.

Kode: middleware Bearer-token

Go
import "strings"

func RequireJWT(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        h := r.Header.Get("Authorization")
        if !strings.HasPrefix(h, "Bearer ") {
            http.Error(w, "authentication failed", http.StatusUnauthorized)
            return
        }
        claims, err := Verify(strings.TrimPrefix(h, "Bearer "))
        if err != nil {
            http.Error(w, "authentication failed", http.StatusUnauthorized)
            return
        }
        // no store lookup, identity comes from the verified token
        u := User{ID: claims["sub"].(string), Role: claims["role"].(string)}
        next(w, r.WithContext(context.WithValue(r.Context(), "user", u)))
    }
}
Python
def require_jwt(view):
    @wraps(view)
    def wrapper(*args, **kwargs):
        h = request.headers.get("Authorization", "")
        if not h.startswith("Bearer "):
            return jsonify(error="authentication failed"), 401
        try:
            claims = verify(h.removeprefix("Bearer "))
        except jwt.PyJWTError:
            return jsonify(error="authentication failed"), 401
        # no store lookup, identity comes from the verified token
        g.user = {"id": claims["sub"], "role": claims["role"]}
        return view(*args, **kwargs)
    return wrapper

09 Ber-State vs Stateless: Memilih

Kekurangan yang satu sebagian besar kelebihan yang lain. Pilih berdasarkan dari mana request datang.

DimensiBer-State (sesi)Stateless (JWT)
Kendali atas sesiTerpusat, pandangan real-time sesi aktifTanpa pandangan global
Cabut / logout penggunaMudah, tinggal buang sesinyaRumit, valid sampai kedaluwarsa
Ketergantungan penyimpananButuh session storeTidak ada
Skalabilitas lintas regionLatensi sinkronisasi, kompleksitas operasi lebih tinggiIdeal untuk sistem terdistribusi
Ramah mobile / APICookie kurang cocokSangat baik
Postur keamananUmumnya dianggap lebih amanPencurian token = penyamaran

Dalam praktik, kebanyakan aplikasi sebaiknya condong ber-state: sifat amannya, kemudahan mencabut token dan logout pengguna, serta visibilitas real-time ke sesi aktif sangat berharga. Harganya adalah batas skalabilitas dan kompleksitas operasional lebih tinggi di sistem terdistribusi besar multi-region. Stateless menang saat Anda butuh skalabilitas dan tidak punya ketergantungan session store: API, arsitektur terdistribusi, klien mobile. Satu kelemahan utamanya adalah pencabutan yang sulit.

The hybrid split

Anda bisa membagi per klien. Pakai auth ber-state untuk aplikasi web browser utama (tempat Anda ingin kendali dan pencabutan), dan JWT stateless untuk aplikasi mobile dan integrasi server-ke-server pihak ketiga (tempat Anda ingin skalabilitas dan kesederhanaan). Ini mengalahkan kekurangan keduanya.

10 Autentikasi API Key

API key melayani kumpulan kasus pemakaian yang sama sekali berbeda dari auth ber-state/stateless. Alurnya sederhana: di UI sebuah platform Anda klik "generate an API key", menerima string acak kriptografis yang aman secara kriptografis, dan sejak itu memakai string itu untuk akses programatik ke server di balik UI, tanpa pernah menyentuh UI.

Contoh kanonisnya OpenAI. Kebanyakan orang memakai ChatGPT lewat UI-nya: ketik di kotak, dapat respons yang ditampilkan rapi. Tapi di balik UI itu ada banyak server. OpenAI juga menawarkan API key untuk orang yang tidak mau UI, yang ingin kemampuan model di dalam produk atau server mereka sendiri, secara programatik. Untuk memberi akses ke server Anda (bukan UI, bukan platform Anda) kepada kumpulan pengguna berbeda dengan cara yang terbatas, berbasis izin, berbasis kedaluwarsa, Anda menyerahkan API key. Mereka menempelkannya (di header, atau apa pun yang diharapkan server), mengirimkannya kembali, dan bisa melakukan operasi yang diizinkan platform aslinya, tapi lewat kode.

Your Server no UI / no human holds API_KEY in env Provider Server e.g. OpenAI maps key -> plan/quota request + Authorization: Bearer API_KEY response (identified, authorized, metered)
FIG 10, Machine-to-machine: no login form, no human trigger, just a key on every call.

Kenapa tim membangun API key:

  • Mudah dibuat: klik tombol di UI, dapat key, pakai.
  • Ideal untuk komunikasi machine-to-machine (M2M). Alur biasa kita klien-ke-server: manusia memakai UI (mouse, keyboard), UI mengirim request programatik, server menjalankan logika bisnis dan merespons, UI menampilkannya untuk manusia. M2M menghapus manusia: server saya ingin memakai kemampuan ChatGPT, mis. pengguna minta aplikasi saya "rangkum paragraf ini", dan server saya memanggil provider di belakang layar. Tidak ada UI dan tidak ada interaksi manusia; semuanya dilakukan dalam kode.

Auth ber-state/stateless mengasumsikan pemicu manusia: form login untuk diisi, token untuk disimpan. M2M tidak butuh semua itu, Anda memberi mesin sebuah secret key, ia menyimpannya di environment variable atau store aman, dan mengirimnya di tiap request untuk mengidentifikasi diri. Itulah persis guna API key: interaksi programatik machine-to-machine.

Kode: memverifikasi API key

Go
import (
    "crypto/sha256"
    "crypto/subtle"
    "encoding/hex"
)

// Store only the HASH of issued keys (like passwords).
// On each call, hash the presented key and constant-time compare.
func hashKey(k string) string {
    sum := sha256.Sum256([]byte(k))
    return hex.EncodeToString(sum[:])
}

func RequireAPIKey(lookup func(hash string) (*Client, bool)) func(http.HandlerFunc) http.HandlerFunc {
    return func(next http.HandlerFunc) http.HandlerFunc {
        return func(w http.ResponseWriter, r *http.Request) {
            key := r.Header.Get("X-API-Key")
            client, ok := lookup(hashKey(key))
            // subtle.ConstantTimeCompare avoids timing leaks
            if !ok || subtle.ConstantTimeCompare([]byte(key), []byte(key)) != 1 {
                http.Error(w, "authentication failed", http.StatusUnauthorized)
                return
            }
            _ = client // check scopes, quota, expiry here
            next(w, r)
        }
    }
}
Python
import hashlib, hmac

# Store only the HASH of issued keys (like passwords).
def hash_key(k: str) -> str:
    return hashlib.sha256(k.encode()).hexdigest()

def require_api_key(view):
    @wraps(view)
    def wrapper(*args, **kwargs):
        key = request.headers.get("X-API-Key", "")
        client = lookup_client(hash_key(key))   # your DB lookup
        if client is None:
            return jsonify(error="authentication failed"), 401
        # check scopes, quota, expiry on `client` here
        # hmac.compare_digest is constant-time
        if not hmac.compare_digest(hash_key(key), client["key_hash"]):
            return jsonify(error="authentication failed"), 401
        g.client = client
        return view(*args, **kwargs)
    return wrapper
Part IV / OAuth & OpenID Connect

11 Masalah Delegasi

Sampai di sini pengaturannya sederhana: buat akun dengan username/password per platform, autentikasi, terima token. Tapi semakin banyak platform yang Anda pakai, semakin banyak kredensial yang Anda buat dan harus ingat. Dua masalah muncul:

  • Risiko keamanan. Memakai ulang kata sandi sangat umum di era awal (bayangkan 12345 atau password), jadi satu breach mengompromikan banyak akun.
  • Kelelahan. Mengelola jumlah akun yang sangat banyak membuat pengguna kewalahan.

Lalu muncul kelas masalah baru: satu situs web butuh akses ke resource situs web lain. Aplikasi travel/pesan tiket perlu memindai Gmail Anda untuk tiket pesawat. Aplikasi sosial ingin mengimpor kontak Anda dari Google atau jaringan lain. Satu platform menginginkan resource platform lain, secara programatik. Inilah masalah delegasi.

The disastrous first solution

Pengguna tinggal berbagi kata sandi. Ini bencana: kata sandi memberi akses penuh ke semua yang ada di akun, tanpa cara membatasi izin, dan tidak mungkin mencabut akses tanpa mengganti kata sandi di mana-mana (karena kata sandi yang sama dipakai ulang lintas situs, dan Anda baru saja memberikannya ke pihak ketiga yang kini bisa melakukan apa saja).

Pada 2007, insinyur dari perusahaan seperti Google dan Twitter berupaya menstandarkan akses yang didelegasikan tanpa berbagi kata sandi. Hasilnya OAuth: Open Authorization, bukan sekadar protokol tapi gagasan revolusioner. Tekniknya: berbagi token, bukan kata sandi. Kata sandi memberi akses total; token memberi akses spesifik dan ter-scope ke bagian tertentu dari akun Anda.

Tokens vs passwords

Jika Anda berbagi kata sandi Google dengan saya, saya bisa melihat Photos, riwayat Maps, Kontak Anda, menambah/menghapus event kalender, semua. Tapi jika Anda berbagi token yang ter-scope hanya untuk "membaca kontak", saya bisa membaca kontak Anda dan tidak lebih. Scoping itulah intinya.

12 OAuth 1.0: Empat Peran

OAuth memperkenalkan empat komponen. Ambil contoh: Facebook (aplikasinya) ingin kontak Google Anda.

PeranSiapaDalam contoh
Resource ownerPengguna pemilik dataAnda
ClientAplikasi yang meminta aksesFacebook
Resource serverServer yang memegang dataGoogle (akun Anda)
Authorization serverServer yang menerbitkan token setelah mengautentikasi penggunaServer auth Google

Alur OAuth 1.0

  1. Client (Facebook) mengarahkan pengguna ke authorization server.
  2. Pengguna mengautentikasi dan memberi izin: "ya, saya izinkan scope ini".
  3. Authorization server mengirim token ke client.
  4. Client memakai token untuk mengakses resource (kontak Anda) di resource server.

Begitulah client mendapat akses ter-scope ke resource platform lain, secara programatik, tanpa Anda pernah berbagi kata sandi, menghapus dengan indah masalah berbagi kata sandi.

Client Facebook Authorization server (Google) Resource server (Google) You (owner) 1 redirect 2 authenticate + grant 3 token -> client 4 use token -> fetch scoped resource
FIG 11, OAuth 1.0: redirect -> grant -> token -> scoped resource access.

13 OAuth 2.0

OAuth 1.0 revolusioner tapi punya keterbatasan: rumit bagi developer untuk diimplementasikan, dan mengandalkan tanda tangan kriptografis yang rawan error. OAuth 2.0 (sekitar 2010) mengatasi keduanya:

  • Bearer token. Jauh lebih sederhana daripada menandatangani tiap request. Secara teori lebih rentan, tapi jauh lebih mudah diimplementasikan; "bearer" berarti siapa pun yang memegang token bisa memakainya.
  • Alur per jenis aplikasi. Developer memilih alur berdasarkan jenis aplikasinya.
AlurUntukCatatan
Authorization CodeAplikasi sisi serverAlur standar, paling aman
ImplicitAplikasi berbasis browserKini tidak disarankan, risikonya keamanan
Client CredentialsMachine-to-machineServer<->server, tanpa pengguna/browser
Device CodePerangkat input terbatasMis. Smart TV (tanpa keyboard/mouse)

Selami: alur Authorization Code dengan PKCE

Alur Authorization Code adalah yang wajib benar-benar dipahami, karena itulah yang dipakai "Sign in with ...". Ia berjalan dalam dua kaki di atas HTTP sungguhan.

Kaki 1: request authorize (front channel, di browser)

Client mengirim pengguna ke authorization server dengan parameter query:

HTTP / redirect
GET /authorize?
    response_type=code
    &client_id=note_app
    &redirect_uri=https://notes.app/callback
    &scope=openid%20email%20keep.read
    &state=xyz123                       # anti-CSRF / anti-mixup nonce
    &code_challenge=E9Melh...           # = BASE64URL(SHA256(verifier))
    &code_challenge_method=S256

Setelah pengguna login dan menyetujui, server mengarahkan kembali: https://notes.app/callback?code=AUTH_CODE&state=xyz123. Client wajib memverifikasi state cocok dengan yang ia kirim, itulah yang menghentikan CSRF/kekacauan pada redirect.

Kaki 2: pertukaran token (back channel, server-ke-server)

HTTP / POST /token
POST /token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=AUTH_CODE
&redirect_uri=https://notes.app/callback
&client_id=note_app
&code_verifier=dBjftJeZ4CVP...         # the original random secret

-> 200 OK
{ "access_token": "...",               # call the resource server
  "refresh_token": "...",
  "id_token": "eyJ...",                # the OIDC identity JWT (sec 14)
  "token_type": "Bearer",
  "expires_in": 600 }

Kenapa PKCE?

PKCE (Proof Key for Code Exchange) melindungi public client: single-page app dan aplikasi mobile yang tidak bisa menyimpan rahasia. Client membuat codeverifier acak, hanya mengirim hash-nya (codechallenge) di kaki 1, dan mengungkap verifier mentahnya di kaki 2. Jika penyerang mencegat authorization code (mis. aplikasi jahat menangkap redirect mobile), code itu tak berguna tanpa verifier, yang tidak pernah meninggalkan client yang sah. PKCE-lah alasan alur Implicit kini tidak disarankan: Authorization Code + PKCE memberi aplikasi browser/mobile keamanan yang sama dengan client sisi server yang konfidensial.

Public client SPA / mobile Auth server 1 /authorize + code_challenge = SHA256(verifier) 2 redirect ?code (useless alone) 3 /token: code + code_verifier -> tokens
FIG 12, PKCE: the intercepted code can't be redeemed without the verifier, which never left the client.

Kode: nilai PKCE + pertukaran token

Go
// Build the PKCE pair before redirecting the user.
func newPKCE() (verifier, challenge string) {
    b := make([]byte, 32)
    rand.Read(b)
    verifier = base64.RawURLEncoding.EncodeToString(b)
    sum := sha256.Sum256([]byte(verifier))
    challenge = base64.RawURLEncoding.EncodeToString(sum[:])
    return
}

// Leg 2: exchange the code (send code_verifier, not the secret).
func exchange(code, verifier string) (*TokenResp, error) {
    form := url.Values{
        "grant_type":    {"authorization_code"},
        "code":          {code},
        "redirect_uri":  {"https://notes.app/callback"},
        "client_id":     {"note_app"},
        "code_verifier": {verifier},
    }
    resp, err := http.PostForm("https://auth.example/token", form)
    if err != nil { return nil, err }
    defer resp.Body.Close()
    var t TokenResp
    json.NewDecoder(resp.Body).Decode(&t)
    return &t, nil
}
Python
import base64, hashlib, secrets, requests

# Build the PKCE pair before redirecting the user.
def new_pkce():
    verifier = base64.urlsafe_b64encode(secrets.token_bytes(32)).rstrip(b"=").decode()
    digest = hashlib.sha256(verifier.encode()).digest()
    challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode()
    return verifier, challenge

# Leg 2: exchange the code (send code_verifier, not the secret).
def exchange(code: str, verifier: str) -> dict:
    resp = requests.post("https://auth.example/token", data={
        "grant_type":    "authorization_code",
        "code":          code,
        "redirect_uri":  "https://notes.app/callback",
        "client_id":     "note_app",
        "code_verifier": verifier,
    })
    resp.raise_for_status()
    return resp.json()   # access_token, refresh_token, id_token, ...
The gap OAuth left open

OAuth sangat baik untuk otorisasi (mendelegasikan apa yang boleh dilakukan client) tapi ia tidak memecahkan autentikasi (membuktikan siapa penggunanya). Ingat: autentikasi = identitas (siapa Anda, nama Anda, ID Anda); otorisasi = izin (apa yang bisa Anda lakukan). OAuth memecahkan yang terakhir lewat delegasi, bukan yang pertama. Celah itulah yang diisi OpenID Connect.

14 OpenID Connect (OIDC)

Sekitar 2014, OpenID Connect (OIDC) dibangun di atas mekanisme keamanan OAuth 2.0 untuk mengisi celah autentikasi. Tambahan intinya adalah ID token: biasanya JWT (persis inilah kenapa JWT pantas dipahami lebih awal; ia muncul kembali di mana-mana).

ID token membawa info identitas: user ID, kapan ia login (field iat), otoritas penerbit (platform mana yang menerbitkan token), dan sering nama serta email pengguna, apa pun yang dibutuhkan relying party. Berkat OIDC, praktis setiap platform kini menawarkan "Sign in with Google / Facebook / Discord." Saat Anda memakainya, OIDC bekerja di balik layar untuk mengambil identitas Anda (email, foto profil, nama) dari Google, dan client menyimpannya atau sekadar memakainya untuk mengautentikasi Anda, tanpa membangun sistem auth sendiri. OIDC menambah lapisan identifikasi di atas OAuth 2.0.

Alur OIDC

Anggap Anda mengunjungi aplikasi pencatat dan mengklik "Sign in with Google".

  1. Client (aplikasi catatan) mengarahkan Anda ke authorization server.
  2. Anda login di authorization server Google (bukan milik client) dan memberi izin yang diminta (baca email / nama / foto profil, dll.).
  3. Authorization server mengembalikan authorization code dan ID token ke client.
  4. Client menukar authorization code dengan resource server untuk mendapatkan access token (dan ID token jika belum dapat).
  5. ID token (sebuah JWT) membawa identitas pengguna (ID, nama, nilai); access token memungkinkan aplikasi catatan melakukan operasi di resource server atas nama Anda, mis. membaca semua catatan Anda dari Google Keep, tapi hanya scope yang Anda beri.
Client note app Auth server Google Resource Google Keep 1 redirect 2 login+grant -> code + ID token 3 exchange code 4 access token + ID token (JWT) 5 use access token -> read notes on your behalf (scoped)
FIG 13, OIDC: an ID token answers "who" on top of OAuth's access token answering "what".

Bersama-sama, OAuth 2.0 dan OpenID Connect bertindak seperti satpam / pembuat kunci era digital: mereka memastikan tidak ada, baik pengguna maupun platform, yang mendapat akses lebih dari yang dibutuhkan atau diminta. Mereka mengubah internet dari kekacauan berbagi kata sandi menjadi sistem yang aman dan saling terhubung, sebab itu kita kini bisa menyambungkan satu platform ke platform lain, berbagi resource dan izin dengan aman, dan menawarkan login satu klik.

15 Memilih Jenis Auth

Aturan praktis untuk empat tipe utama yang harus diketahui setiap engineer backend:

TipeMekanismePakai untuk
Ber-StateSession ID / JWT + store persistenAlur auth aplikasi web, sebagian besar model SaaS, tempat data sesi khusus pengguna tinggal di server
StatelessToken bertanda tangan mandiriAPI dan sistem terdistribusi yang berskala, tempat token membawa info pengguna
OAuth / OIDCToken yang didelegasikan + ID tokenIntegrasi pihak ketiga dan "login via Google / Facebook / Discord"
API keyString secret yang dibagikan di awalServer-to-server / machine-to-machine, atau akses klien tujuan tunggal ke API

Dalam kerja backend sehari-hari membangun API, yang paling sering Anda raih adalah auth ber-state dan stateless.

Part V / Authorization & Hardening

16 Otorisasi & RBAC

Otorisasi tidak seluas autentikasi, tapi gagasan tingkat tingginya penting. Setelah pengguna terautentikasi (mis. ke platform pencatat), mereka bisa membuat, menghapus, dan memperbarui catatan. Sekarang anggap fitur hapus Anda tidak menghapus permanen, ia memindahkan catatan ke keranjang sampah, dan setelah 30 hari mereka pindah ke zona mati (hilang bagi pengguna, tapi tidak dihapus dari DB). Sebagai pembuat, Anda ingin admin UI terpisah dengan kemampuan khusus yang tidak dimiliki pengguna biasa, seperti mengelola catatan zona mati.

Why a "god-mode string" fails

Perbaikan naif adalah mengirim string acak rahasia dengan tiap panggilan API admin supaya server memberi akses yang ditinggikan. Dua masalah: (1) jika ada yang mencegat string itu, mereka bisa menghapus DB atau mengacaukan data pengguna lain, risiko keamanan besar; (2) untuk memberi akses yang sama ke teman/pemelihara tepercaya, Anda harus berbagi string atau menciptakan string lain dan mengkhususkan semuanya di sisi server. Sistemnya cepat jadi rumit, sulit dikelola, dan makin rawan cacat.

Kebutuhan umum, memberi izin spesifik ke pengguna spesifik: persis itulah yang dipecahkan otorisasi. Tidak semua pengguna punya akses yang sama; sebagian lebih banyak, sebagian lebih sedikit, sebagian kemampuan yang sama sekali berbeda. Dalam aplikasi multi-tenant, admin org mungkin menetapkan read atau read/write ke anggota yang berbeda. Model paling terkenalnya RBAC, Role-Based Access Control.

Cara kerja RBAC

Definisikan role (user, admin, moderator, atau role kustom Anda sendiri), dan beri tiap role satu set izin atas resource. Anda bisa segranular apa pun yang Anda suka.

ROLE notes (read/write/delete) dead-zone notes user read / write / delete forbidden moderator read / write forbidden admin read / write / delete full access
FIG 14, RBAC: roles map to permissions per resource. Granularity is up to you.

Alur runtime-nya: pengguna mendaftar dan server menetapkan role (user, admin, ...). Pada tiap request berikutnya pengguna mengirim identifikasinya (session ID untuk ber-state, atau JWT untuk stateless). Di awal siklus request server menyimpulkan role, dari klaim token atau lewat pencarian DB, dan menempelkannya supaya middleware/logika berikutnya bisa memutuskan akses. Jika role-nya admin, endpoint zona mati diizinkan; jika role-nya user, server mengembalikan 403 Forbidden: artinya Anda tidak punya cukup izin untuk resource ini.

Kode: middleware RBAC

Go
// RequireRole runs AFTER an auth middleware that set "user".
func RequireRole(roles ...string) func(http.HandlerFunc) http.HandlerFunc {
    allowed := map[string]bool{}
    for _, r := range roles {
        allowed[r] = true
    }
    return func(next http.HandlerFunc) http.HandlerFunc {
        return func(w http.ResponseWriter, r *http.Request) {
            u, _ := r.Context().Value("user").(User)
            if !allowed[u.Role] {
                http.Error(w, "forbidden", http.StatusForbidden) // 403
                return
            }
            next(w, r)
        }
    }
}

// usage: only admins reach the dead-zone handler
// mux.Handle("/admin/deadzone",
//     RequireJWT(RequireRole("admin")(deadZoneHandler)))
Python
# require_role runs AFTER an auth decorator that set g.user
def require_role(*roles):
    allowed = set(roles)
    def deco(view):
        @wraps(view)
        def wrapper(*args, **kwargs):
            if g.user["role"] not in allowed:
                return jsonify(error="forbidden"), 403   # 403
            return view(*args, **kwargs)
        return wrapper
    return deco

# usage: only admins reach the dead-zone handler
@app.get("/admin/deadzone")
@require_jwt
@require_role("admin")
def dead_zone():
    return jsonify(notes=[...])

Selami: hirarki role, ABAC, dan seterusnya

Di mana RBAC polos mulai renggang: ledakan role

RBAC bersih sampai kebutuhan menjadi kondisional. Saat Anda butuh "editor, tapi hanya untuk dokumen di departemen mereka sendiri, dan hanya saat jam kerja", itu tidak bisa diekspresikan dengan nama role saja, jadi tim menciptakan editordeptfinance, editordeptlegal, dan seterusnya. Inilah ledakan role: jumlah role menggembung karena role hanya bisa meng-encode siapa, bukan resource mana atau dalam kondisi apa.

Hirarki role / pewarisan

Katup pelepas pertama adalah membuat role mewarisi: admin mencakup semua yang bisa moderator, yang mencakup semua yang bisa user. Anda mendefinisikan izin sekali di role terendah dan membiarkan role lebih tinggi mengakumulasikannya, alih-alih mendaftar ulang. Ini menjinakkan duplikasi tapi tidak menambah kondisi.

ABAC: Attribute-Based Access Control

ABAC memutuskan akses dengan mengevaluasi atribut terhadap kebijakan, lintas empat dimensi: subjek (role, departemen, izin tingkat tinggi), resource (pemilik, klasifikasi, departemen), aksi (read/write/delete), dan lingkungan (waktu, IP, perangkat). Satu kebijakan, "pengguna boleh mengedit dokumen jika ia pemiliknya atau berbagi departemen, dan dokumennya tidak diarsipkan", menggantikan lusinan role buatan. ABAC jauh lebih ekspresif; komprominya adalah kebijakan lebih sulit diaudit ("siapa yang bisa menyentuh ini?" tidak terjawab dengan memindai daftar role).

ModelMemutuskan berdasarkanKekuatan / kelemahan
RBACRole yang Anda pegangSederhana, mudah diaudit / ledakan role saat kondisional
ABACAtribut + kebijakan (subjek/resource/aksi/env)Sangat ekspresif / lebih sulit diaudit & dinalar
ReBACRelasi (owner-of, member-of, parent-of)Hebat untuk grafik berbagi (gaya Google Drive) / butuh store relasi, mis. Google Zanzibar

Banyak sistem nyata adalah hibrida: RBAC untuk role kasar, kebijakan ABAC untuk sisi kondisional, dan ReBAC saat akses mengikuti grafik berbagi.

Kode: evaluator kebijakan ABAC kecil

Go
type Subject struct{ ID, Dept, Role string }
type Resource struct{ OwnerID, Dept string; Archived bool }

// Policy: can `s` perform `action` on `r`, given the environment?
func CanEdit(s Subject, r Resource, hour int) bool {
    if r.Archived {
        return false
    }
    sameDept := s.Dept == r.Dept
    owns := s.ID == r.OwnerID
    businessHours := hour >= 9 && hour < 18
    // owner OR same-department editor, only in business hours
    return (owns || (sameDept && s.Role == "editor")) && businessHours
}

// usage inside a handler, after auth has populated the subject:
// if !CanEdit(subj, doc, time.Now().Hour()) {
//     http.Error(w, "forbidden", http.StatusForbidden)
// }
Python
from dataclasses import dataclass
from datetime import datetime

@dataclass
class Subject: id: str; dept: str; role: str
@dataclass
class Resource: owner_id: str; dept: str; archived: bool

# Policy: can `s` perform the action on `r`, given the environment?
def can_edit(s: Subject, r: Resource, hour: int) -> bool:
    if r.archived:
        return False
    owns = s.id == r.owner_id
    same_dept = s.dept == r.dept
    business_hours = 9 <= hour < 18
    # owner OR same-department editor, only in business hours
    return (owns or (same_dept and s.role == "editor")) and business_hours

# usage inside a view, after auth populated the subject:
# if not can_edit(subj, doc, datetime.now().hour):
#     return jsonify(error="forbidden"), 403

17 Pesan Error & Serangan Timing

Pesan error generik

Saat auth, Anda tergoda mengirim pesan membantu: "user not found," "incorrect password," "account locked due to too many failed attempts." Ini membantu pengguna yang sah, tapi memberi penyerang petunjuk. "User not found" memberi tahu penyerang username itu tidak ada, jadi mereka pindah ke yang berikutnya. "Incorrect password" mengonfirmasi username-nya valid, jadi mereka memusatkan serangan brute-force/kamus hanya pada kata sandi, memperluas permukaan serangan.

Rule

Dalam alur autentikasi, selalu kirim satu pesan generik: mis. autentikasi gagal, untuk setiap kasus kegagalan, supaya penyerang tidak bisa tahu langkah mana yang gagal atau merencanakan langkah berikutnya. Pesan ramah pengguna boleh di tempat lain (validasi, API lain), tapi tidak di sini.

Serangan timing

Bahkan dengan pesan generik, waktu yang diambil sebuah respons bisa membocorkan informasi. Login khas melakukan tiga langkah berurutan:

1 / find user does email exist? 2 / check locked too many attempts? 3 / compare hash slow on purpose fail here -> fast reply fail here -> slower (hashing)
FIG 15, If an invalid username short-circuits at step 1, the faster reply reveals it.

Kata sandi disimpan sebagai hash (tak pernah teks polos): saat pendaftaran kata sandi di-hash menjadi string aman kriptografis; saat login server men-hash kata sandi yang diberikan dengan cara yang sama dan membandingkannya dengan hash tersimpan. Jebakannya: jika username tidak valid, sistem berhenti di langkah 1 dan membalas cepat. Jika username valid tapi kata sandi salah, ia mencapai langkah 3 dan lebih lambat, karena hashing memakan waktu lebih lama daripada pemeriksaan awal. Dari jeda itu, katakanlah 200 ms, penyerang bisa menyimpulkan apakah username ada dan di langkah mana auth gagal, lalu menyusun strategi.

Two defenses

1. Perbandingan waktu konstan. Pakai fungsi perbandingan waktu-konstan yang aman secara kriptografis untuk hash, yang waktu eksekusinya tidak berubah dengan kemiripan input. 2. Samakan/simulasikan jeda. Bahkan saat username tidak ada, jangan membalas seketika, simulasikan jeda (Node: setTimeout; Go: time.Sleep) supaya penyerang tidak bisa membedakan kegagalan username dari kegagalan kata sandi.

Kode: pesan generik + waktu yang disamakan

Go
// dummyHash: a fixed, precomputed bcrypt hash, used to equalize timing.
var dummyHash = "$2a$12$abcdefghijklmnopqrstuv0000000000000000000000000000000000"

func Authenticate(w http.ResponseWriter, email, pw string) {
    start := time.Now()
    const floor = 250 * time.Millisecond // equalize every path

    defer func() { // pad so all outcomes take ~the same time
        if d := time.Since(start); d < floor {
            time.Sleep(floor - d)
        }
    }()

    user, found := lookupByEmail(email)
    if !found {
        // still run a dummy hash so step-3 cost is paid anyway
        bcrypt.CompareHashAndPassword([]byte(dummyHash), []byte(pw))
        http.Error(w, "authentication failed", http.StatusUnauthorized)
        return
    }
    // CompareHashAndPassword is constant-time internally
    if bcrypt.CompareHashAndPassword([]byte(user.Hash), []byte(pw)) != nil {
        http.Error(w, "authentication failed", http.StatusUnauthorized)
        return
    }
    // success: issue session / JWT ...
}
Python
import time

DUMMY_HASH = bcrypt.hashpw(b"x", bcrypt.gensalt())
FLOOR = 0.25  # seconds, equalize every path

def authenticate(email: str, pw: str):
    start = time.perf_counter()
    try:
        user = lookup_by_email(email)
        if user is None:
            # run a dummy hash so timing matches the valid path
            bcrypt.checkpw(pw.encode(), DUMMY_HASH)
            return None, "authentication failed"
        if not bcrypt.checkpw(pw.encode(), user["hash"]):  # constant-time
            return None, "authentication failed"
        return user, None  # success: issue session / JWT
    finally:
        # pad so all outcomes take ~the same wall-clock time
        elapsed = time.perf_counter() - start
        if elapsed < FLOOR:
            time.sleep(FLOOR - elapsed)

18 Crib-Sheet Debug

Seluruh manual, dipadatkan menjadi yang Anda raih saat tertekan.

KonsepSatu baris
AutentikasiSiapa Anda?, menetapkan identitas dalam konteks.
OtorisasiApa yang bisa Anda lakukan?, izin dalam konteks itu.
FaktorSesuatu yang Anda tahu / miliki / adalah (gabungkan untuk MFA).
HashingSatu arah, panjang tetap, deterministik. Simpan hash-nya, jangan pernah kata sandinya.
SesiMemori sisi server; cookie hanya memegang session ID.
JWTheader.payload.signature; mandiri, stateless, ditandatangani dengan secret.
CookiePenyimpanan browser yang di-set server; terkirim otomatis per request; pakai HttpOnly + Secure + SameSite.
Ber-StatePencarian store tiap request; mudah dicabut; lebih sulit berskala.
StatelessTanpa store; berskala hebat; pencabutan adalah titik lemah.
API keySecret yang dibagikan di awal untuk machine-to-machine; simpan hash-nya.
OAuth 2.0Otorisasi ter-delegasi dan ter-scope via bearer token.
OIDCMenambah ID token (JWT) di atas OAuth untuk autentikasi.
RBACRole -> izin -> resource. 403 Forbidden saat kurang.
Higiene errorSatu pesan "autentikasi gagal" generik untuk semua kegagalan.
TimingPerbandingan waktu konstan + samakan waktu respons.
Tips produksiBikin sendiri untuk belajar; pakai auth provider untuk rilis.