OAuth2 & OpenID Connect (OIDC)
Pernahkah Anda membangun aplikasi dan berharap "kok gak bisa sekali klik langsung login pakai Google aja?" Nah, jawabannya ada di dua standar yang hampir selalu bekerja berdampingan: OAuth2 dan OpenID Connect (OIDC). Di artikel ini kita tidak akan menyentuh detail frontend terlalu dalam — kita fokus dari sisi backend: bagaimana token diproduksi, bagaimana API Anda memverifikasinya, dan bagaimana semua itu diintegrasikan ke Spring Boot.
OAuth2 itu seperti ember: protokol untuk membawa dan menyerahkan "hak akses". OIDC itu seperti tambahan stiker nama di ember: memberitahu siapa pemilik akses tersebut. Di artikel JWT kita sudah melihat format token yang ditandatangani. Sekarang kita lihat bagaimana token itu diterbitkan oleh pihak ketiga seperti Okta, Google, atau Auth0 — dan kenapa API Anda cukup memegang public key tanpa perlu menyimpan rahasia apa pun.
Kenapa OAuth2 Ada
Bayangkan Anda membangun aplikasi notes yang pengguna login lewat Google. Ada dua pendekatan:
- Pendekatan lama (salah): minta pengguna menyerahkan email dan password akun Google-nya ke aplikasi Anda. Ini buruk — password pengguna bocor ke aplikasi yang bahkan bukan punya Google.
- Pendekatan OAuth2: Google memverifikasi pengguna, lalu memberi aplikasi Anda token yang hanya berisi izin yang diperlukan. Password tidak pernah lewat aplikasi Anda.
Secara singkat, OAuth2 adalah kerangka otorisasi yang memungkinkan aplikasi memperoleh akses terbatas ke akun pengguna di layanan HTTP. Karena identitas (login) ditangani penyedia eksternal, aplikasi Anda tidak perlu menyimpan tabel password, tidak perlu tahu aturan keamanan password, dan tidak perlu memikirkan reset password. Itu sebabnya OAuth2 menjadi fondasi dari hampir semua tombol "Login with ...".
Single Sign-On (SSO)
Kalau perusahaan Anda punya Okta sebagai identity provider, karyawan login sekali lalu otomatis masuk ke Gmail, Slack, dan aplikasi internal perusahaan. Ini disebut SSO (Single Sign-On). Dari perspektif backend, setiap aplikasi cukup mempercayai token dari Okta — mereka tidak perlu membangun sistem login sendiri. OIDC dibangun di atas OAuth2 persis untuk skenario "satu login, banyak aplikasi".
Delegasi Akses
Kasus lain: aplikasi Anda ingin mengambil calendar pengguna dari Google Calendar. Anda tidak butuh password pengguna; Anda butuh izin (consent) untuk membaca kalender. OAuth2 memungkinkan pengguna memberi izin itu tanpa membagikan kredensial — inilah yang disebut delegasi: pengguna melimpahkan hak akses tertentu kepada aplikasi Anda.
Masalah yang Dipecahkan OAuth2
Dua masalah inti yang diselesaikan OAuth2:
- Password tidak boleh sampai ke aplikasi klien. Kredensial pengguna hanya dikirim ke Authorization Server (Okta/Google/Auth0). Aplikasi Anda hanya menerima token.
- Akses harus terbatas. Pengguna tidak memberi "semua akses", cukup izin yang diminta — konsep ini disebut scope.
Scopes adalah "lembar izin" token. Misalnya
openid(untuk identitas),profile,calendar:read. Jangan pernah meminta scope lebih dari yang Anda butuhkan — ingat prinsip least privilege.
Empat Aktor OAuth2
Sebelum masuk ke token, kita harus hafal empat peran yang selalu ada di setiap alur OAuth2. Semuanya didefinisikan secara resmi di RFC 6749.
| Aktor | Peran | Contoh nyata |
|---|---|---|
| Resource Owner | Pemilik data; orang yang memberi izin | Pengguna akhir yang menekan tombol "Izinkan" |
| Client | Aplikasi yang meminta akses atas nama Resource Owner | Aplikasi notes Anda (SPA, mobile, atau server) |
| Authorization Server | Penerbit token; memverifikasi identitas & consent | Okta, Google, Auth0, Keycloak |
| Resource Server | Server yang menyimpan data/proteksi; memvalidasi token | API backend Anda (/api/notes) |
Perhatikan perbedaannya: Client adalah aplikasi Anda, bukan manusia. Dan Resource Server bisa jadi berbeda sama sekali dari Authorization Server — justru inilah intinya: Okta yang menerbitkan token, API Anda yang memverifikasinya, dan keduanya tidak perlu saling mengenal secara langsung.
Token OAuth2
Semua token OAuth2 berformat JWT jika dikeluarkan oleh penyedia modern. Ada tiga jenis yang wajib Anda bedakan:
| Token | Isi | Umur | Dipakai untuk |
|---|---|---|---|
access_token | JWT berisi scope & identitas yang relevan untuk API | Pendek (mis. 5–60 menit) | Dikirim ke Resource Server di header Authorization: Bearer ... |
refresh_token | Opaque string (biasanya bukan JWT) | Panjang (mis. 7–30 hari) | Mendapatkan access_token baru tanpa login ulang; diputar tiap dipakai |
id_token | JWT khusus OIDC, berisi identitas pengguna (sub, email, name) | Pendek | Diklaim oleh Client untuk mengenali "siapa yang login" |
Jangan tertukar:
access_tokenadalah bukti otorisasi (boleh memanggil API apa).id_tokenadalah bukti autentikasi (identitas siapa). API Anda hanya perluaccess_token;id_tokendipakai di sisi aplikasi, bukan untuk otorisasi API.
Sebagai gambaran, isi access_token khas dari Okta (setelah di-decode dari Base64URL, seperti yang sudah kita pelajari di artikel JWT) kira-kira seperti ini:
{
"ver": 1,
"iss": "https://your-tenant.okta.com",
"aud": "api://default",
"iat": 1754342400,
"exp": 1754344200,
"jti": "AT.abc123",
"sub": "00uid4BxXw6I6TV4m0g3",
"scp": ["openid", "profile", "email", "notes:read"],
"uid": "00uid4BxXw6I6TV4m0g3"
}
Perhatikan: iss menunjuk ke Okta, aud menunjuk ke API Anda, scp berisi scope yang disetujui user, dan exp hanya 30 menit setelah iat. Semua field ini nanti dipakai untuk verifikasi dan otorisasi di Resource Server.
Refresh token dibuat berumur panjang sehingga berbahaya jika bocor — makanya refresh token biasanya diputar (yang lama dinonaktifkan begitu yang baru terbit) dan bisa dicabut di server. Ini persis konsep yang sudah dibahas di artikel JWT bagian refresh token.
Grant Types: Bagaimana Token Diperoleh
RFC 6749 mendefinisikan beberapa grant types — cara yang berbeda untuk mendapatkan token, tergantung jenis klien dan konteksnya.
| Grant type | Cocok untuk | Alur inti |
|---|---|---|
authorization_code (+ PKCE) | SPA, aplikasi mobile, web apps | User login di Auth Server, kode ditukar token di backend |
client_credentials | Service-to-service (M2M) | Client memakai client secret-nya sendiri, tanpa user |
implicit | Deprecated | Token langsung di URL redirect; rawan bocor lewat history/log |
password (resource owner) | Legacy, hindari | Client mengirim username/password langsung ke token endpoint |
Dua yang pertama akan kita bedah. Yang dua terakhir hanya perlu Anda tahu bahwa keduanya sudah tidak disarankan — jangan pakai untuk proyek baru.
Authorization Code + PKCE (untuk SPA & Mobile)
Ini adalah alur yang paling penting di praktik nyata. Ide intinya: kode sementara (authorization code) ditukar dengan token di sisi server, jadi token tidak pernah terlihat oleh JavaScript di browser. Mari kita bedah langkah demi langkah:
- Client mengarahkan user ke Authorization Server — misalnya
https://okta.com/authorize?response_type=code&client_id=...&redirect_uri=...&scope=openid&state=...&code_challenge=...&code_challenge_method=S256. - User login & memberi izin di halaman Okta (browser diarahkan ke sana).
- Auth Server mengarahkan balik ke
redirect_uriAnda dengan membawa?code=...&state=.... Kode ini berumur sangat pendek (hitungan menit) dan hanya sah sekali. - Client menukar
codeke token endpoint:POST /tokendengangrant_type=authorization_code,code,redirect_uri,code_verifier, dan — untuk SPA — tanpa client secret. - Auth Server memvalidasi kode +
code_verifier, lalu mengembalikanaccess_token,refresh_token, danid_token. - Client menyimpan token dan mulai memanggil API Anda dengan
Authorization: Bearer <access_token>.
Semua percakapan di langkah 4–5 terjadi di server side atau lewat permintaan yang tidak bisa diintip JavaScript — itu yang membuat token tidak bocor ke browser.
Sebagai ilustrasi, langkah 4 terlihat seperti ini:
POST /oauth2/v1/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=0O2mZ2f... (kode sementara dari langkah 3)
&redirect_uri=https://app.example.com/callback
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
&client_id=0oa1234567890
Dan responsnya membawa ketiga token sekaligus:
{
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyJ9.eyJpc3MiOiJodHRwczovL29rdGEuY29tIiwiZXhwIjoxNzU0MzQ0MjAwLCJzY3AiOlsib3BlbmlkIiwibm90ZXM6cmVhZCJdfQ.s0me-Sign4tur3",
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyJ9.eyJpc3MiOiJodHRwczovL29rdGEuY29tIiwic3ViIjoiMDB1aWQ0QnhYdzZJNlRWNG0wZzMiLCJlbWFpbCI6ImhlbmRybyBtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlfQ.diFF3-Rent-Sign4tur3",
"token_type": "Bearer",
"expires_in": 1800,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA"
}
Setelah ini, langkah 5–6 berjalan: token disimpan, expires_in dicatat (dalam detik, di sini 1800 detik = 30 menit), dan refresh token disimpan untuk nanti — biasanya di secure storage perangkat, bukan di variabel global.
PKCE: Kenapa SPA Butuh code_verifier
Masalah klasik SPA: ia tidak punya client secret yang bisa dirahasiakan (kode JavaScript-nya bisa dibaca siapa saja). Kalau tanpa proteksi, penyerang bisa mencuri kode yang lewat di URL. Solusinya adalah PKCE (Proof Key for Code Exchange, didefinisikan di RFC 7636):
- Sebelum mengarahkan user, SPA membuat
code_verifier(string acak panjang) dan hash-nya,code_challenge. code_challengedikirim saatauthorize,code_verifierdikirim saat menukar kode.- Auth Server hanya menukar kode jika hash
code_verifiercocok dengancode_challenge.
Analoginya:
code_challengeitu seperti "SMS berisi teka-teki", dancode_verifieradalah jawabannya. Hanya aplikasi yang menyimpan jawabannya yang bisa menukar kode menjadi token. Penyerang yang mencuri kode pun tetap tidak bisa menukarnya tanpacode_verifier.
Client Credentials (untuk Service-to-Service)
Kadang yang memanggil API Anda bukan pengguna, tapi service lain (misalnya job scheduler yang meng-update laporan). Di sini tidak ada user dan tidak ada consent. Client memakai client_id + client_secret miliknya sendiri untuk meminta token:
POST /oauth2/token
grant_type=client_credentials
client_id=<id aplikasi>
client_secret=<secret aplikasi>
scope=internal:report
Token yang dihasilkan mewakili aplikasi, bukan pengguna — klaim sub di dalamnya biasanya adalah identitas client. Karena secret disimpan di server, grant type ini relatif aman dan sangat umum untuk komunikasi antar microservice.
OIDC vs OAuth2
Keduanya sering disebut bersamaan, tapi keduanya menjawab pertanyaan berbeda:
| Aspek | OAuth2 | OpenID Connect (OIDC) |
|---|---|---|
| Menjawab pertanyaan | "Bolehkah aplikasi mengakses resource ini?" | "Siapa pengguna yang login?" |
| Fokus | Otorisasi — token akses | Autentikasi — identitas pengguna |
| Token utama | access_token | id_token (JWT) |
| Klaim identitas | Tidak dijamin ada | sub, email, name dari /userinfo |
| Standar | RFC 6749 | OIDC Core |
Cara paling sederhana mengingatnya: OIDC = OAuth2 + "tahu siapa usernya". OIDC bukan protokol baru yang menggantikan OAuth2; ia adalah lapisan yang menambahkan identity layer di atas OAuth2 — menambahkan id_token, scope openid, dan endpoint /userinfo. Ketika Anda meminta scope openid, Authorization Server tahu Anda bukan sekadar minta akses API, tapi juga minta data identitas.
Endpoint /userinfo
Selain id_token, OIDC menyediakan endpoint /userinfo untuk mengambil profil pengguna secara terpisah. Anda memanggilnya dengan access_token:
GET /oauth2/v1/userinfo
Authorization: Bearer <access_token>
{
"sub": "00uid4BxXw6I6TV4m0g3",
"email": "hendro@mail.com",
"email_verified": true,
"name": "Hendro",
"preferred_username": "hendro"
}
Bedanya dengan id_token: id_token berisi klaim yang sudah "dikunci" oleh signature saat diterbitkan, sedangkan /userinfo memberikan data segar dari penyedia. Untuk aplikasi yang hanya perlu identitas sekali saat login, id_token sudah cukup; untuk aplikasi yang perlu memastikan data masih terbaru (misal email sudah diverifikasi), panggil /userinfo.
Klaim sub (subject) di id_token dan /userinfo selalu sama untuk pengguna yang sama — inilah "kunci utama" pengguna di sisi Anda. Jangan pernah memakai email sebagai primary key, karena email bisa berubah atau belum diverifikasi; sub justru immutable.
Discovery & JWKS
Sekarang pertanyaan kunci dari sisi backend: bagaimana API saya memverifikasi token dari Okta tanpa berbagi rahasia apa pun?
Jawabannya: karena token itu RS256 (asimetris), API Anda cukup memegang public key Okta — dan public key itu diambil otomatis lewat Discovery document.
.well-known/openid-configuration
Setiap penyedia OIDC yang serius mempublikasikan metadata di URL yang bisa ditebak:
GET https://your-tenant.okta.com/.well-known/openid-configuration
Responsnya adalah JSON berisi segala yang API Anda butuhkan:
{
"issuer": "https://your-tenant.okta.com",
"authorization_endpoint": "https://your-tenant.okta.com/oauth2/v1/authorize",
"token_endpoint": "https://your-tenant.okta.com/oauth2/v1/token",
"jwks_uri": "https://your-tenant.okta.com/oauth2/v1/keys",
"scopes_supported": ["openid", "profile", "email", "offline_access"],
"id_token_signing_alg_values_supported": ["RS256"]
}
jwks_uri & Public Key
Nilai jwks_uri menunjuk ke JWKS (JSON Web Key Set) — kumpulan public key Okta dalam format JSON:
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"kid": "abc123",
"alg": "RS256",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps...",
"e": "AQAB"
}
]
}
Untuk memverifikasi token, API Anda: mengambil kid dari header JWT → mencari public key yang cocok di JWKS → memverifikasi signature dengan key itu.
Kenapa API tidak perlu secret? Karena RS256 itu asimetris: Okta menandatangani token dengan private key (rahasia), dan verifikasi hanya butuh public key yang boleh diketahui siapa saja. Bandingkan dengan HS256 yang simetris: verifikasi butuh secret yang sama dengan penandatangan — mustahil dibagikan ke banyak service tanpa bocor. Itulah kenapa semua identity provider memakai RS256. Selengkapnya sudah dibahas di bagian HS256 vs RS256 pada artikel JWT.
Setelah signature valid, jangan lupa cek klaim iss (harus sama dengan issuer Anda) dan aud (harus nama API/resource Anda). Kalau tidak, token Okta yang sah untuk aplikasi lain bisa saja "berhasil" diverifikasi tapi seharusnya ditolak.
Implementasi di Spring Boot sebagai Resource Server
Sekarang bagian praktiknya. Spring Security memiliki dukungan resource server yang matang — dokumentasi lengkapnya ada di Spring Security OAuth2 Resource Server.
1. Dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
2. Konfigurasi issuer-uri
Di application.yml, cukup beri tahu Spring URL issuer Okta Anda:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://your-tenant.okta.com
Spring Security akan membaca .well-known/openid-configuration, menemukan jwks_uri, lalu membuat NimbusJwtDecoder otomatis. Anda bahkan tidak perlu menulis decoder sendiri.
Lazy fetching: Spring tidak men-download JWKS saat aplikasi start. Public key baru diambil saat request pertama yang butuh verifikasi, lalu di-cache (dengan refresh otomatis berdasarkan jadwal dari
Cache-Control). Ini berarti aplikasi Anda tetap bisa start walaupun Okta sedang tidak bisa dijangkau — verifikasi baru gagal ketika ada request yang benar-benar butuh token.
3. SecurityFilterChain
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasAuthority("SCOPE_admin")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(Customizer.withDefaults())
);
return http.build();
}
}
Perhatikan hasAuthority("SCOPE_admin") — Spring memetakan scope dari token (scp claim) menjadi authority dengan prefix SCOPE_. Jadi token dengan scope admin otomatis diberi authority SCOPE_admin.
4. Memetakan klaim menjadi authority
Kadang otorisasi Anda tidak berdasarkan scope, tapi klaim kustom seperti role. Di sinilah jwtAuthenticationConverter berperan:
@Bean
Converter<Jwt, AbstractAuthenticationToken> jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
authoritiesConverter.setAuthoritiesClaimName("realm_access");
authoritiesConverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
return converter;
}
Dengan converter ini, klaim realm_access (gaya Okta/Auth0) dipetakan ke ROLE_..., sehingga di controller Anda bisa memakai @PreAuthorize("hasRole('ADMIN')").
5. Membaca klaim di controller
@RestController
public class NotesController {
@GetMapping("/api/notes/me")
public Map<String, Object> myNotes(@AuthenticationPrincipal Jwt jwt) {
return Map.of(
"subject", jwt.getSubject(),
"email", jwt.getClaimAsString("email"),
"roles", jwt.getClaimAsStringList("realm_access")
);
}
}
Ini adalah contoh nyata alur kerja backend: menerima token Okta, memverifikasi via JWKS, lalu memakai klaim untuk identitas dan otorisasi — semua tanpa satu pun panggilan ke Okta di setiap request.
Keamanan & Jebakan
OAuth2 sangat kuat, tapi juga penuh jebakan kalau diimplementasikan dengan asal-asalan.
| Jebakan | Kenapa berbahaya | Solusi |
|---|---|---|
| Percaya token tanpa verifikasi signature | Attacker bisa memalsukan role/email sendiri | Selalu verifikasi signature via JWKS sebelum memakai klaim |
Tidak mengecek iss dan aud | Token aplikasi lain bisa lolos sebagai token Anda | Validasi iss = issuer Anda, aud = nama resource Anda |
| Client secret dibundel di SPA | Secret bisa dibaca siapa pun dari JavaScript | SPA harus pakai PKCE, bukan secret; secret hanya di server |
redirect_uri tidak exact match | Attacker mencuri code lewat redirect palsu | Auth Server wajib mencocokkan redirect_uri secara persis |
| Access token berumur terlalu panjang | Token bocor dipakai lama | Set access token pendek (menit), gunakan refresh token + rotasi |
| Scopes terlalu lebar | Aplikasi yang bocor punya akses ke semua data | Minta scope sesempit mungkin; revoke jika tidak dipakai |
Golden rule OAuth2: token itu "uang digital". Sama seperti uang, Anda tidak menyimpannya di tempat terbuka, Anda tidak memberikannya ke sembarang orang, dan Anda mengecek keasliannya sebelum menerimanya.
Studi Kasus: Aplikasi dengan Login Okta
Mari kita rangkai semuanya dalam satu alur utuh:
- User membuka aplikasi Anda dan mengklik "Login with Okta".
- Browser diarahkan ke Okta; user login di sana (password tidak pernah menyentuh aplikasi Anda).
- Okta mengarahkan balik ke aplikasi dengan
code(untuk SPA, disertai PKCE). - Aplikasi menukar
code→ Okta mengembalikanaccess_token,refresh_token, danid_token. - Frontend menyimpan token dan memanggil
GET /api/notes/medenganAuthorization: Bearer <access_token>. - Spring Security di backend menerima token, membaca
kid, mengambil public key darijwks_uriOkta, lalu memverifikasi signature dan cekexp,iss,aud. jwtAuthenticationConvertermemetakan klaimemaildanrealm_accessmenjadi identitas dan authority.- Endpoint merespons dengan data pengguna yang diambil dari klaim — tanpa satu pun query ke database Okta.
Perhatikan bahwa di langkah 6–8 backend Anda tidak pernah berkomunikasi dengan Okta (kecuali fetch JWKS sekali lalu di-cache). Ini membuat API Anda stateless terhadap penyedia identitas, cepat, dan mudah diuji.
Kalau Anda perlu menyimpan pengguna di database sendiri (misalnya untuk relasi notes), pola umumnya: ambil sub dari token saat request pertama masuk, lakukan upsert ke tabel users, lalu gunakan id internal Anda di seluruh aplikasi. Yang penting: otorisasi tetap berbasis klaim token, bukan sesuka hati membaca database.
Pola "trust boundary" yang benar: Authorization Server yang menentukan siapa pengguna (autentikasi) dan scope apa yang disetujui (otorisasi dasar). Resource Server hanya memverifikasi dan memetakan klaim. Jangan biarkan API Anda menjadi tempat memutuskan apakah pengguna "benar-benar" telah login — itu sudah diputuskan Okta, tugas Anda hanya menegakkan aturan akses berdasarkan klaim yang sudah diverifikasi.
Ringkasan
OAuth2 adalah kerangka otorisasi yang memisahkan kredensial dari aplikasi: Resource Owner (user) login di Authorization Server (Okta/Google/Auth0), Client (aplikasi Anda) menerima token, dan Resource Server (API Anda) memverifikasi token tanpa tahu password apa pun. Grant type yang paling relevan untuk SPA/mobile adalah authorization code + PKCE, sementara service-to-service memakai client credentials; implicit dan password sudah tidak disarankan. OIDC menambahkan lapisan autentikasi di atas OAuth2 lewat id_token, scope openid, dan endpoint /userinfo. API Anda memverifikasi token RS256 hanya dengan public key dari jwks_uri — cukup konfigurasi issuer-uri di Spring Boot, dan NimbusJwtDecoder menangani discovery, verifikasi signature, exp, iss, dan aud untuk Anda. Ingat selalu: verifikasi sebelum percaya, scope sesempit mungkin, dan redirect_uri harus exact match.
Lanjut membaca
- JWT (JSON Web Token) untuk Autentikasi — fondasi token yang dipakai OAuth2, termasuk HS256 vs RS256.
- Session vs JWT — perbandingan pendekatan autentikasi untuk memahami posisi token di ekosistem Anda.
- OAuth.net: OAuth 2.0 — ringkasan resmi dan referensi komunitas OAuth2.
- OpenID Connect — situs resmi OpenID Foundation untuk OIDC.
- RFC 6749: The OAuth 2.0 Authorization Framework — spesifikasi resmi OAuth2.
- RFC 7636: PKCE — spesifikasi PKCE untuk public clients.
- Spring Security: OAuth2 — dokumentasi resmi integrasi OAuth2 di Spring.
- Okta Developer Docs — panduan praktis OIDC, JWKS, dan resource server.