Learn Java Identity Authentication Authorization Api Platform Part 010 Token Models Jwt Opaque Introspection
title: Learn Java Identity, Authentication & Authorization for Secure Enterprise API Platform - Part 010 description: Desain token model untuk platform API Java enterprise: JWT, opaque token, introspection, reference token, revocation, audience, issuer, claims, dan trade-off validasi lokal vs validasi terpusat. series: learn-java-identity-authentication-authorization-api-platform seriesTitle: Learn Java Identity, Authentication & Authorization for Secure Enterprise API Platform order: 10 partTitle: Token Models: JWT, Opaque Token, Introspection, Reference Token tags:
- java
- identity
- authentication
- authorization
- oauth
- oidc
- jwt
- api-security date: 2026-06-28
Part 010 — Token Models: JWT, Opaque Token, Introspection, Reference Token
Target part ini: kamu bisa memilih token model yang tepat untuk enterprise API platform, bukan sekadar mengikuti default framework. Kamu harus bisa menjelaskan trade-off antara JWT, opaque token, introspection, reference token, revocation, latency, coupling, auditability, dan blast radius.
Token adalah salah satu objek paling disalahpahami di sistem modern. Banyak engineer menyebut semua token sebagai “JWT”, padahal OAuth tidak mewajibkan access token berbentuk JWT. Banyak juga yang menganggap JWT “stateless dan pasti lebih scalable”, padahal trade-off-nya sering buruk untuk revocation, entitlement freshness, tenant isolation, dan incident response.
Part ini membahas token sebagai security artifact dan distributed decision input.
Pertanyaan desain yang harus bisa kamu jawab:
- Siapa yang menerbitkan token?
- Siapa audience token?
- Apakah token bisa divalidasi lokal?
- Apakah resource server perlu introspection?
- Bagaimana token direvoke?
- Claim apa yang boleh dimasukkan?
- Berapa lama token hidup?
- Apakah token membawa entitlement atau hanya reference?
- Apakah token aman jika bocor ke log/browser/proxy?
- Apa yang terjadi saat signing key rotate atau Authorization Server down?
1. Kaufman Skill Slice
1.1 Target performa
Setelah part ini, kamu harus bisa:
- membedakan JWT access token, opaque access token, reference token, refresh token, dan ID Token;
- menjelaskan local validation vs introspection;
- memilih token model untuk internal API, public API, high-risk API, dan multi-tenant API;
- mendesain token claim minimal yang aman;
- menghindari claim bloat, stale entitlement, dan token-as-session confusion;
- menulis Spring Security resource server config untuk JWT dan opaque token;
- merancang revocation dan incident response berdasarkan token model.
1.2 Subskill latihan
| Subskill | Latihan | Output |
|---|---|---|
| Token taxonomy | Klasifikasi 10 token sample | Tidak tertukar ID/access/refresh/session |
| Claim review | Review payload JWT | Bisa menandai claim berbahaya/stale |
| Validation design | Pilih local vs introspection | Justifikasi latency/security |
| Revocation design | Simulasikan credential compromise | Tahu blast radius |
| Spring implementation | Configure JWT/opaque resource server | Bisa implementasi dua model |
| Failure analysis | AS down, JWKS stale, key rotation gagal | Runbook realistis |
2. Token Taxonomy
2.1 Access Token
Access token dipakai client untuk memanggil resource server.
GET /api/cases/CASE-123 HTTP/1.1
Host: api.example.gov
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjIwMjYtMDYifQ...
Access token menjawab:
Client ini diberi otorisasi untuk meminta akses ke resource server tertentu dalam batas tertentu.
Namun access token bukan keputusan akhir domain authorization. Resource server tetap harus memutuskan apakah request spesifik boleh dilakukan.
2.2 ID Token
ID Token dikonsumsi oleh OIDC client/RP untuk login. Jangan kirim ID Token ke resource server sebagai bearer API credential.
2.3 Refresh Token
Refresh token dipakai client untuk mendapatkan access token baru. Refresh token biasanya lebih sensitif daripada access token karena lifetime lebih panjang.
2.4 Application Session
Application session adalah state login lokal aplikasi. Bisa berupa server-side session id dalam cookie atau session token internal. Jangan menyamakan application session dengan OAuth access token.
2.5 Reference Token
Reference token adalah token yang isinya tidak bermakna bagi resource server tanpa lookup/introspection. Biasanya berupa random handle.
0.IyYxMmFiMzQtcmVmZXJlbmNlLXRva2VuLWlkCg
2.6 Opaque Token
Opaque token adalah token yang tidak perlu dipahami oleh client/resource server sebagai struktur self-contained. Resource server bertanya ke Authorization Server lewat introspection endpoint untuk mengetahui apakah token aktif dan metadata apa yang melekat.
Reference token biasanya opaque, tetapi istilahnya tidak selalu identik di semua produk.
3. JWT Access Token
JWT adalah format compact untuk membawa claims yang bisa ditandatangani atau dienkripsi. Dalam OAuth, JWT access token memungkinkan resource server melakukan validasi lokal tanpa call ke Authorization Server pada setiap request.
3.1 Struktur JWT
base64url(header).base64url(payload).base64url(signature)
Contoh header:
{
"alg": "RS256",
"typ": "at+jwt",
"kid": "2026-06-key-1"
}
Contoh payload access token:
{
"iss": "https://idp.example.gov/realms/enforcement",
"sub": "user-248289761001",
"aud": "case-api",
"client_id": "case-management-web",
"exp": 1760000000,
"iat": 1759999700,
"jti": "0c71f4ce-44b8-4d53-b230-0c8d705f4ab6",
"scope": "case:read case:update",
"tenant_id": "tenant-regulator-001",
"acr": "urn:example:aal2"
}
3.2 JWT local validation pipeline
3.3 Claim minimum untuk JWT access token
| Claim | Wajib? | Catatan |
|---|---|---|
iss | Ya | Issuer harus trusted dan exact match |
sub | Biasanya | Subject user/service; jangan selalu asumsikan human user |
aud | Ya | Harus match resource server/API |
exp | Ya | Short-lived access token |
iat | Disarankan | Untuk audit dan freshness sanity |
nbf | Opsional | Jangan gunakan terlalu kompleks tanpa kebutuhan |
jti | Disarankan | Berguna untuk audit/replay/revocation list |
client_id / azp | Disarankan | Penting untuk delegated access |
scope / scp | Umum | Coarse permission, bukan object-level authorization |
tenant_id | Kontekstual | Harus diverifikasi terhadap account/resource |
acr / amr | Kontekstual | Untuk assurance-aware decisions |
3.4 Apa yang jangan dimasukkan ke JWT
Hindari:
- data pribadi berlebihan;
- alamat lengkap, nomor identitas, data rahasia case;
- entitlement besar yang sering berubah;
- daftar ratusan resource id;
- role teknis internal yang tidak punya kontrak;
- flag admin global tanpa tenant/context;
- informasi yang jika bocor ke log menjadi insiden privacy;
- claim yang tidak bisa diverifikasi semantics-nya oleh resource server.
Rule of thumb: JWT access token adalah credential, bukan mini database user.
4. Opaque Token dan Introspection
Dalam model opaque, resource server tidak membaca isi token. Resource server memanggil Authorization Server introspection endpoint.
4.1 Contoh introspection response
{
"active": true,
"iss": "https://idp.example.gov/realms/enforcement",
"sub": "user-248289761001",
"aud": "case-api",
"client_id": "case-management-web",
"scope": "case:read case:update",
"exp": 1760000000,
"iat": 1759999700,
"jti": "opaque-token-handle-id",
"tenant_id": "tenant-regulator-001"
}
Jika token tidak valid/expired/revoked, response bisa:
{
"active": false
}
4.2 Kelebihan opaque + introspection
- revocation lebih cepat terlihat;
- token tidak membocorkan claims ke client/log;
- claim bisa lebih fresh;
- Authorization Server bisa melakukan centralized policy checks;
- cocok untuk high-risk atau regulated APIs;
- format token bisa diganti tanpa mengubah client.
4.3 Kekurangan opaque + introspection
- latency tambahan;
- dependency runtime pada Authorization Server;
- perlu cache agar performa stabil;
- AS menjadi critical path;
- failure mode lebih kompleks;
- introspection endpoint harus diamankan kuat.
5. JWT vs Opaque: Trade-Off Nyata
| Dimensi | JWT Access Token | Opaque + Introspection |
|---|---|---|
| Validasi | Lokal di resource server | Call ke AS/introspection |
| Latency | Rendah | Lebih tinggi, bisa cache |
| Revocation | Sulit sampai token expiry kecuali denylist | Lebih natural |
| Claim privacy | Payload terlihat oleh holder | Claim tersembunyi |
| AS dependency per request | Tidak, kecuali JWKS refresh | Ya, kecuali cache |
| Key rotation risk | Ada, butuh JWKS handling | Lebih terkonsentrasi di AS |
| Entitlement freshness | Potensi stale | Lebih fresh |
| Operational complexity | Key/JWKS di semua RS | Introspection scale/cache |
| API ecosystem | Baik untuk distributed microservices | Baik untuk high-control platform |
| Incident response | Blast radius sampai expiry | Bisa revoke lebih cepat |
Tidak ada model yang selalu benar. Pilihan bergantung pada constraint.
6. Decision Matrix
6.1 Pilih JWT jika
- resource server banyak dan latency sangat penting;
- access token pendek;
- revocation cepat tidak kritikal;
- claims minimal dan tidak sensitif;
- issuer/resource server dalam trust domain yang stabil;
- JWKS rotation dan monitoring siap;
- authorization final tetap dilakukan di resource server.
6.2 Pilih opaque + introspection jika
- token perlu bisa direvoke cepat;
- claim sensitif tidak boleh terlihat di token;
- entitlement harus lebih fresh;
- API high-risk/regulatory;
- Authorization Server bisa di-scale dan di-cache;
- platform menginginkan centralized control;
- token format harus disembunyikan dari client/partner.
6.3 Hybrid model
Banyak enterprise memakai kombinasi:
| Area | Model |
|---|---|
| Internal low-risk microservice | JWT short-lived |
| External partner API | Opaque/reference token |
| High-value payment/regulatory API | Sender-constrained token + introspection/high-assurance profile |
| Browser session | Server-side session cookie/BFF |
| Service-to-service inside mesh | Workload identity + short-lived token/mTLS |
7. Token as Input, Not Final Decision
Access token biasanya hanya memberikan coarse context.
subject = user-123
client = case-management-web
scope = case:update
tenant = regulator-001
assurance = aal2
Request aktual:
PATCH /cases/CASE-9001/status
Authorization: Bearer ...
Content-Type: application/json
{"status":"APPROVED"}
Domain authorization masih harus menjawab:
- apakah
CASE-9001milik tenantregulator-001? - apakah user adalah assigned investigator/supervisor?
- apakah case sedang di state yang boleh approved?
- apakah user punya conflict of interest?
- apakah action butuh AAL2/AAL3?
- apakah approval melanggar segregation of duties?
Access token tidak boleh menggantikan policy domain.
8. Spring Security Resource Server: JWT
8.1 Maven dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
8.2 Basic config
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://idp.example.gov/realms/enforcement
Spring dapat menggunakan issuer metadata untuk menemukan JWKS dan memvalidasi issuer.
8.3 Custom audience validator
Validasi audience sering terlupakan.
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder decoder = JwtDecoders.fromIssuerLocation(
"https://idp.example.gov/realms/enforcement");
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(
"https://idp.example.gov/realms/enforcement");
OAuth2TokenValidator<Jwt> audience = token -> {
if (token.getAudience().contains("case-api")) {
return OAuth2TokenValidatorResult.success();
}
OAuth2Error error = new OAuth2Error("invalid_token", "missing required audience", null);
return OAuth2TokenValidatorResult.failure(error);
};
decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(withIssuer, audience));
return decoder;
}
8.4 Authority converter
Scope conversion harus eksplisit.
@Bean
JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter scopes = new JwtGrantedAuthoritiesConverter();
scopes.setAuthorityPrefix("SCOPE_");
scopes.setAuthoritiesClaimName("scope");
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(jwt -> {
Collection<GrantedAuthority> authorities = new ArrayList<>(scopes.convert(jwt));
String tenantId = jwt.getClaimAsString("tenant_id");
if (tenantId != null) {
authorities.add(new SimpleGrantedAuthority("TENANT_CONTEXT_PRESENT"));
}
return authorities;
});
return converter;
}
Jangan mengubah semua roles token menjadi authority aplikasi tanpa policy mapping.
9. Spring Security Resource Server: Opaque Token
9.1 Config
spring:
security:
oauth2:
resourceserver:
opaque-token:
introspection-uri: https://idp.example.gov/oauth2/introspect
client-id: case-api
client-secret: ${INTROSPECTION_CLIENT_SECRET}
9.2 Security filter chain
@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health").permitAll()
.anyRequest().authenticated())
.oauth2ResourceServer(oauth -> oauth
.opaqueToken(Customizer.withDefaults()))
.build();
}
9.3 Custom introspector
Gunakan custom introspector untuk validasi audience/issuer/tenant mapping.
@Bean
OpaqueTokenIntrospector introspector() {
NimbusOpaqueTokenIntrospector delegate = new NimbusOpaqueTokenIntrospector(
"https://idp.example.gov/oauth2/introspect",
"case-api",
System.getenv("INTROSPECTION_CLIENT_SECRET"));
return token -> {
OAuth2AuthenticatedPrincipal principal = delegate.introspect(token);
String issuer = principal.getAttribute("iss");
if (!"https://idp.example.gov/realms/enforcement".equals(issuer)) {
throw new BadOpaqueTokenException("invalid issuer");
}
Object audience = principal.getAttribute("aud");
if (!containsAudience(audience, "case-api")) {
throw new BadOpaqueTokenException("invalid audience");
}
return principal;
};
}
10. Caching Introspection Results
Opaque token introspection tanpa cache bisa membebani Authorization Server.
Namun caching terlalu lama mengurangi manfaat revocation.
10.1 Cache rule
Cache TTL harus maksimum:
min(configuredMaxTtl, tokenExp - now, revocationRiskWindow)
10.2 Example wrapper
public final class CachingOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
private final OpaqueTokenIntrospector delegate;
private final Cache<String, OAuth2AuthenticatedPrincipal> cache;
private final Duration maxTtl;
public CachingOpaqueTokenIntrospector(
OpaqueTokenIntrospector delegate,
Cache<String, OAuth2AuthenticatedPrincipal> cache,
Duration maxTtl) {
this.delegate = delegate;
this.cache = cache;
this.maxTtl = maxTtl;
}
@Override
public OAuth2AuthenticatedPrincipal introspect(String token) {
String cacheKey = sha256(token);
OAuth2AuthenticatedPrincipal cached = cache.getIfPresent(cacheKey);
if (cached != null && !isExpired(cached)) {
return cached;
}
OAuth2AuthenticatedPrincipal principal = delegate.introspect(token);
Duration ttl = computeTtl(principal, maxTtl);
cache.put(cacheKey, principal);
return principal;
}
private static boolean isExpired(OAuth2AuthenticatedPrincipal principal) {
Instant exp = principal.getAttribute("exp");
return exp != null && Instant.now().isAfter(exp);
}
private static Duration computeTtl(OAuth2AuthenticatedPrincipal principal, Duration maxTtl) {
Instant exp = principal.getAttribute("exp");
if (exp == null) {
return maxTtl;
}
Duration untilExpiry = Duration.between(Instant.now(), exp);
return untilExpiry.compareTo(maxTtl) < 0 ? untilExpiry : maxTtl;
}
private static String sha256(String token) {
// Use a real SHA-256 implementation in production.
return Integer.toHexString(token.hashCode());
}
}
Catatan: contoh sha256 di atas sengaja disederhanakan. Di production, gunakan SHA-256 beneran dan jangan log token mentah.
11. Revocation Model
11.1 JWT revocation problem
JWT access token short-lived bisa divalidasi lokal. Jika token dicuri, resource server akan menerimanya sampai expired kecuali ada mekanisme tambahan.
Opsi mitigasi:
- access token sangat pendek;
- revoke refresh token agar tidak bisa mint token baru;
- maintain denylist
jtiuntuk token high-risk; - introspection untuk token tertentu;
- sender-constrained token;
- step-up untuk operasi sensitif;
- account/session version check.
11.2 Token version pattern
Tambahkan session_version atau account_token_version di token, lalu resource server cek terhadap cache/store untuk high-risk action.
{
"sub": "user-123",
"aud": "case-api",
"exp": 1760000000,
"account_version": 17
}
Saat account direvoke:
update account_security_state
set token_version = token_version + 1,
updated_at = now()
where account_id = ?;
Resource server:
public void requireCurrentAccountVersion(Jwt jwt) {
UUID accountId = UUID.fromString(jwt.getSubject());
Integer tokenVersion = jwt.getClaim("account_version");
Integer currentVersion = securityStateCache.get(accountId).tokenVersion();
if (!Objects.equals(tokenVersion, currentVersion)) {
throw new InvalidBearerTokenException("stale token");
}
}
Trade-off: ini mengurangi statelessness dan menambah dependency, tetapi berguna untuk privileged domains.
11.3 Opaque revocation
Opaque token lebih natural direvoke karena introspection bisa mengembalikan active=false.
Namun jika resource server cache introspection result, revocation tidak instan. Karena itu TTL cache harus sesuai risk profile.
12. Audience dan Issuer: Dua Claim yang Wajib Dipahami
12.1 Issuer (iss)
Issuer menjawab:
Siapa yang menerbitkan token ini?
Resource server hanya boleh menerima issuer yang trusted.
12.2 Audience (aud)
Audience menjawab:
Untuk siapa token ini dimaksudkan?
Jika case-api menerima token dengan aud=profile-api, itu audience confusion.
12.3 Multi-audience caution
Token multi-audience meningkatkan risiko. Jika satu token berlaku untuk banyak API, blast radius lebih besar. Lebih baik token spesifik per resource server, terutama untuk domain sensitif.
13. Scope, Permission, Role, Entitlement
Token sering membawa scope.
scope = "case:read case:update"
Scope adalah coarse authorization grant. Ia tidak cukup untuk object-level authorization.
| Konsep | Lokasi umum | Contoh | Catatan |
|---|---|---|---|
| Scope | Access token | case:update | Coarse API permission |
| Role | Directory/app | supervisor | Bisa terlalu kasar |
| Entitlement | App policy store | approve_case_in_region_X | Lebih domain-specific |
| Permission | Derived decision | canApprove(caseId) | Kontekstual |
| Ownership | Domain data | assigned investigator | Harus dicek dari resource |
13.1 Bad pattern
@PreAuthorize("hasAuthority('SCOPE_case:update')")
public void approveCase(UUID caseId) {
caseService.approve(caseId);
}
Ini hanya membuktikan token punya scope update, bukan user boleh approve case itu.
13.2 Better pattern
public void approveCase(UUID caseId) {
AuthenticatedActor actor = currentActor.require();
CaseRecord caseRecord = cases.requireById(caseId);
authorization.requireAllowed(actor, Action.APPROVE_CASE, caseRecord);
caseWorkflow.approve(caseRecord, actor);
}
Scope dapat menjadi precondition, tetapi bukan keputusan final.
14. Token Lifetime Design
14.1 Access token lifetime
Umumnya access token sebaiknya short-lived. Semakin panjang lifetime, semakin besar window misuse saat token bocor.
Pertimbangan:
- risk domain;
- revocation requirement;
- AS availability;
- client type;
- network trust;
- sender-constrained atau bearer;
- ability to refresh silently;
- user friction.
14.2 Refresh token lifetime
Refresh token harus diperlakukan sebagai high-value credential.
Praktik penting:
- rotation;
- reuse detection;
- bind ke client;
- storage aman;
- revoke saat logout/compromise;
- jangan berikan refresh token ke SPA kecuali platform dan mitigasi mendukung dengan benar;
- gunakan BFF untuk browser high-risk app.
14.3 Lifetime matrix
| Client | Access token | Refresh token | Catatan |
|---|---|---|---|
| Server-side web/BFF | Pendek | Bisa, server-side storage | Aman jika secret/session storage kuat |
| SPA | Pendek | Hati-hati; prefer BFF | Browser storage risk |
| Mobile app | Pendek | Bisa dengan secure storage + rotation | Device compromise tetap mungkin |
| Machine-to-machine | Pendek | Biasanya tidak perlu refresh token | Client credentials bisa mint ulang |
| High-risk API | Sangat pendek / introspected | Ketat | Step-up/sender-constrained |
15. Token Storage
15.1 Browser
Jangan simpan bearer token sensitif sembarangan di localStorage untuk high-risk apps. Risiko XSS menjadikan token mudah dicuri.
Pattern yang lebih aman untuk enterprise web:
Browser holds HttpOnly SameSite Secure session cookie.
BFF/server holds tokens.
APIs are called via BFF or with strict token handling.
15.2 Server-side
Jika server menyimpan token:
- encrypt at rest jika persistent;
- jangan log token;
- restrict memory dumps;
- rotate secrets;
- isolate per tenant/client;
- audit refresh/revocation.
15.3 Logs
Bearer token di log adalah insiden. Filter:
Authorizationheader;- query parameter
access_token; - cookie value;
- token endpoint response;
- error traces yang mencetak request.
16. Token Exchange
Token exchange memungkinkan sistem menukar token satu konteks menjadi token konteks lain. Ini relevan untuk delegated service calls.
Contoh:
Frontend token -> API A -> token exchange -> API B token
Gunakan ketika:
- downstream API perlu audience berbeda;
- perlu membatasi scope untuk downstream;
- perlu mencatat actor chain;
- perlu menghindari token forwarding liar.
Jangan terus menerus forward access token awal ke semua service. Itu memperbesar blast radius dan menciptakan audience confusion.
17. Sender-Constrained Token Preview
Bearer token berarti siapa pun yang memegang token bisa menggunakannya. Sender-constrained token mengikat token ke client/key/channel tertentu, misalnya lewat mTLS atau DPoP.
Part FAPI/high-assurance nanti akan membahas ini lebih jauh. Untuk sekarang, cukup pegang mental model:
Bearer token = possession is enough.
Sender-constrained token = possession + proof of key/channel.
Untuk API bernilai tinggi, bearer token biasa sering tidak cukup.
18. Failure Modes
18.1 JWT accepted by wrong API
Penyebab:
- audience tidak divalidasi;
- token multi-audience terlalu luas;
- resource server hanya validasi signature.
Mitigasi:
- wajib audience validator;
- token per API;
- reject unknown audience.
18.2 Stale entitlement
Token membawa role SUPERVISOR, user dicabut dari role, tetapi token masih valid.
Mitigasi:
- token pendek;
- entitlement tidak dimasukkan ke token untuk high-risk decision;
- introspection;
- account version check;
- step-up real-time check.
18.3 Token bloat
JWT terlalu besar karena group/role/resource list. Akibatnya header HTTP besar, latency naik, proxy/gateway error.
Mitigasi:
- minimal claims;
- reference entitlement;
- local policy store;
- reduce group claims;
- use opaque token for complex context.
18.4 JWKS rotation outage
Resource server gagal memvalidasi token baru karena JWKS cache stale atau AS metadata unreachable.
Mitigasi:
- overlap old/new keys;
- cache JWKS with sane refresh;
- alert on unknown
kidspike; - pre-publish keys;
- runbook rollback.
18.5 Introspection AS outage
Opaque-token resource server tidak bisa introspect token.
Pilihan fail mode:
- fail closed untuk high-risk API;
- limited cached allow untuk low-risk read API;
- degrade with strict TTL;
- circuit breaker + alert.
Jangan fail open tanpa risk acceptance eksplisit.
19. Testing Strategy
19.1 JWT negative tests
Wajib test:
- expired token;
- wrong issuer;
- wrong audience;
- missing audience;
- invalid signature;
- unsupported algorithm;
- unknown
kid; - token with wrong
typ; - token from tenant A used for tenant B;
- token with scope but no object permission.
19.2 Opaque introspection tests
Wajib test:
active=falserejected;- wrong audience rejected;
- wrong issuer rejected;
- missing subject rejected;
- introspection timeout behavior;
- cache TTL does not exceed token expiry;
- revoked token not accepted beyond risk TTL;
- introspection credentials not exposed.
19.3 Contract test with Authorization Server
Contract:
Given a token issued for case-api
When resource server validates it
Then issuer, audience, expiry, subject, client, scope, tenant_id are interpreted consistently.
Jangan hanya test happy path dengan mock JWT sembarang. Mock yang tidak realistis sering menyembunyikan claim mismatch production.
20. Observability dan Audit
Log security event tanpa membocorkan token.
20.1 Event fields
{
"event_type": "api_token_accepted",
"request_id": "req-123",
"issuer": "https://idp.example.gov/realms/enforcement",
"subject_hash": "sha256:...",
"client_id": "case-management-web",
"audience": "case-api",
"tenant_id": "tenant-regulator-001",
"jti_hash": "sha256:...",
"token_model": "jwt",
"scope": ["case:read", "case:update"],
"decision": "accepted"
}
Jangan log:
- raw access token;
- raw refresh token;
- raw ID Token;
- full Authorization header;
- unnecessary PII claim.
20.2 Metrics
Pantau:
- invalid token count by reason;
- unknown issuer;
- audience mismatch;
- expired token;
- unknown
kid; - introspection latency;
- introspection error rate;
- JWKS refresh error;
- token replay suspicion;
- revoked token use attempt.
21. Operational Checklist
Sebelum production:
- resource server memvalidasi issuer;
- resource server memvalidasi audience;
- access token lifetime terdokumentasi;
- token claim minimal;
- sensitive claim tidak dimasukkan;
- token tidak masuk log;
- JWT key rotation diuji;
- opaque introspection cache TTL aman;
- AS outage behavior diputuskan;
- revocation model jelas;
- scope tidak menggantikan object authorization;
- tenant claim diverifikasi terhadap resource;
- incident response untuk token leak tersedia;
- monitoring invalid token aktif.
22. Practice Drill
Drill 1 — Pilih token model
Untuk setiap case, pilih JWT atau opaque/introspection dan jelaskan alasannya:
- Internal catalog read API dengan traffic sangat tinggi.
- API approval enforcement untuk regulatory case closure.
- Partner API dengan data sensitif dan revocation requirement ketat.
- SPA dashboard untuk internal staff.
- Service-to-service API dalam mesh dengan mTLS.
Drill 2 — Review payload JWT
Payload:
{
"iss": "https://idp.example.com",
"sub": "123",
"aud": ["case-api", "payment-api", "profile-api"],
"exp": 1999999999,
"email": "ana@example.gov",
"groups": ["admin", "finance", "case-supervisor", "all-regions"],
"national_id": "SENSITIVE-ID",
"scope": "*"
}
Temukan minimal 8 masalah.
Drill 3 — Failure mode table
Buat table untuk sistemmu sendiri:
| Failure | Current behavior | Desired behavior | Test exists? | Runbook exists? |
|---|---|---|---|---|
| JWKS unavailable | ? | ? | ? | ? |
| AS introspection timeout | ? | ? | ? | ? |
| Token leak | ? | ? | ? | ? |
| User role revoked | ? | ? | ? | ? |
23. Ringkasan
Token model adalah pilihan arsitektural, bukan detail library. JWT memberi validasi lokal dan latency rendah, tetapi sulit untuk revocation cepat dan mudah membawa stale/sensitive claims. Opaque token dengan introspection memberi kontrol dan revocation lebih kuat, tetapi menambah latency dan dependency runtime pada Authorization Server.
Mental model paling penting:
A token is not authorization. A token is evidence used by a resource server to build an authentication/authorization context.
The final permission decision still belongs to the protected resource boundary.
Jika kamu memahami trade-off ini, kamu tidak akan memilih JWT hanya karena populer atau opaque token hanya karena terlihat lebih aman. Kamu akan memilih berdasarkan risk, latency, freshness, revocation, privacy, dan operability.
24. Referensi Utama
- IETF RFC 7519 — JSON Web Token.
- IETF RFC 7662 — OAuth 2.0 Token Introspection.
- IETF RFC 9068 — JWT Profile for OAuth 2.0 Access Tokens.
- IETF RFC 9700 — OAuth 2.0 Security Best Current Practice.
- OpenID Connect Core 1.0 — ID Token semantics.
- Spring Security Reference — OAuth2 Resource Server JWT.
- Spring Security Reference — OAuth2 Resource Server Opaque Token.
You just completed lesson 10 in build core. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.