Tampilan
Kenapa penting
Di bab sebelumnya kamu memvalidasi JWT. Pertanyaan berikutnya: siapa yang menerbitkan token itu, dan bagaimana aplikasi mobile, SPA, atau layanan lain mendapatkannya dengan aman? Membangun login, reset password, MFA, dan penerbitan token sendiri adalah pekerjaan besar yang mudah salah. Di industri, tugas itu diserahkan ke authorization server yang berbicara protokol standar OAuth2 dan OpenID Connect (OIDC) — Keycloak, Auth0, Okta, Azure Entra ID, atau Google. API kamu cukup menjadi resource server yang memercayai token dari sana. Memahami peran, flow, dan perbedaan scope/role membuatmu bisa mengintegrasikan login perusahaan, "Login dengan Google", dan komunikasi antarlayanan tanpa menebak-nebak.
Konsep inti
Empat peran OAuth2
OAuth2 adalah protokol delegasi otorisasi: pengguna mengizinkan sebuah aplikasi mengakses resource atas namanya tanpa memberikan password kepada aplikasi itu.
| Peran | Di sistem toko online |
|---|---|
| Resource owner | pelanggan pemilik data pesanan |
| Client | aplikasi mobile/SPA "Tokoku" |
| Authorization server | Keycloak — login, consent, menerbitkan token |
| Resource server | API pesanan Spring Boot yang memvalidasi token |
Client dibedakan menjadi confidential client (bisa menyimpan secret dengan aman, misalnya backend) dan public client (tidak bisa, misalnya SPA dan aplikasi mobile — kodenya bisa dibongkar).
Authorization Code + PKCE
Flow untuk aplikasi yang dipakai manusia. Alurnya:
text
1. Client membuat code_verifier acak, menghitung code_challenge = BASE64URL(SHA-256(verifier))
2. Client mengarahkan browser ke /authorize?response_type=code&client_id=...&redirect_uri=...
&scope=openid orders:read&state=...&code_challenge=...&code_challenge_method=S256
3. Pengguna login di Keycloak (password, MFA) — client tidak pernah melihat password
4. Keycloak redirect ke redirect_uri?code=...&state=...
5. Client POST /token dengan code + code_verifier
6. Keycloak mencocokkan SHA-256(verifier) dengan challenge, lalu mengembalikan access token (+ refresh token, ID token)
7. Client memanggil API dengan Authorization: Bearer <access token>PKCE (Proof Key for Code Exchange, RFC 7636) melindungi langkah 4–5: penyerang yang mencegat code tidak bisa menukarnya karena tidak tahu code_verifier. Parameter state mencegah CSRF pada redirect. Kini PKCE dianjurkan untuk semua client, termasuk yang confidential. Perhitungan challenge-nya bisa kamu coba dengan Java murni:
java
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Base64;
public class PkceDemo {
static final Base64.Encoder B64URL = Base64.getUrlEncoder().withoutPadding();
static String newVerifier() {
byte[] bytes = new byte[32];
new SecureRandom().nextBytes(bytes);
return B64URL.encodeToString(bytes); // 43 karakter
}
static String challengeS256(String verifier) throws Exception {
byte[] hash = MessageDigest.getInstance("SHA-256")
.digest(verifier.getBytes(StandardCharsets.US_ASCII));
return B64URL.encodeToString(hash);
}
public static void main(String[] args) throws Exception {
String random = newVerifier();
System.out.println("Panjang verifier acak: " + random.length());
// Vektor uji dari RFC 7636 Appendix B
String verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
System.out.println("code_challenge: " + challengeS256(verifier));
}
}Output:
text
Panjang verifier acak: 43
code_challenge: E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cMHasilnya sama dengan vektor uji resmi RFC 7636. Di proyek nyata, library client (AppAuth untuk mobile, library OIDC untuk SPA, atau Spring Security OAuth2 Client untuk backend) yang membuatnya untukmu.
Client Credentials
Flow untuk mesin-ke-mesin tanpa pengguna: layanan menukar client ID + secret miliknya dengan access token.
bash
curl -s -X POST http://localhost:8180/realms/tokoku/protocol/openid-connect/token \
-d grant_type=client_credentials \
-d client_id=billing-service \
-d client_secret="$BILLING_CLIENT_SECRET"Responsnya JSON berisi access_token, expires_in, dan token_type (Bearer). Tidak ada refresh token — cukup minta token baru saat yang lama hampir kedaluwarsa. Flow lama Implicit dan Resource Owner Password sudah dianggap usang dan jangan dipakai untuk sistem baru.
OpenID Connect dan ID token
OAuth2 murni hanya mengurus otorisasi ("boleh akses apa"), bukan autentikasi ("siapa kamu"). OIDC adalah lapisan di atas OAuth2 yang menambahkan identitas: client meminta scope openid, dan authorization server mengembalikan ID token — JWT berisi sub, name, email, waktu login, dengan aud = client ID. OIDC juga menstandarkan endpoint discovery /.well-known/openid-configuration (berisi URL token, JWKS, dsb) dan endpoint userinfo.
| Token | Untuk siapa | Dipakai untuk |
|---|---|---|
| ID token | client | mengetahui siapa yang login |
| Access token | resource server | memanggil API |
| Refresh token | authorization server | meminta access token baru |
Aturan emas: jangan kirim ID token ke API. API hanya menerima access token.
Scope vs role
- Scope membatasi apa yang boleh dilakukan client atas nama pengguna — "aplikasi ini boleh membaca pesananmu" (
orders:read). Scope diminta client dan disetujui pengguna. - Role menyatakan apa yang boleh dilakukan pengguna itu sendiri —
ADMIN,CUSTOMER,WAREHOUSE.
Keduanya bisa digabung: endpoint pengembalian dana mensyaratkan scope payments:refund dan role FINANCE. Spring memetakan scope menjadi authority SCOPE_orders:read; role biasanya dipetakan menjadi ROLE_FINANCE.
Keycloak via Docker
Keycloak adalah authorization server open source yang populer untuk pengembangan lokal dan production. Potongan compose.yaml untuk development:
yaml
# Potongan compose.yaml — hanya untuk development
services:
keycloak:
image: quay.io/keycloak/keycloak:26.8
command: start-dev --import-realm
environment:
KC_BOOTSTRAP_ADMIN_USERNAME: admin
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
ports:
- "8180:8080"
volumes:
- ./keycloak:/opt/keycloak/data/importstart-dev memakai database H2 bawaan dan HTTP tanpa TLS — jangan dipakai di production (gunakan start dengan PostgreSQL dan HTTPS). Setelah docker compose up -d, buka http://localhost:8180, buat realm tokoku, client orders-spa (public, Standard flow + PKCE S256), client billing-service (confidential, Service accounts), dan realm role ADMIN/CUSTOMER. Ekspor realm ke folder ./keycloak agar setup bisa diulang lewat --import-realm.
Resource server dengan issuer-uri
Dengan starter spring-boot-starter-security-oauth2-resource-server dari bab sebelumnya, cukup tulis issuer:
yaml
# Potongan application.yml
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:8180/realms/tokoku
audiences: orders-apiSpring Boot mengambil dokumen discovery dari issuer itu, menemukan jwks_uri, mengunduh public key, dan memasang validasi signature, exp/nbf, serta iss secara otomatis. Key rotation di Keycloak juga ditangani karena JWKS diambil ulang saat muncul kid baru. Catatan: access token Keycloak secara default tidak memuat aud untuk API kamu — tambahkan audience mapper pada client scope agar claim aud berisi orders-api.
Memetakan role Keycloak ke authority Spring
Payload access token Keycloak kira-kira seperti ini:
json
{
"iss": "http://localhost:8180/realms/tokoku",
"sub": "8f14e45f-ceea-4c1e-9d3a-2b7c1f0a9e11",
"aud": "orders-api",
"scope": "openid orders:read",
"preferred_username": "rina",
"realm_access": { "roles": ["CUSTOMER", "default-roles-tokoku"] },
"resource_access": { "orders-api": { "roles": ["refund-approver"] } }
}Converter default hanya membaca scope. Untuk role, buat JwtAuthenticationConverter sendiri:
java
// Potongan — Spring Security 7
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.GET, "/api/orders/**").hasAuthority("SCOPE_orders:read")
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.oauth2ResourceServer(o -> o.jwt(jwt -> jwt.jwtAuthenticationConverter(keycloakConverter())));
return http.build();
}
private JwtAuthenticationConverter keycloakConverter() {
JwtGrantedAuthoritiesConverter scopes = new JwtGrantedAuthoritiesConverter(); // SCOPE_...
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(jwt -> {
Collection<GrantedAuthority> authorities = new ArrayList<>(scopes.convert(jwt));
Map<String, Object> realmAccess = jwt.getClaimAsMap("realm_access");
if (realmAccess != null && realmAccess.get("roles") instanceof Collection<?> roles) {
for (Object role : roles) {
authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
}
}
return authorities;
});
converter.setPrincipalClaimName("preferred_username");
return converter;
}
}Perhatikan pattern matching instanceof Collection<?> roles — claim JSON tidak bertipe kuat, jadi periksa tipenya sebelum dipakai. hasRole("ADMIN") mencari authority ROLE_ADMIN, sesuai prefix yang ditambahkan converter.
Contoh di dunia backend
Layanan billing memanggil API inventory setiap malam memakai Client Credentials. Spring Security menyediakan OAuth2 Client yang mengurus pengambilan dan pembaruan token secara otomatis (starter spring-boot-starter-security-oauth2-client):
yaml
# Potongan application.yml layanan billing
spring:
security:
oauth2:
client:
registration:
inventory:
provider: keycloak
client-id: billing-service
client-secret: ${BILLING_CLIENT_SECRET}
authorization-grant-type: client_credentials
scope: inventory:read
provider:
keycloak:
issuer-uri: http://localhost:8180/realms/tokokujava
// Potongan — RestClient yang otomatis menambahkan bearer token
@Configuration
class InventoryClientConfig {
// Job malam berjalan di luar HTTP request, jadi pakai manager berbasis service (default: client_credentials)
@Bean
OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository registrations,
OAuth2AuthorizedClientService clientService) {
return new AuthorizedClientServiceOAuth2AuthorizedClientManager(registrations, clientService);
}
@Bean
RestClient inventoryRestClient(RestClient.Builder builder, OAuth2AuthorizedClientManager manager) {
OAuth2ClientHttpRequestInterceptor interceptor = new OAuth2ClientHttpRequestInterceptor(manager);
return builder
.baseUrl("http://inventory:8080")
.requestInterceptor(interceptor)
.defaultRequest(r -> r.attributes(clientRegistrationId("inventory")))
.build();
}
}clientRegistrationId adalah static import dari org.springframework.security.oauth2.client.web.ClientAttributes. Manager default (DefaultOAuth2AuthorizedClientManager) bergantung pada HTTP request yang sedang berjalan, sehingga untuk job terjadwal kita memakai AuthorizedClientServiceOAuth2AuthorizedClientManager. Interceptor meminta token ke Keycloak saat pertama dipakai, menyimpannya, dan meminta ulang ketika kedaluwarsa. Di sisi inventory, resource server memeriksa authority SCOPE_inventory:read. Tidak ada password pengguna yang terlibat, dan secret billing-service disuntikkan dari environment.
Pertanyaan interview
1. Apa beda OAuth2 dan OpenID Connect?
OAuth2 adalah protokol delegasi otorisasi: client mendapat access token untuk memanggil API atas nama pengguna. OAuth2 tidak mendefinisikan cara mengetahui identitas pengguna. OIDC menambahkan lapisan autentikasi di atasnya: scope openid, ID token berformat JWT, endpoint userinfo, dan discovery. Untuk fitur "login", yang dipakai adalah OIDC.
2. Kenapa Authorization Code butuh PKCE, dan kenapa Implicit flow ditinggalkan?
Public client tidak bisa menyimpan secret, sehingga kode otorisasi yang dicegat (misalnya lewat aplikasi jahat yang mendaftarkan redirect URI yang sama) bisa ditukar penyerang. PKCE mengikat kode ke code_verifier yang hanya diketahui client asli. Implicit flow mengirim token langsung lewat URL fragment, mudah bocor lewat riwayat browser dan log, dan tidak mendukung refresh token yang aman.
3. Apa beda scope dan role?
Scope membatasi hak client atas nama pengguna dan biasanya disetujui pengguna saat consent, misalnya orders:read. Role adalah hak pengguna di sistem, misalnya ADMIN. Token bisa membawa keduanya; otorisasi akhir sering mensyaratkan kombinasi keduanya.
Jebakan umum
- Mengirim ID token ke API sebagai bearer token — API seharusnya hanya menerima access token.
- Issuer tidak cocok karena perbedaan host/port (
localhost:8180vskeycloak:8080) di lingkungan Docker — samakan hostname publik Keycloak. - Lupa audience mapper, lalu mematikan validasi
audagar request lolos — ini membuka celah token lintas aplikasi. - Menanam client secret di SPA atau aplikasi mobile — public client harus memakai PKCE tanpa secret.
- Memakai
start-devdengan akun adminadmin/admindi production — gunakan modestart, database PostgreSQL, HTTPS, dan kredensial dari secret manager. - Mengira
hasRoleotomatis membaca role Keycloak — tanpa converter kustom, yang ada hanya authoritySCOPE_....
Ringkasan
- OAuth2 punya empat peran: resource owner, client, authorization server, resource server.
- Aplikasi pengguna memakai Authorization Code + PKCE; mesin-ke-mesin memakai Client Credentials; Implicit dan Password flow sudah usang.
- OIDC menambahkan identitas lewat ID token untuk client; API hanya menerima access token.
- Scope = hak client atas nama pengguna, role = hak pengguna; Spring memetakan ke
SCOPE_...danROLE_.... - Resource server cukup
issuer-uri(+audiences); role Keycloak dipetakan lewatJwtAuthenticationConverter.