Series MapLesson 10 / 35
Build CoreOrdered learning track

Learn Java Identity Authentication Authorization Api Platform Part 010 Token Models Jwt Opaque Introspection

16 min read3090 words
PrevNext
Lesson 1035 lesson track0719 Build Core

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:

  1. Siapa yang menerbitkan token?
  2. Siapa audience token?
  3. Apakah token bisa divalidasi lokal?
  4. Apakah resource server perlu introspection?
  5. Bagaimana token direvoke?
  6. Claim apa yang boleh dimasukkan?
  7. Berapa lama token hidup?
  8. Apakah token membawa entitlement atau hanya reference?
  9. Apakah token aman jika bocor ke log/browser/proxy?
  10. 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

SubskillLatihanOutput
Token taxonomyKlasifikasi 10 token sampleTidak tertukar ID/access/refresh/session
Claim reviewReview payload JWTBisa menandai claim berbahaya/stale
Validation designPilih local vs introspectionJustifikasi latency/security
Revocation designSimulasikan credential compromiseTahu blast radius
Spring implementationConfigure JWT/opaque resource serverBisa implementasi dua model
Failure analysisAS down, JWKS stale, key rotation gagalRunbook 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

ClaimWajib?Catatan
issYaIssuer harus trusted dan exact match
subBiasanyaSubject user/service; jangan selalu asumsikan human user
audYaHarus match resource server/API
expYaShort-lived access token
iatDisarankanUntuk audit dan freshness sanity
nbfOpsionalJangan gunakan terlalu kompleks tanpa kebutuhan
jtiDisarankanBerguna untuk audit/replay/revocation list
client_id / azpDisarankanPenting untuk delegated access
scope / scpUmumCoarse permission, bukan object-level authorization
tenant_idKontekstualHarus diverifikasi terhadap account/resource
acr / amrKontekstualUntuk 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

DimensiJWT Access TokenOpaque + Introspection
ValidasiLokal di resource serverCall ke AS/introspection
LatencyRendahLebih tinggi, bisa cache
RevocationSulit sampai token expiry kecuali denylistLebih natural
Claim privacyPayload terlihat oleh holderClaim tersembunyi
AS dependency per requestTidak, kecuali JWKS refreshYa, kecuali cache
Key rotation riskAda, butuh JWKS handlingLebih terkonsentrasi di AS
Entitlement freshnessPotensi staleLebih fresh
Operational complexityKey/JWKS di semua RSIntrospection scale/cache
API ecosystemBaik untuk distributed microservicesBaik untuk high-control platform
Incident responseBlast radius sampai expiryBisa 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:

AreaModel
Internal low-risk microserviceJWT short-lived
External partner APIOpaque/reference token
High-value payment/regulatory APISender-constrained token + introspection/high-assurance profile
Browser sessionServer-side session cookie/BFF
Service-to-service inside meshWorkload 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-9001 milik tenant regulator-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 jti untuk 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.

KonsepLokasi umumContohCatatan
ScopeAccess tokencase:updateCoarse API permission
RoleDirectory/appsupervisorBisa terlalu kasar
EntitlementApp policy storeapprove_case_in_region_XLebih domain-specific
PermissionDerived decisioncanApprove(caseId)Kontekstual
OwnershipDomain dataassigned investigatorHarus 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

ClientAccess tokenRefresh tokenCatatan
Server-side web/BFFPendekBisa, server-side storageAman jika secret/session storage kuat
SPAPendekHati-hati; prefer BFFBrowser storage risk
Mobile appPendekBisa dengan secure storage + rotationDevice compromise tetap mungkin
Machine-to-machinePendekBiasanya tidak perlu refresh tokenClient credentials bisa mint ulang
High-risk APISangat pendek / introspectedKetatStep-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:

  • Authorization header;
  • 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 kid spike;
  • 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=false rejected;
  • 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:

  1. Internal catalog read API dengan traffic sangat tinggi.
  2. API approval enforcement untuk regulatory case closure.
  3. Partner API dengan data sensitif dan revocation requirement ketat.
  4. SPA dashboard untuk internal staff.
  5. 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:

FailureCurrent behaviorDesired behaviorTest 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.
Lesson Recap

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.