Authentication vs Authorization: Bedanya Apa?
Sebelum masuk ke OAuth, penting untuk bedain dua konsep yang sering ketuker:
- Authentication (autentikasi): "Siapa kamu?" verifikasi identitas. Login pakai username + password itu authentication.
- Authorization (otorisasi): "Kamu boleh ngapain?" verifikasi hak akses. Setelah login, boleh akses data apa aja itu authorization.
OAuth 2.0 itu authorization framework, bukan authentication. Tapi karena sering dipake bareng OpenID Connect (yang nambah authentication layer), orang sering nyebutnya "login dengan Google" padahal di baliknya ada dua protokol berbeda.
Masalah yang OAuth Selesaikan
Sebelum OAuth, kalau kamu mau app A akses data kamu di Google, solusinya...kasih password Google ke app A.
Coba bayangin konsekuensinya:
- App A punya akses penuh ke semua yang bisa kamu lakukan di Google
- Kalau app A di-hack, password Google kamu bocor
- Kalau mau cabut akses app A, kamu harus ganti password Google dan semua app lain yang pakai password itu ikut kena
Ini jelas buruk. OAuth muncul untuk ngasih cara yang lebih aman: app A bisa dapet akses terbatas ke data kamu di Google, tanpa pernah tau password kamu.
Empat Peran di OAuth 2.0
OAuth punya empat peran yang harus dipahami:
| Peran | Definisi | Contoh |
|---|---|---|
| Resource Owner | Pemilik data biasanya user | Kamu |
| Client | Aplikasi yang minta akses | App foto pihak ketiga |
| Resource Server | Server yang nyimpan data yang dilindungi | Google Photos API |
| Authorization Server | Server yang issue token setelah user approve | Google Auth Server |
graph TB
RO["Resource Owner (User)"]
C["Client (App pihak ketiga)"]
AS["Authorization Server (Google)"]
RS["Resource Server (Google Photos API)"]
RO -->|"1. Approve akses"| AS
AS -->|"2. Issue token"| C
C -->|"3. Akses dengan token"| RS
OAuth 2.0 Grant Types
OAuth punya beberapa "flow" yang disebut grant type. Masing-masing untuk situasi yang berbeda.
| Grant Type | Untuk | Rekomendasi |
|---|---|---|
| Authorization Code | Web app dengan backend server | ✅ Gunakan ini |
| Authorization Code + PKCE | Mobile app / SPA (public client) | ✅ Gunakan ini |
| Client Credentials | Machine-to-machine (server ke server) | ✅ Gunakan ini |
| Device Code | Smart TV, CLI tools | ✅ Kalau perlu |
| SPA (deprecated) | ❌ Jangan | |
| Aplikasi yang percaya penuh | ❌ Jangan |
Dua yang di-deprecated karena ada security issue. Yang akan kita bahas detail adalah Authorization Code (dengan dan tanpa PKCE) karena yang paling umum.
Authorization Code Flow (Confidential Client)
Ini flow yang dipakai web app yang punya backend server. Disebut "confidential client" karena bisa nyimpan secret secara aman di server.
sequenceDiagram
participant U as User
participant C as Client (Web App)
participant AS as Authorization Server
participant RS as Resource Server
Note over U,C: 1. User klik "Login dengan Google"
C->>AS: Redirect ke /authorize?client_id=...&response_type=code&scope=...&redirect_uri=...&state=xyz
Note over U,AS: 2. User login dan approve
AS->>U: Tampilkan login page
U->>AS: Login + approve scope
Note over AS,C: 3. Authorization Code dikirim
AS->>C: Redirect ke redirect_uri?code=AUTH_CODE&state=xyz
Note over C,AS: 4. Tukar code dengan token (server-to-server)
C->>AS: POST /token {code, client_id, client_secret, redirect_uri}
AS-->>C: {access_token, refresh_token, id_token}
Note over C,RS: 5. Akses resource dengan token
C->>RS: GET /photos dengan Authorization: Bearer access_token
RS-->>C: Data
Step by step:
1. Mulai flow
User klik "Login dengan Google". Client redirect user ke Authorization Server dengan parameter:
https://accounts.google.com/o/oauth2/auth?
client_id=YOUR_CLIENT_ID
&response_type=code
&scope=openid%20email%20profile
&redirect_uri=https://yourapp.com/callback
&state=random_state_string
Parameter penting:
client_ididentitas aplikasi kamu, didapat saat register di Googleresponse_type=codeminta authorization codescopehak akses apa yang diminta (lebih detail nanti)redirect_uriURL yang Google akan redirect setelah user approvestaterandom string untuk CSRF protection. Wajib. Kalau ga ada, bisa kena CSRF attack
2. User Login dan Approve
Google tampilkan halaman login (kalau belum login) dan consent screen "App X minta izin untuk: lihat email kamu, akses profil kamu."
User setuju atau tolak.
3. Authorization Code Dikirim
Kalau setuju, Google redirect user ke redirect_uri kamu dengan authorization code:
https://yourapp.com/callback?code=4/P7q7W91a-oMsCeLvIaQm6bTrgtp7&state=random_state_string
Authorization code ini masa berlakunya sangat pendek (biasanya 1-10 menit) dan single-use. Ini bukan token, ini cuma tiket untuk minta token.
Cek state dari response harus sama dengan yang dikirim. Kalau beda, kemungkinan CSRF attack.
4. Tukar Code dengan Token
Di backend server (bukan di browser), client POST ke token endpoint dengan code + client secret:
POST https://oauth2.googleapis.com/token
Content-Type: application/x-www-form-urlencoded
code=4/P7q7W91a-oMsCeLvIaQm6bTrgtp7
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
&redirect_uri=https://yourapp.com/callback
&grant_type=authorization_code
Kenapa harus di backend? Karena client_secret ini rahasia ga boleh terekspos ke browser/client.
Response:
{
"access_token": "ya29.a0AfH6SMC...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "1//0g...",
"scope": "openid email profile",
"id_token": "eyJhbGciOiJSUzI1NiJ9..."
}
5. Akses Resource
Pakai access_token untuk akses Google Photos API:
GET https://photoslibrary.googleapis.com/v1/albums
Authorization: Bearer ya29.a0AfH6SMC...
Kenapa ada authorization code dulu, bukan langsung token?
Kenapa ga langsung dapet token dari redirect? Kenapa harus dua langkah?
Karena authorization code dikirim lewat browser redirect URL bisa ketahuan dari history, referrer headers, atau browser logs. Kalau token langsung di URL, token bisa bocor.
Dengan code exchange, yang lewat browser cuma code yang short-lived dan single-use. Token yang sebenarnya ditukar di channel yang aman (server-to-server, ga lewat browser).
Authorization Code Flow + PKCE (Public Client)
Mobile app dan SPA (Single Page App) ga bisa nyimpan client_secret dengan aman. Secret di mobile app bisa di-extract dari APK. Secret di browser JavaScript bisa dilihat siapa aja.
Solusinya: PKCE (Proof Key for Code Exchange, dibaca "pixie").
PKCE menggantikan client secret dengan cryptographic proof yang dibuat per-request.
sequenceDiagram
participant C as Client (Mobile/SPA)
participant AS as Authorization Server
Note over C: Generate code_verifier (random string)
Note over C: code_challenge = BASE64URL(SHA256(code_verifier))
C->>AS: /authorize?...&code_challenge=xxx&code_challenge_method=S256
Note over AS: Simpan code_challenge
AS->>C: Authorization Code
C->>AS: POST /token {code, code_verifier}
Note over AS: Verify: SHA256(code_verifier) == code_challenge?
AS-->>C: Tokens
Caranya:
- Client generate code_verifier random string 43-128 karakter
- Client hitung code_challenge =
BASE64URL(SHA256(code_verifier)) - Kirim
code_challengesaat request authorization - Authorization server simpan
code_challenge - Saat tukar code dengan token, kirim
code_verifier(yang original) - Authorization server hitung sendiri SHA256 dari verifier, cocokkan dengan challenge yang tersimpan
Kalau attacker intercept authorization code, mereka ga punya code_verifier dan ga bisa tukar code dengan token.
Sekarang PKCE direkomendasikan untuk semua client, termasuk web app dengan backend.
Client Credentials Flow
Ini untuk machine-to-machine ga ada user yang login, dua service yang saling berkomunikasi.
sequenceDiagram
participant S as Service A (Client)
participant AS as Authorization Server
participant RS as Service B (Resource Server)
S->>AS: POST /token {client_id, client_secret, grant_type=client_credentials}
AS-->>S: {access_token}
S->>RS: Request dengan access_token
RS-->>S: Response
Contoh: microservice billing yang perlu akses microservice user untuk cek data subscription.
POST /token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id=billing-service
&client_secret=xxx
&scope=user:read
Ga ada authorization code, ga ada user redirect langsung dapet token.
OpenID Connect (OIDC): OAuth untuk Authentication
OAuth 2.0 itu authorization framework ngasih akses ke resource. Tapi OAuth ga ngasih cara standar untuk tahu siapa user-nya.
OpenID Connect (OIDC) itu layer di atas OAuth 2.0 yang nambah authentication. OIDC standarisasi gimana client bisa dapet informasi tentang user yang sudah login.
graph TB
OIDC["OpenID Connect (Authentication)"]
OAuth["OAuth 2.0 (Authorization)"]
HTTP["HTTP/TLS (Transport)"]
OIDC --> OAuth --> HTTP
Yang ditambah OIDC:
- ID Token JWT yang berisi informasi user (siapa yang login)
- UserInfo Endpoint endpoint standar untuk dapet info user
- Standard Claims claim standar untuk user info (sub, email, name, dll)
- Discovery endpoint
.well-known/openid-configurationyang describe semua endpoint
ID Token vs Access Token
Ini sering bikin bingung:
| Access Token | ID Token | |
|---|---|---|
| Untuk siapa | Resource Server (API) | Client (App kamu) |
| Isinya | Hak akses (scopes) | Informasi user |
| Format | Bisa JWT atau opaque | Selalu JWT |
| Dipakai untuk | Akses API | Tahu siapa yang login |
| Dikirim ke API? | Ya | Tidak |
ID Token itu buat app kamu, bukan buat dikirim ke API. ID Token ngasih tau siapa yang sudah login. Access Token yang dikirim ke API untuk akses resource.
Claims di ID Token
ID Token itu JWT. Kalau di-decode, payload-nya berisi:
{
"iss": "https://accounts.google.com",
"sub": "110169484474386276334",
"aud": "812741506391.apps.googleusercontent.com",
"iat": 1353601026,
"exp": 1353604926,
"email": "janedoe@gmail.com",
"email_verified": true,
"name": "Jane Doe",
"picture": "https://lh4.googleusercontent.com/...",
"given_name": "Jane",
"family_name": "Doe"
}
Standard claims:
subSubject. Unique identifier user di Authorization Server. Ini yang dipake sebagai user ID, bukan email. Email bisa ganti, sub tidak.issIssuer. Siapa yang issue tokenaudAudience. Untuk siapa token ini (client_id kamu)iatIssued atexpExpiryemail,name,pictureuser info (kalau scope-nya diminta)
UserInfo Endpoint
Alternatif dari ID Token, bisa juga call UserInfo endpoint dengan access token:
GET https://openidconnect.googleapis.com/v1/userinfo
Authorization: Bearer access_token
Response:
{
"sub": "110169484474386276334",
"email": "janedoe@gmail.com",
"name": "Jane Doe"
}
Scopes: Minta Hak Akses yang Tepat
Scope itu hak akses spesifik yang diminta client. Prinsipnya: minta sesedikit mungkin yang dibutuhkan (principle of least privilege).
Contoh scope Google:
openid → Aktifkan OIDC, dapet ID token
email → Baca email address
profile → Baca nama, foto, dll
https://www.googleapis.com/auth/gmail.readonly → Baca email di Gmail
https://www.googleapis.com/auth/calendar → Akses Google Calendar
Kalau app cuma perlu email user untuk register, minta openid email ga perlu gmail.readonly atau akses yang lebih luas.
User akan lihat consent screen dengan daftar scope yang kamu minta. Semakin banyak akses yang diminta, semakin kecil kemungkinan user setuju.
Refresh Token
Access token punya masa berlaku pendek (biasanya 1 jam). Setelah expired, ada dua pilihan:
- Suruh user login ulang buruk untuk UX
- Pakai refresh token untuk minta access token baru secara silent
sequenceDiagram
participant C as Client
participant AS as Authorization Server
Note over C: Access token expired
C->>AS: POST /token {grant_type=refresh_token, refresh_token=xxx, client_id, client_secret}
AS-->>C: {access_token baru, refresh_token baru (opsional)}
Note over C: Simpan token baru
Refresh token biasanya:
- Masa berlaku jauh lebih panjang (hari/bulan)
- Bisa di-revoke
- Harus disimpan dengan aman jangan di localStorage browser
Kalau refresh token bocor, attacker bisa terus minta access token baru sampai refresh token di-revoke.
Token Storage: Jangan Salah Tempat Nyimpan
| Token | Web App (Backend) | SPA (Browser) | Mobile App |
|---|---|---|---|
| Access Token | Memory / server session | Memory (JS variable) | Secure storage |
| Refresh Token | HttpOnly cookie / server session | ❌ Jangan di localStorage | Keychain / Keystore |
| ID Token | Server session | Memory (kalau perlu) | Secure storage |
Jangan pernah simpan refresh token di localStorage. XSS bisa baca localStorage → refresh token bocor → attacker bisa impersonate user selamanya.
HttpOnly cookie yang di-set server itu aman dari XSS karena JavaScript ga bisa baca HttpOnly cookie.
OAuth di Microservice
Di microservice, ada pattern umum: API Gateway verify token, service di belakangnya trust gateway.
graph LR
Client -->|"Bearer token"| GW["API Gateway"]
GW -->|"Verify token"| AS["Authorization Server"]
AS -->|"Valid + claims"| GW
GW -->|"X-User-Id, X-User-Role"| SA["Service A"]
GW -->|"X-User-Id, X-User-Role"| SB["Service B"]
Gateway verify token sekali, extract claims (user ID, roles, scopes), terus forward ke service sebagai trusted header.
Service di belakang ga perlu verify token lagi mereka trust header dari gateway (dan gateway ada di internal network, ga bisa diakses langsung).
Common Mistakes
1. Ga validasi state parameter
# ❌ Bahaya
@app.route('/callback')
def callback():
code = request.args.get('code')
# langsung tukar code, ga cek state
# ✅ Benar
@app.route('/callback')
def callback():
state = request.args.get('state')
if state != session.pop('oauth_state', None):
abort(403) # CSRF!
code = request.args.get('code')
Tanpa validasi state, app kamu rentan CSRF. Attacker bisa trick user untuk menghubungkan akun mereka ke akun attacker.
2. Simpan refresh token di localStorage
// ❌ Jangan
localStorage.setItem('refresh_token', token);
// ✅ Serahkan ke backend, simpan di HttpOnly cookie
// Frontend ga perlu pegang refresh token
3. Ga cek aud di ID Token
# ❌ Bahaya - ga cek audience
decoded = jwt.decode(id_token, key, algorithms=['RS256'])
# ✅ Selalu cek audience
decoded = jwt.decode(
id_token, key,
algorithms=['RS256'],
audience=YOUR_CLIENT_ID # Pastikan token memang untuk app kamu
)
Kalau ga cek aud, token yang diissue untuk app lain bisa dipake ke app kamu.
4. Pakai email sebagai user identifier
# ❌ Email bisa ganti
user = db.find_by_email(id_token['email'])
# ✅ Pakai kombinasi iss + sub (stable dan unique)
user = db.find_by_provider(
provider=id_token['iss'],
provider_user_id=id_token['sub']
)
User bisa ganti email di Google. sub adalah identifier yang permanent dan unik per user per provider.
5. Redirect URI yang terlalu permissive
# ❌ Terlalu lebar
redirect_uri=https://yourapp.com/
# ✅ Exact match
redirect_uri=https://yourapp.com/auth/callback
Authorization server harus validasi exact match redirect URI. Kalau terlalu permissive, attacker bisa arahkan authorization code ke domain mereka.
6. Tidak pakai PKCE
# ❌ Tanpa PKCE (untuk SPA/mobile)
url = f"{AUTH_URL}?client_id={CLIENT_ID}&response_type=code&..."
# ✅ Dengan PKCE
import secrets, hashlib, base64
code_verifier = secrets.token_urlsafe(64)
code_challenge = base64.urlsafe_b64encode(
hashlib.sha256(code_verifier.encode()).digest()
).rstrip(b'=').decode()
url = f"{AUTH_URL}?...&code_challenge={code_challenge}&code_challenge_method=S256"
OIDC Discovery
OIDC punya standar discovery endpoint di:
https://accounts.google.com/.well-known/openid-configuration
Endpoint ini return JSON yang describe semua endpoint (authorization, token, userinfo, jwks_uri), algoritma yang didukung, dan capabilities lainnya.
Library OAuth/OIDC biasanya auto-discover dari sini, jadi kamu ga perlu hardcode URL.
Ringkasan
| Konsep | Penjelasan |
|---|---|
| OAuth 2.0 | Framework untuk authorization delegasi akses ke resource |
| OpenID Connect | Layer authentication di atas OAuth 2.0 |
| Authorization Code Flow | Flow utama untuk web app dengan backend |
| PKCE | Extension untuk public client (mobile, SPA) |
| Client Credentials | Flow untuk machine-to-machine |
| Access Token | Token untuk akses API/resource |
| ID Token | JWT berisi informasi user (OIDC) |
| Refresh Token | Token untuk minta access token baru |
| Scope | Hak akses spesifik yang diminta |
| State | CSRF protection parameter |
Kesimpulan
"Login dengan Google" yang kelihatan simpel itu sebenernya ada banyak yang terjadi di baliknya.
Yang perlu diingat:
- OAuth ≠ Authentication. OAuth itu authorization. OpenID Connect yang nambah authentication
- Gunakan Authorization Code + PKCE aman untuk semua jenis client, ga ada alasan pakai Implicit
- ID Token untuk app kamu, Access Token untuk API jangan tukar-tukar
- Pakai
subsebagai user identifier, bukan email - Refresh token itu sensitif simpan di tempat yang aman, jangan di localStorage
- Validasi
stateselalu, tanpa pengecualian - Minta scope seminimal mungkin prinsip least privilege
OAuth dan OIDC itu kompleks, tapi library modern (Passport.js, spring-security-oauth2, authlib) handle banyak detail ini. Yang penting paham konsepnya supaya tahu cara konfigurasinya dengan benar dan bisa debug kalau ada yang salah.
