Series MapLesson 17 / 35
Build CoreOrdered learning track

Learn Java Security Cryptography Integrity Part 017 Oauth2 Oidc Token Security And Federation

20 min read3935 words
PrevNext
Lesson 1735 lesson track0719 Build Core

title: Learn Java Security, Cryptography and Integrity - Part 017 description: OAuth2, OpenID Connect, token security, JWT validation, JWKS rotation, refresh-token safety, sender-constrained tokens, and federation failure modes for Java systems. series: learn-java-security-cryptography-integrity seriesTitle: Learn Java Security, Cryptography and Integrity order: 17 partTitle: OAuth2, OIDC, Token Security & Federation tags:

  • java
  • security
  • oauth2
  • oidc
  • jwt
  • federation
  • token-security
  • integrity date: 2026-06-30

Part 017 — OAuth2, OIDC, Token Security & Federation

Target: setelah part ini, kamu mampu mendesain dan mereview integrasi OAuth2/OIDC pada aplikasi Java production: memilih flow yang benar, memvalidasi token secara ketat, mengelola JWKS/key rotation, mencegah replay/token substitution/confused deputy, dan membangun federation boundary yang eksplisit.

OAuth2/OIDC sering gagal bukan karena engineer tidak bisa membaca dokumentasi, tetapi karena boundary-nya kabur:

OAuth2 menjawab: apakah client diberi otorisasi untuk mengakses resource atas nama subject atau dirinya sendiri?
OIDC menjawab: apakah client dapat memverifikasi identity subject berdasarkan authentication di authorization server?

Keduanya bukan sinonim.

Kesalahan production yang umum:

  • memakai OAuth2 seolah-olah itu authentication protocol tanpa OIDC;
  • memakai ID token untuk authorize API;
  • menerima JWT hanya karena signature valid, tanpa mengecek issuer, audience, subject, token type, expiry, dan claim semantics;
  • mendukung flow lama seperti implicit flow untuk use case modern;
  • memperlakukan JWKS endpoint sebagai trust anchor dinamis tanpa issuer pinning;
  • mempercayai claim dari token yang diterbitkan untuk client/API lain;
  • mencampur token dari tenant/issuer berbeda dalam sistem multi-tenant;
  • menyimpan access token di browser storage yang mudah dicuri script;
  • melakukan account linking berdasarkan email tanpa proof yang cukup.

Referensi utama:


1. Kaufman Deconstruction: Skill Map

OAuth/OIDC skill bukan “bisa konfigurasi dependency”. Skill-nya terdiri dari kemampuan-kemampuan kecil yang bisa dilatih dan divalidasi.

CapabilityPertanyaan korektifOutput engineering
Protocol distinctionIni OAuth2, OIDC, SAML, session, atau API key?Terminologi benar.
Actor mappingSiapa resource owner, client, authorization server, resource server?Trust boundary diagram.
Flow selectionFlow apa yang sesuai untuk web app, SPA, mobile, service-to-service?Flow decision record.
Redirect safetyApakah redirect URI exact match dan state dipakai?Authorization request validation.
Token semanticsToken ini ID token, access token, refresh token, atau authorization code?Token handling rules.
JWT validationClaim apa yang wajib diverifikasi?Token verifier.
JWKS trustDari mana key diambil dan kapan boleh dipercaya?Issuer-pinned JWKS cache.
Scope/claim mappingScope/claim mana yang menjadi authority internal?Authority mapping policy.
Replay defenseApakah token bearer atau sender-constrained?DPoP/mTLS/short TTL design.
Refresh safetyBagaimana refresh token disimpan, diputar, dicabut?Refresh token lifecycle.
Federation trustIssuer/tenant mana yang dipercaya?Federation registry.
Failure reviewBagaimana mendeteksi token substitution, mix-up, confused deputy?Abuse test suite.

Core invariant:

Token hanya valid untuk konteks yang secara eksplisit cocok: issuer, audience, subject, client, token type, time window, signing key, algorithm, binding, scope, tenant, dan resource. Satu mismatch harus menghasilkan deny.


2. Mental Model: OAuth2 vs OIDC

OAuth2 adalah delegated authorization framework. OAuth2 tidak mendefinisikan siapa user secara lengkap; ia mendefinisikan bagaimana client memperoleh token untuk mengakses resource server.

OIDC adalah identity layer di atas OAuth2. OIDC menambahkan ID token, UserInfo endpoint, discovery metadata, dan claim semantics untuk authentication di client.

OAuth vocabulary:

TermMakna engineeringJangan disamakan dengan
Resource ownerSubject yang memberi delegasi. Biasanya user.System owner.
ClientAplikasi yang meminta token.Browser saja.
Authorization serverKomponen yang menerbitkan token.Resource server.
Resource serverAPI yang menerima access token.Login server.
Access tokenBukti akses ke resource/API tertentu.Identity proof untuk login client.
ID tokenBukti authentication subject untuk client tertentu.API authorization token.
Refresh tokenCredential untuk mendapatkan access token baru.Session ID biasa.
Authorization codeOne-time intermediate credential.Token untuk API.

OIDC vocabulary:

TermMakna engineering
issIssuer yang menerbitkan token. Harus di-pin ke trusted issuer.
subStable identifier subject pada issuer tertentu. Jangan global tanpa issuer.
audAudience yang dituju. Client ID untuk ID token; API/resource untuk access token.
azpAuthorized party, penting ketika audience lebih dari satu.
nonceBinding antara auth request dan ID token untuk replay defense.
auth_timeKapan user authenticated. Berguna untuk reauthentication.
acr / amrContext/method authentication, misalnya MFA/passkey class.

3. Boundary yang Harus Jelas

OAuth/OIDC punya banyak boundary. Security bug sering muncul saat dua boundary dicampur.

Boundary penting:

BoundaryRisikoGuardrail
Browser → clientXSS, CSRF, token theftBFF/session cookie, CSP, no access token in localStorage.
Browser → authorization serverredirect manipulation, state lossExact redirect URI, state, PKCE, no open redirect.
Client → token endpointcode interception, client impersonationPKCE, client authentication, TLS, PAR for high-risk.
Client → APIbearer token replayTLS, short TTL, DPoP/mTLS for high assurance.
API → JWKSkey confusion, malicious key URLIssuer-pinned JWKS URI, cache, alg allowlist.
API → authorization policyscope overtrustMap external claims to internal policy carefully.
Federation → account linkingaccount takeoverLink by issuer+sub, not email alone.

4. Flow Selection

Modern baseline untuk interactive login adalah:

Authorization Code Flow + PKCE

PKCE awalnya dibuat untuk public clients, tetapi sekarang menjadi baseline luas karena melindungi authorization code dari interception.

4.2 Flow decision table

Use caseRecommended flowCatatan
Server-rendered web appAuthorization Code + PKCE + confidential client authToken disimpan server-side; browser hanya punya session cookie.
SPA murniAuthorization Code + PKCEHindari long-lived token di browser. BFF sering lebih aman.
Mobile/native appAuthorization Code + PKCE via system browserJangan embedded WebView untuk auth.
Service-to-serviceClient CredentialsSubject adalah client/service, bukan human user.
CLI/device-limitedDevice Authorization GrantUser verifikasi di browser lain.
Legacy first-party password form to tokenAvoid Resource Owner Password CredentialsBiasanya anti-pattern; gunakan hosted login/authorization code.
Silent browser token renewalAvoid implicit-style hidden iframe relianceBrowser privacy controls membuatnya rapuh dan sering insecure.

4.3 Authorization Code + PKCE sequence

Security invariants:

  • state binds response to browser session and mitigates CSRF/mix-up class issues.
  • nonce binds ID token to authentication request.
  • code_verifier proves the token request came from the party that initiated the auth request.
  • redirect_uri must be exact-match, not wildcard-friendly.
  • authorization code is one-time use and short-lived.
  • access token is validated by resource server for API-specific context.
  • ID token is consumed by client, not by arbitrary resource servers.

5. Token Types and Handling Rules

TokenReceiverStorageValidationCommon misuse
Authorization codeClient backend/token endpointTransient onlyExchanged once with PKCE/client authTreating it as API token.
ID tokenOIDC clientServer-side or short browser handling depending architectureOIDC validation: iss, aud, exp, nonce, signatureSending to API as bearer auth.
Access tokenResource server/APIPrefer server-side/BFF; if browser, short-lived and carefully isolatedAPI validation: iss, aud, exp, scope, token type, signature/introspectionAccepting token for wrong audience.
Refresh tokenClient/token endpointSecure server-side or OS secure storageRotation/reuse detection/revocationStoring long-lived refresh token in browser localStorage.
Session cookieClient appBrowser cookieServer-side session validationConfusing with OAuth token.

Rule of thumb:

ID token proves login to the client.
Access token authorizes calls to the resource server.
Refresh token is a high-value credential.
Authorization code is a temporary proof in the flow.

6. JWT Validation: What “Signature Valid” Does Not Prove

Signature validity only proves that the JWT bytes were signed by a key corresponding to some verification key. It does not prove the token is meant for your API, current, accepted for this action, or issued by a trusted authority.

Minimum validation set for JWT access tokens:

ValidationWhy it matters
Parse safelyReject malformed, huge, nested, unsupported tokens.
Algorithm allowlistPrevent algorithm confusion and none acceptance.
Key selection from trusted issuerPrevent attacker-controlled jku, x5u, kid confusion.
Signature verificationEnsure token integrity.
iss exact matchPrevent accepting tokens from untrusted issuer.
aud contains this API/resourcePrevent confused deputy and token substitution.
exp not expiredPrevent long-lived replay.
nbf not in futurePrevent early-use bugs.
iat reasonableDetect old or suspiciously future tokens.
token type / typ / profileAvoid accepting ID token as access token.
sub present where neededIdentify subject.
client_id / azp where applicableBind delegation to expected client class.
scope/claim mappingEnforce least privilege.
tenant claim consistencyPrevent cross-tenant token reuse.
PoP binding if usedVerify DPoP/mTLS confirmation claim.

6.1 Access token validation pseudocode

public final class AccessTokenPolicy {
    private final URI expectedIssuer;
    private final String expectedAudience;
    private final Set<String> allowedAlgorithms;
    private final Duration maxClockSkew;

    public AccessTokenClaims validate(Jwt jwt) {
        requireAllowedAlgorithm(jwt.header().algorithm());
        requireIssuer(jwt.claim("iss"), expectedIssuer);
        requireAudience(jwt.claimAsList("aud"), expectedAudience);
        requireNotExpired(jwt.instantClaim("exp"), maxClockSkew);
        requireNotBefore(jwt.optionalInstantClaim("nbf"), maxClockSkew);
        requireTokenUse(jwt); // e.g. access-token profile, not ID token
        requireTenantConsistency(jwt);
        requireSubjectOrClientIdentity(jwt);
        return mapToInternalClaims(jwt);
    }
}

The important part is not the class shape. The important part is the invariant:

A token that is valid for someone else, somewhere else, or some other action must be rejected.

6.2 ID token validation checklist

For OIDC login, the client validates ID token differently from API access-token validation.

Claim/checkMeaning
issMust match configured OIDC issuer.
audMust contain client ID.
azpRequired/important when multiple audiences exist; must match client.
expMust be current.
iatMust be reasonable.
nonceMust match nonce stored for auth request.
signatureMust verify under issuer key.
auth_timeRequired when max age/reauth was requested.
acr/amrUseful for MFA/passkey-sensitive flows.
subStable subject identifier within issuer.

Never use email as the primary account key unless the issuer contract and verification state justify it. Prefer:

external_identity_key = issuer + subject

Email can change, be recycled, be unverified, or collide across issuers.


7. JWKS, Key Rotation, and Key Confusion

JWKS solves key distribution, not trust by itself.

Secure JWKS rules:

RuleReason
Pin issuer configurationToken header must not decide who is trusted.
Pin JWKS URI via issuer metadata/configDo not follow attacker-controlled jku/x5u.
Allowlist algorithmsPrevent alg confusion.
Cache with expiryAvoid network call per request and survive transient outage.
Refresh on unknown kid carefullySupport rotation without opening DoS.
Reject duplicate/ambiguous kidPrevent wrong-key selection.
Keep old keys during overlapAvoid breaking tokens issued before rotation.
Monitor key-set driftUnexpected key changes may indicate issuer compromise/misconfig.

Dangerous anti-pattern:

// Anti-pattern: token header decides key source.
String jku = jwt.getHeader().get("jku");
JWKSet keys = JWKSet.load(new URL(jku));

Safer pattern:

// Trust is configured out-of-band.
IssuerConfig issuer = issuerRegistry.requireTrusted(jwt.claim("iss"));
JwkSource jwks = jwksCache.forIssuer(issuer.id(), issuer.jwksUri());
VerificationKey key = jwks.select(jwt.header().kid(), jwt.header().alg());

8. Java/Spring Resource Server Pattern

In many Java stacks, Spring Security is the practical enforcement library. Do not treat framework defaults as substitute for policy clarity.

8.1 Minimal resource server configuration

@Configuration
@EnableWebSecurity
class ApiSecurityConfig {

    @Bean
    SecurityFilterChain security(HttpSecurity http) throws Exception {
        return http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/actuator/health").permitAll()
                .requestMatchers("/api/cases/**").hasAuthority("SCOPE_cases.read")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
            .build();
    }
}

This is only the outer shell. Production systems usually need explicit claim validation.

8.2 Issuer and audience validation

@Bean
JwtDecoder jwtDecoder() {
    NimbusJwtDecoder decoder = JwtDecoders.fromIssuerLocation("https://idp.example.com/realms/regulatory");

    OAuth2TokenValidator<Jwt> issuer = JwtValidators.createDefaultWithIssuer(
        "https://idp.example.com/realms/regulatory"
    );

    OAuth2TokenValidator<Jwt> audience = jwt -> {
        List<String> aud = jwt.getAudience();
        if (aud.contains("regulatory-case-api")) {
            return OAuth2TokenValidatorResult.success();
        }
        OAuth2Error error = new OAuth2Error(
            "invalid_token",
            "Token audience does not include regulatory-case-api",
            null
        );
        return OAuth2TokenValidatorResult.failure(error);
    };

    decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(issuer, audience));
    return decoder;
}

8.3 Claim-to-authority mapping

External scopes are not your domain authorization model. They are inputs.

@Bean
JwtAuthenticationConverter jwtAuthenticationConverter() {
    JwtGrantedAuthoritiesConverter scopes = new JwtGrantedAuthoritiesConverter();
    scopes.setAuthorityPrefix("SCOPE_");
    scopes.setAuthoritiesClaimName("scope");

    JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
    converter.setJwtGrantedAuthoritiesConverter(jwt -> {
        Collection<GrantedAuthority> external = scopes.convert(jwt);
        List<GrantedAuthority> mapped = new ArrayList<>(external);

        String tenant = jwt.getClaimAsString("tenant_id");
        if (tenant == null || tenant.isBlank()) {
            return List.of();
        }

        mapped.add(new SimpleGrantedAuthority("TENANT_" + tenant));
        return mapped;
    });
    return converter;
}

Review question:

Apakah claim eksternal ini authority final, atau hanya evidence untuk policy decision internal?

For critical business authorization, do not stop at endpoint-level scope. Perform object-level authorization in the domain/service layer.


9. Access Token: Opaque vs JWT

Token styleProsConsGood fit
Opaque token + introspectionEasy revocation, central policy, smaller leak surfaceRuntime dependency on authorization server/introspectionHigh-control environments, internal APIs.
JWT self-containedLow latency, decentralized validationRevocation harder, claim staleness, key rotation complexityHigh-scale APIs with short-lived tokens.
Reference token with gateway exchangeAPI sees internal token onlyMore infrastructure complexityLarge platforms with API gateway and policy tier.

JWT does not remove the need for server-side authorization. JWT only packages claims. Claims are not decisions.

Decision rule:

Choose JWT when local validation and scale matter more than instant revocation.
Choose opaque/reference tokens when central control and revocation matter more.

10. Refresh Token Lifecycle

Refresh tokens are high-value credentials. Treat them like long-lived secrets.

Secure lifecycle:

Invariants:

  • refresh tokens must be bound to client and subject;
  • store only server-side or in platform secure storage for native apps;
  • browser-based SPAs should avoid long-lived refresh tokens unless strict rotation and sender constraints are in place;
  • rotation should detect reuse;
  • reuse of an already-rotated token indicates theft or race and should revoke the token family;
  • refresh-token grant should apply risk checks and may require reauthentication;
  • logout/revocation must invalidate server-side refresh state.

Refresh-token table model:

CREATE TABLE oauth_refresh_token_family (
    family_id UUID PRIMARY KEY,
    subject_key TEXT NOT NULL,
    client_id TEXT NOT NULL,
    tenant_id TEXT NOT NULL,
    status TEXT NOT NULL,
    created_at TIMESTAMP NOT NULL,
    revoked_at TIMESTAMP NULL,
    revoke_reason TEXT NULL
);

CREATE TABLE oauth_refresh_token_instance (
    token_hash BYTEA PRIMARY KEY,
    family_id UUID NOT NULL,
    sequence_no BIGINT NOT NULL,
    status TEXT NOT NULL,
    issued_at TIMESTAMP NOT NULL,
    used_at TIMESTAMP NULL,
    expires_at TIMESTAMP NOT NULL,
    FOREIGN KEY (family_id) REFERENCES oauth_refresh_token_family(family_id)
);

Store hashes of refresh tokens, not raw values.


11. Sender-Constrained Tokens: mTLS and DPoP

Bearer tokens are usable by whoever holds them. Sender-constrained tokens add proof that the caller controls a key.

MechanismBindingGood fitTrade-off
mTLS-bound access tokenToken contains confirmation bound to client certificateBackend/service-to-service, regulated environmentsCertificate lifecycle complexity.
DPoPToken bound to public key; each request carries proof JWTPublic clients, APIs needing replay reductionClock/replay cache/key handling complexity.
Plain bearerPossession of token onlyLower-risk, short-lived, TLS-only APIsStolen token can be replayed until expiry/revocation.

mTLS and DPoP do not replace authorization. They reduce replay impact.

DPoP mental model:

Resource server must verify:

  • DPoP proof signature;
  • proof htm equals HTTP method;
  • proof htu equals target URI normalization policy;
  • proof iat within narrow clock window;
  • proof jti not replayed;
  • access token confirmation claim matches DPoP key.

12. Federation and Multi-Issuer Trust

Federation is not “accept tokens from many IdPs”. Federation is a trust registry with explicit rules.

Federation rules:

RuleWhy
Trust issuers by explicit configPrevent arbitrary issuer acceptance.
Use issuer + subject as external identity keysub is only unique within issuer.
Do not auto-link by email aloneEmail may be unverified, changed, or controlled elsewhere.
Require verified email claim when email is usedAvoid account takeover via unverified claim.
Separate tenant resolution from user-provided domainPrevent domain spoofing.
Pin allowed client IDs per issuerPrevent token for other application being accepted.
Record policy version used for linkingAudit identity binding.

Account linking example:

public ExternalIdentityKey externalKey(Jwt idToken) {
    URI issuer = URI.create(idToken.getIssuer().toString());
    String subject = idToken.getSubject();
    if (!trustedIssuers.contains(issuer)) {
        throw new AccessDeniedException("untrusted issuer");
    }
    return new ExternalIdentityKey(issuer, subject);
}

Anti-pattern:

// Anti-pattern: creates takeover risk when email is not a stable, verified binding.
accountRepository.findByEmail(idToken.getClaimAsString("email"));

Safer linking ceremony:

  1. User authenticates with existing account.
  2. User authenticates with external issuer.
  3. App validates issuer, subject, email verification state, nonce, and risk signals.
  4. App links (issuer, subject) to internal account.
  5. App records audit event with issuer, subject hash, policy version, actor, IP/device, and timestamp.

13. Common Vulnerability Patterns

13.1 Token substitution

Attacker obtains a valid token for API A and sends it to API B. API B validates signature but not audience.

Guardrail:

Every resource server validates audience/resource indicator.

13.2 ID token accepted as access token

API accepts an ID token because signature and issuer are valid.

Guardrail:

Resource server validates token type/profile/audience and refuses ID token semantics.

13.3 Algorithm confusion

Verifier accepts algorithm from token header and uses inappropriate verification path.

Guardrail:

Allowed algorithms are configured per issuer/client/resource, not selected freely by token.

13.4 JWKS injection

Verifier follows jku from token header to attacker-controlled JWKS.

Guardrail:

JWKS URI is issuer-pinned; token header never controls trust anchor.

13.5 Redirect URI abuse

Client registration allows wildcard redirect or open redirect in app.

Guardrail:

Exact redirect URI matching + no open redirect endpoints in redirect path.

13.6 Authorization code interception

Authorization code is stolen from redirect path/log/proxy.

Guardrail:

PKCE + one-time code + short expiration + TLS + no code logging.

13.7 Refresh-token replay

Stolen refresh token is reused after legitimate rotation.

Guardrail:

Refresh token rotation + reuse detection + family revocation.

13.8 Scope overtrust

API maps admin scope directly to internal superadmin.

Guardrail:

External claim is evidence; internal policy still checks tenant, object, action, and role assignment.

13.9 Multi-tenant issuer confusion

API accepts token from tenant A for tenant B because path tenant is not checked against token tenant.

Guardrail:

tenant_id in path/resource/object must match token-authorized tenant context.

13.10 Logging token leakage

Reverse proxy/application logs Authorization header or callback query string.

Guardrail:

Redact Authorization, cookies, code, state, id_token, access_token, refresh_token, and DPoP proofs.

14. API Gateway vs Service-Level Validation

Gateway validation helps, but it is not enough by itself.

LayerWhat it can doWhat it cannot fully do
API gatewayTLS termination, token presence, issuer/audience validation, rate limitingObject-level domain authorization, deep tenant invariants.
Service filterRequest authentication, claim extraction, endpoint scopeBusiness object permission.
Domain serviceAction/object authorizationGlobal network enforcement.
Repository/query layerTenant/data scopingUser-intent semantics alone.

Recommended model:

Gateway validates coarse token integrity.
Service validates token context and maps principal.
Domain layer enforces action/object authorization.
Repository layer enforces data boundary.

This prevents “validated at the edge, bypassed internally” failure.


15. Token Lifetime and Revocation Strategy

TokenTypical lifetime designRevocation strategy
Authorization codeSeconds/minutes, one-timeMark used; reject replay.
Access tokenShort-livedLet expire; introspection/denylist for critical cases.
Refresh tokenLonger-lived but rotatedRevoke token family.
ID tokenShort-livedReauthenticate or rely on session lifecycle.
Session cookieApp-dependentServer-side invalidation.

Do not solve revocation by making every JWT very long-lived and adding a global blacklist unless you understand the scaling and consistency cost. Prefer short access-token lifetime and refresh-token rotation.

Risk events that should revoke or reduce token validity:

  • password/passkey/MFA change;
  • recovery event;
  • suspicious refresh-token reuse;
  • admin role grant/revoke;
  • tenant membership change;
  • device lost;
  • issuer key compromise;
  • user disabled;
  • client secret compromise.

16. Authorization Server Integration Decision Record

Use this template whenever adding an IdP/client/API integration.

# OAuth/OIDC Integration Decision Record

## Context
- System:
- Client type:
- Resource server/API:
- Issuer:
- Tenant model:

## Protocol Choice
- Flow:
- Why this flow:
- Alternatives rejected:

## Trust Configuration
- Issuer URI:
- JWKS URI source:
- Allowed algorithms:
- Expected audience:
- Expected client ID / authorized party:

## Token Rules
- Access token format:
- ID token usage:
- Refresh token policy:
- Token storage location:
- Token lifetime:
- Revocation strategy:

## Claims Mapping
- Subject key:
- Tenant key:
- Scope mapping:
- Roles/groups mapping:
- Claims explicitly ignored:

## Replay and Theft Defense
- PKCE:
- State:
- Nonce:
- Sender-constrained token:
- DPoP/mTLS:
- Token redaction:

## Federation and Account Linking
- External identity key:
- Email usage:
- Linking ceremony:
- Deprovisioning behavior:

## Verification
- Unit tests:
- Integration tests:
- Abuse tests:
- Monitoring:

## Approval
- Security reviewer:
- Engineering owner:
- Expiry/review date:

17. Abuse Tests

A mature implementation has tests for invalid-but-plausible tokens.

TestExpected result
Valid signature, wrong audReject.
Valid signature, wrong issReject.
Expired tokenReject.
Future nbfReject.
alg=noneReject.
Token signed with unexpected algReject.
Unknown kid with attacker jkuReject; do not fetch attacker URL.
ID token sent to APIReject.
Access token without required scopeReject.
Token from tenant A used on tenant B resourceReject.
Reused authorization codeReject.
Reused rotated refresh tokenRevoke family.
Missing/incorrect OIDC nonceReject login.
DPoP proof replayReject.

Example test shape:

@Test
void rejectsTokenWithWrongAudience() {
    String jwt = tokenFactory.validToken(builder -> builder
        .issuer("https://idp.example.com/realms/regulatory")
        .audience("some-other-api")
        .scope("cases.read")
    );

    assertThatThrownBy(() -> verifier.verify(jwt))
        .isInstanceOf(InvalidTokenException.class)
        .hasMessageContaining("audience");
}

18. Production Observability

Security-relevant OAuth/OIDC events:

EventFields
login_startedclient_id, issuer, redirect_uri hash, state_id hash, user_agent risk.
login_completedissuer, subject hash, auth_time, acr/amr, client_id.
token_validation_failedreason, issuer, audience, kid, alg, endpoint, correlation_id.
jwks_refresh_failedissuer, jwks_uri, status, cache_age.
unknown_kid_seenissuer, kid, alg, rate.
refresh_token_rotatedsubject hash, client_id, family_id, sequence.
refresh_token_reuse_detectedfamily_id, client_id, subject hash, IP/device risk.
account_linkedinternal account, issuer, subject hash, policy_version.
high_risk_claim_mappingclaim name, source issuer, decision.

Never log raw tokens, authorization codes, refresh tokens, DPoP proofs, or cookies.


19. Lab: Build a Misuse-Resistant Token Verifier

Goal

Build a small Java verifier facade around your JWT library. The facade must make insecure validation hard.

Requirements

  • issuer registry is configured out-of-band;
  • JWKS URI comes from trusted issuer metadata/config, not token header;
  • allowed algorithms configured per issuer;
  • audience required;
  • expiry and not-before checked;
  • token type/profile checked;
  • tenant consistency checked;
  • scope mapping separated from domain authorization;
  • tests include wrong audience, wrong issuer, alg confusion, unknown kid, expired token, and ID-token-as-access-token.

Suggested API

public interface AccessTokenVerifier {
    VerifiedPrincipal verify(String rawToken, ResourceContext resourceContext);
}

public record ResourceContext(
    String expectedAudience,
    String tenantFromRoute,
    String endpointId
) {}

public record VerifiedPrincipal(
    URI issuer,
    String subject,
    String clientId,
    String tenantId,
    Set<String> scopes,
    Instant authenticatedAt
) {}

The verifier should return a normalized principal. It should not leak raw JWT objects throughout the domain model.


20. Final Checklist

Flow

  • Authorization Code + PKCE used for interactive clients.
  • Implicit flow avoided for new systems.
  • ROPC avoided unless explicitly justified for legacy migration.
  • Exact redirect URI matching enforced.
  • state used and validated.
  • OIDC nonce used and validated for login.
  • Authorization code is one-time and short-lived.

Token validation

  • Issuer exact match.
  • Audience exact/expected.
  • Expiry and not-before checked.
  • Algorithm allowlist.
  • Signature validation against issuer-pinned key source.
  • JWKS URI is not taken from token header.
  • Token type/profile checked.
  • ID token not accepted by API.
  • Tenant claim/path/resource consistency checked.
  • External claims mapped explicitly.

Refresh/session

  • Refresh tokens stored securely.
  • Refresh-token rotation enabled.
  • Reuse detection revokes token family.
  • Logout/revocation semantics defined.
  • Sensitive token material redacted from logs/traces.

Federation

  • Trusted issuers configured explicitly.
  • Account key uses issuer + subject.
  • Email is not used as sole linking proof.
  • Tenant/issuer/client mappings reviewed.
  • Deprovisioning behavior defined.

Review stance

  • A valid token for another API is rejected.
  • A valid token from another issuer is rejected.
  • A valid token from another tenant is rejected.
  • A valid ID token is rejected by resource server.
  • A valid token without object permission is rejected by domain authorization.

21. What You Should Be Able to Explain

After this part, you should be able to explain:

  1. Why OAuth2 is not automatically authentication.
  2. Why OIDC ID token must not be used as API access token.
  3. Why signature-valid JWT can still be invalid.
  4. Why issuer + subject is safer than email for account linking.
  5. Why audience validation prevents confused deputy.
  6. Why PKCE matters even when TLS exists.
  7. Why JWKS is key distribution, not dynamic trust.
  8. Why gateway token validation does not replace object-level authorization.
  9. Why refresh token rotation needs reuse detection.
  10. Why external scopes are policy inputs, not necessarily final domain permissions.

22. Bridge to Part 018

OAuth/OIDC tells the system who the caller is and what delegated claims/scopes arrived with the request. It does not answer the full domain question:

Can this subject perform this action on this object in this tenant and business state right now?

That is authorization. Part 018 builds the authorization model: RBAC, ABAC, ReBAC, policy engines, object-level checks, tenant boundaries, data filtering, decision auditing, and policy testing.

Lesson Recap

You just completed lesson 17 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.