jSentinel V00.79.10 — Token-Binding + Logout-Hardening (Etappe 2)
Release Notes — V00.79.10
jSentinel V00.79.10 — Token-Binding + Logout-Hardening (Etappe 2). The second
deploy of the V00.79 hardening line, building on the Etappe-1 vendor profiles +
Stable-API promotion. Three security blocks: replay-protection stores, DPoP
sender-constrained tokens (RFC 9449) and OIDC logout-hardening (Back-Channel +
Front-Channel Logout). The remaining V00.79 blocks (mTLS / PAR / JAR / JWE, FIPS,
demos) ship in v00.79.20 (Etappe 3).
All V00.79.10-new types carry @ExperimentalJSentinelApi — they start their own soak
window. The Etappe-1 promoted JWT/OAuth2/OIDC surfaces are unchanged.
Replay-protection stores (B3)
Two new persistence-neutral SPIs in jSentinel-core (com.svenruppert.jsentinel.replay.api):
| SPI | Purpose |
|---|---|
JtiStore |
Single-use jti (record(jti, expiresAt) → Result<Boolean, ReplayError>) — backs DPoP-proof and logout-token replay protection. |
NonceStore |
Per-request-key nonce binding (bind / single-use TTL-checked consume). |
InMemoryJtiStore is a bounded sliding window (default 100k). On overflow it purges
expired entries first, then evicts the entry with the soonest expiry — never the
least-recently-used one. An LRU policy would let an attacker flood the store with
self-signed valid proofs to evict a victim's still-live jti and replay a captured
proof; this mirrors the V00.75.20 R012 fix to the event replay store. InMemoryNonceStore
is a clock-injected, single-use, TTL-checked map. Both are single-JVM; a multi-node
deployment plugs a shared store.
DPoP — sender-constrained tokens (B4, RFC 9449)
New opt-in module jSentinel-dpop (depends on jSentinel-jwt + jSentinel-propagation).
DpopProofGeneratorsigns an outbounddpop+jwtproof:typ+ embedded public
JWK in the header;jti/htm/htu/iatin the body, plusath(base64url
SHA-256 of the access token) when bound to a token at a resource server.NimbusDpopProofValidator(final, composition over inheritance) parses the
proof, verifies the signature against the embedded public key, enforces an
algorithm allow-list (rejectsnoneand private/symmetric header keys), checks
htm/htu(scheme+host+path, default-port-normalised, query/fragment stripped) /
iatfreshness (too-old and future-dated rejected) /ath(constant-time), and
enforces single-usejtivia the B3JtiStore. It returns the RFC 7638 key
thumbprint so the caller can bind the proof to a DPoP-confirmed access token
(cnf.jkt, viaValidatedDpopProof.confirms(...)).- Sealed
DpopValidationError:proof-malformed/signature-invalid/
htm-mismatch/htu-mismatch/proof-expired/replay/ath-mismatch— each
leak-free (never echoes the proof). DpopKeyStoreSPI +InMemoryDpopKeyStorehold the client's per-target signing
keys (distinct key per server, private-half required).
The thumbprint (the last fallible step) is computed before the jti is recorded,
so a thumbprint failure cannot consume a one-time jti.
OIDC logout-hardening (B5)
Back-Channel Logout 1.0 + Front-Channel Logout 1.0. API in jSentinel-core
(oidc.api); impls in jSentinel-identity-oidc.
DefaultLogoutTokenValidatorcomposes a V00.76JwtValidator(signature / iss /
aud / iat) and adds the §2.4 logout-token checks: theevents
back-channel-logout member must be present,noncemust be absent (the guard
that stops an ID token being replayed as a logout token),suband/orsidmust be
present,jtiis required, andjtiis enforced single-use viaJtiStore.
SealedLogoutTokenValidationError(jwt-invalid/missing-sub-and-sid/
missing-backchannel-event/nonce-present/missing-jti/replay).BackChannelLogoutReceivervalidates the postedlogout_tokenthen terminates
the matching RP sessions via theSessionRegistrySPI. It is pure
token-in / outcome-out — it dereferences no URL from the token, so it introduces
no SSRF (§2.6).BackChannelLogoutOutcomemaps to the §2.7 response
(Accepted→ 200,Rejected→ 400).FrontChannelLogoutEndpointchecks theissquery parameter against the trusted
issuer before terminating bysid— a forged issuer is a silent no-op (an attacker
cannot force-logout via a spoofediss).InMemorySessionRegistryindexes RP session ids by (issuer,subject) and
(issuer,sid);terminateremoves bysidwhen present, otherwise every session
of the subject (§2.5), with cross-index cleanup so terminated ids cannot resurface.
Exit-review (R-EXIT) — fixes folded in before ship
A two-pronged adversarial review (DPoP/replay + logout) ran at the Stage-D gate. Three
findings were fixed in-release:
| Id | Finding | Fix |
|---|---|---|
| R-EXIT-1 | InMemoryJtiStore evicted LRU → replay-flood defeat |
Evict soonest-to-expire (purge expired first); R012-class fix |
| R-EXIT-2 | DPoP thumbprint computed after jti recorded |
Compute thumbprint before recording the jti |
| R-EXIT-3 | logout token without jti accepted (replay silently skipped) |
jti now required (MissingJwtId) |
Documented, lower-severity nits (htu host-canonicalisation edge cases; front-channel
return value as a session-existence oracle internal to RP code; registry composite-key
delimiters) are carried as backlog — none is reachable through the validated trust
boundary.
Tests (no-mocks)
ReplayStoresTest(5) — replay rejection, re-record-after-expiry, nonce single-use +
TTL, soonest-to-expire eviction under overflow.DpopProofRoundTripTest(6) — real RSA + EC keys, real Nimbus signing/verification,
realInMemoryJtiStore: accept-on-valid + reject replay / htm / htu / ath / staleness.LogoutHardeningTest(8) — real RSA-signed logout tokens: accept-and-terminate +
reject forged-signature / nonce-present / missing-event / missing-sub-sid / missing-jti
/ replay; front-channel trusted-vs-forged issuer.
All library modules green under -Pstatic-analysis clean install. The full reactor
also surfaced pre-existing demo-only debt unrelated to V00.79.10 — a batch of
MS_EXPOSE_REP findings on the demos' deliberate static test-seam accessors plus two
flaky in-process HTTP-timing tests (demo-rest); the deterministic JWT-tamper flake
was fixed here, the rest is tracked as a separate demo-hardening task. The Central
bundle ships library modules only.
Mutation coverage (V00.79.10)
| Module | Mutation | Line | Notes |
|---|---|---|---|
jSentinel-dpop |
63 % (57/91) | 70 % (129/185) | New module, first PIT pass; remaining survivors are normalisation/guard branches |
jSentinel-core and jSentinel-identity-oidc keep their established baselines; the
B3/B5 additions are covered by the no-mock suites above (focused PIT deferred to the
next touched-module pass).
Module inventory
54 reactor modules (including the new jSentinel-dpop). New coordinates:
com.svenruppert.jsentinel:jSentinel-dpop:00.79.10
Upgrade
Additive release. No source changes required for existing consumers. To adopt:
- DPoP: add
jSentinel-dpop; sign outbound proofs withDpopProofGenerator,
validate inbound withNimbusDpopProofValidator(allowList, jtiStore, clock). - Back-Channel Logout: wire
BackChannelLogoutReceiver(validator, sessionRegistry)
behind yourbackchannel_logout_uri; implementSessionRegistryover your session
store (or start withInMemorySessionRegistry).