This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch docs
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/docs by this push:
     new 7c874f27d5 feat(rest,jwt,oidc): TODO-127/114/140/141/115/117 - 
BeanPropertyMeta abstract-collection fix, TraceContextResponseProcessor, 
OIDC-RP module, JwksCache/JwtTokenValidator improvements, observability 
annotations+metrics, MDC async propagation
7c874f27d5 is described below

commit 7c874f27d517b63bf2c83cfbdc30b221c3ec5e0b
Author: James Bognar <[email protected]>
AuthorDate: Fri May 29 09:50:24 2026 -0400

    feat(rest,jwt,oidc): TODO-127/114/140/141/115/117 - BeanPropertyMeta 
abstract-collection fix, TraceContextResponseProcessor, OIDC-RP module, 
JwksCache/JwtTokenValidator improvements, observability annotations+metrics, 
MDC async propagation
---
 pages/release-notes/9.5.0.md                    | 215 +++++++++++++++++++++++-
 pages/topics/02.21.07.ValueFrameworkInternal.md |   2 +
 pages/topics/10.20e.RestServerAuthGuards.md     |   3 +
 pages/topics/10.20g.RestServerObservability.md  |  33 ++++
 pages/topics/10.20i.AuthFilterFramework.md      |   1 +
 pages/topics/10.20k.OAuthAuthSupport.md         |   4 +-
 pages/topics/10.20l.OidcRelyingParty.md         | 167 ++++++++++++++++++
 7 files changed, 419 insertions(+), 6 deletions(-)

diff --git a/pages/release-notes/9.5.0.md b/pages/release-notes/9.5.0.md
index a0de569d02..1675afa2c9 100644
--- a/pages/release-notes/9.5.0.md
+++ b/pages/release-notes/9.5.0.md
@@ -392,6 +392,14 @@ land, parser sessions other than `JsonParserSession` / 
`Json5ParserSession` prod
 
 #### Bugs
 
+- **`@Inject`-method and constructor `@Value Optional<T>` parameters** now 
wrap resolved values
+  correctly (TODO-128). Previously, declaring a parameter as 
`@Value("${maybe}") Optional<String>`
+  caused `IllegalArgumentException` at reflective invoke because 
`ParameterInfo.resolveValue`
+  returned the unwrapped scalar. `ParameterInfo.resolveValue` now mirrors 
`FieldInfo.inject`:
+  when the declared parameter type is `Optional<T>`, a missing or empty-string 
resolution becomes
+  `Optional.empty()` and a non-empty resolved value becomes 
`Optional.of(value)`. The plain-scalar
+  workaround (declare as `T` + null-check) is no longer needed for 
Optional-typed parameters.
+
 - **`ClassFormatSwap.unswap(...)` now consults the session classloader** 
before falling back to the
   thread-context classloader (TODO-138). Sessions that explicitly set a 
classloader via
   `MarshallingContext.Builder.classLoader(ClassLoader)` — such as OSGi bundle 
classloaders, webapp
@@ -411,6 +419,19 @@ land, parser sessions other than `JsonParserSession` / 
`Json5ParserSession` prod
   change forecloses a class of latent bugs where a future caller of 
`skipWhitespaceAndComments` could
   silently reintroduce the over-read.
 
+- **Abstract `Collection`-typed bean properties with no setter and no 
`@BeanProp(type=...)` now populate
+  correctly** (TODO-127). A field such as `Set<MyEnum> tags;` (or `List` / 
`SortedSet` / `Deque` / `Queue`,
+  generic or raw) with only a public field and no concrete type hint 
previously could not be parsed from a
+  JSON array — there was no concrete-collection materialization branch (the 
sibling abstract-`Map` case was
+  already handled). `BeanPropertyMeta.setPropertyValue(...)` now materializes 
a concrete collection using a
+  best-effort-then-fallback strategy: it first tries to instantiate the 
field's declared type directly (so a
+  concrete `AbstractSet` / `AbstractList` subclass with an accessible no-arg 
constructor is honored as
+  itself), and only falls back to a shape-based default when the declared type 
is genuinely not instantiable
+  — `SortedSet`/`NavigableSet` → `TreeSet`, `Set` → `LinkedHashSet` (insertion 
order preserved),
+  `Deque`/`Queue` → `ArrayDeque`, `List`/raw `Collection` → `ArrayList`. 
Element values are coerced to the
+  declared element type. An explicit `@BeanProp(type=...)` override still 
wins, but it is **no longer
+  required** as a workaround for these abstract-collection fields.
+
 ### juneau-commons
 
 #### `@Value` annotation + `${xxx}` shortcut (TODO-79)
@@ -2257,6 +2278,49 @@ String name
 
 ### juneau-rest-server
 
+#### Outgoing-response W3C trace-context headers (TODO-114)
+
+When a non-no-op `TracerHook` is active on a request, the server now writes 
the W3C `traceparent` — and, when present, the `tracestate` — header back onto 
the HTTP **response**, so a client calling the server can read the resulting 
trace id off the response and correlate against it. The emitted `traceparent` 
reflects the **server-started span's** context (a freshly-minted span id 
parented to the inbound trace), not the inbound header verbatim. `tracestate` 
is written **only when the acti [...]
+
+- **`org.apache.juneau.rest.tracing.TraceContextResponseProcessor`** (new) — a 
`ResponseProcessor` prepended to the default `ResponseProcessorList`. It reads 
the rendered `traceparent` / `tracestate` strings that a `TracerHook` bridge 
stashed as request attributes (`juneau.traceparent` / `juneau.tracestate`) and 
writes them as response headers. Its first action is a single request-attribute 
read that short-circuits with zero allocations when no tracer ran, preserving 
the off-by-default o [...]
+- **`RestContext.responseTraceparent`** (new env-driven default, 
`${RestContext.responseTraceparent:true}`) — controls whether 
`TraceContextResponseProcessor` is registered. Defaults to `true` 
(on-when-tracer); set it to `false` to keep the processor out of the chain 
entirely even when a tracer is active. Exposed via 
`RestContext.isResponseTraceparent()`. A per-resource 
`@Rest(observability="false")` opt-out is now available via TODO-115 — see 
below.
+
+The companion OTel bridge change lives in `juneau-rest-server-otel`: 
`OtelTracerHook.startSpan(...)` now renders the W3C header values from the 
server-started span context at span-start time (the only point where it is 
reliably active) and stashes them as the request attributes the processor 
reads. See [REST Server — Observability](/docs/topics/RestServerObservability) 
for the full topic.
+
+#### Per-resource / per-method observability opt-in attributes (TODO-115)
+
+`@Rest`, `@RestOp`, and all HTTP verb annotations (`@RestGet`, `@RestPost`, 
`@RestPut`, `@RestDelete`, `@RestPatch`, `@RestOptions`) now carry three new 
`String` members for observability control:
+
+- **`observability`** (default `""`) — tri-state opt-in attribute with SVL 
variable support:
+  - `""` (empty, default) — inherit from parent / preserve today's behavior 
(observability fires when a backend `@Bean` is wired, is silent otherwise).
+  - `"true"` — strict mode: the `RestContext` / `RestOpContext` constructor 
throws `InternalServerError` at startup if no non-NoOp `MetricsRecorder` or 
`TracerHook` `@Bean` is wired on the resource. Use this to make a 
missing-backend wiring mistake a hard deployment error rather than a silent 
no-op.
+  - `"false"` — explicit opt-out: the observability block in `RestOpInvoker` 
is short-circuited for that resource or op, even when a backend is wired. Use 
this on health-probe and admin endpoints that should not contribute to metrics 
cardinality.
+  - Verb-level setting overrides the resource-level setting symmetrically (can 
both enable and disable).
+  - SVL variables (e.g. `@Rest(observability="$S{my.env.obs,}")`) are resolved 
at `RestContext` build time, so the startup-fail check fires correctly against 
the environment's resolved value.
+
+- **`metricName`** (default `""`) — per-op timer / metric name override. When 
non-empty, passed to `MetricsRecorder.record(...)` as the `metricName` 
parameter; `MicrometerMetricsRecorder` uses it as the Micrometer `Timer` name 
instead of the default `http.server.requests`.
+
+- **`metricTags`** (default `""`) — per-op additional metric tags in 
comma-separated `key=value` format (e.g. `"team=payments,region=us-east"`). 
Passed to `MetricsRecorder.record(...)` as the `metricTags` parameter; 
`MicrometerMetricsRecorder` adds these as extra `Timer` tags alongside the 
standard `method` / `uri` / `status` / `exception` tags.
+
+Example:
+
+```java
+@Rest(path="/api", observability="true")  // startup-fail if no @Bean 
MetricsRecorder / TracerHook
+public class ApiResource extends RestServlet {
+
+    @RestGet("/orders/{id}")
+    @RestGet(path="/orders/{id}", metricName="orders.lookup", 
metricTags="team=payments")
+    public Order getOrder(@Path String id) { ... }
+
+    @RestGet(path="/health", observability="false")  // health probe: never 
metered
+    public HealthStatus health() { return HealthStatus.OK; }
+}
+```
+
+**`MetricsRecorder.record(...)` signature change (migration required if you 
implement `MetricsRecorder`):** The method now takes two additional trailing 
parameters — `String metricName` and `String metricTags` — after the existing 
`Throwable error` parameter. Update any custom `MetricsRecorder` 
implementations to add these two parameters (they may be safely ignored if not 
needed). The `NoOpMetricsRecorder` and `MicrometerMetricsRecorder` bundled with 
Juneau have been updated.
+
+**Interaction with `TraceContextResponseProcessor` (TODO-114):** When 
`observability="false"` is effective for an op, `RestOpInvoker` skips the 
`TracerHook.startSpan(...)` call. Since `OtelTracerHook` stashes the 
`juneau.traceparent` / `juneau.tracestate` request attributes inside 
`startSpan(...)`, those attributes are never set, and 
`TraceContextResponseProcessor` naturally emits no trace-context response 
headers for that op — no additional wiring is needed.
+
 #### `View`-returning `@RestOp` methods now reach their renderer automatically 
(TODO-96)
 
 Response processors implementing the new `ViewRenderer` marker interface are 
automatically
@@ -2445,7 +2509,7 @@ A new JUnit 5 extension and `@TestBean` annotation enable 
Spring-style test-time
 
 ##### New SPIs in `juneau-rest-server`
 
-- **`org.apache.juneau.rest.metrics.MetricsRecorder`** — 
`@FunctionalInterface` with a single method `record(String opName, String 
httpMethod, String uriTemplate, int statusCode, Duration elapsed, Throwable 
error)`. Called once per `@RestOp` invocation (happy path and exception path). 
The `uriTemplate` is the `@RestOp` path template (`/users/{id}`) — not the raw 
concrete URI — so tag cardinality stays bounded.
+- **`org.apache.juneau.rest.metrics.MetricsRecorder`** — interface with a 
single method `record(String opName, String httpMethod, String uriTemplate, int 
statusCode, Duration elapsed, Throwable error, String metricName, String 
metricTags)`. Called once per `@RestOp` invocation (happy path and exception 
path). The `uriTemplate` is the `@RestOp` path template (`/users/{id}`) — not 
the raw concrete URI — so tag cardinality stays bounded. The `metricName` and 
`metricTags` parameters (added i [...]
 - **`org.apache.juneau.rest.metrics.NoOpMetricsRecorder`** — process-wide 
singleton `INSTANCE`; resolved when no bean is registered. Drops events on the 
floor with zero allocations.
 - **`org.apache.juneau.rest.tracing.TracerHook`** — `@FunctionalInterface` 
with `Scope startSpan(RestRequest request)`. The framework opens the scope 
before parameter binding and always closes it in a `finally` block; the 
resolved HTTP status and any thrown throwable are delivered via 
`Scope.setStatusCode(int)` / `Scope.setError(Throwable)` before `Scope.close()`.
 - **`org.apache.juneau.rest.tracing.Scope`** — `AutoCloseable` with 
`setStatusCode(int)`, `setError(Throwable)`, and `close()`. The framework 
guarantees `close()` runs exactly once per call (including parameter-resolution 
failures and 5xx paths).
@@ -2513,10 +2577,79 @@ Synchronous handlers (no `CompletableFuture`, no 
`virtualThreads=true`) have **z
 
 ##### Thread-local caveats
 
-`RequestAttributes`, `VarResolverSession`, `Locale`, and the 
`@Rest(debug=...)` `DebugConfig` are request-scoped, not `ThreadLocal`-backed — 
they survive the async hop. **MDC and security contexts** that rely on 
`ThreadLocal` (SLF4J MDC, classic `SecurityContextHolder`-style patterns) **do 
not** survive the async hop. If you need MDC across an async boundary, copy the 
relevant keys into `RequestAttributes` before returning the future.
+`RequestAttributes`, `VarResolverSession`, `Locale`, and the 
`@Rest(debug=...)` `DebugConfig` are request-scoped, not `ThreadLocal`-backed — 
they survive the async hop. **MDC and security contexts** that rely on 
`ThreadLocal` (SLF4J MDC, classic `SecurityContextHolder`-style patterns) **do 
not** survive the async hop. If you need MDC across an async boundary, copy the 
relevant keys into `RequestAttributes` before returning the future, or enable 
the new SLF4J MDC bridge described below.
 
 See [REST Server — Async Returns + Virtual-Thread 
Dispatch](/docs/topics/RestServerAsyncDispatch) for the full reference.
 
+#### SLF4J MDC async propagation bridge (TODO-117)
+
+When a `@RestOp` handler returns a `CompletableFuture`, the `whenComplete` 
callback typically runs on a different thread (a virtual thread, pool thread, 
or fork-join worker). Log statements emitted inside that callback previously 
had no SLF4J MDC context — any `requestId`, `userId`, or trace-correlation keys 
set by an upstream filter were invisible to the logger running on the 
completion thread.
+
+**New behavior (default-on when SLF4J is on the classpath):** 
`AsyncResponseProcessor` now snapshots the request thread's MDC map immediately 
before registering the `whenComplete` callback, then wraps the callback so the 
completion thread sees the same diagnostic context.
+
+```java
+// No code change required — MDC propagates automatically:
+@RestGet("/orders/{id}")
+public CompletableFuture<Order> getOrder(@Path String id) {
+    // MDC.get("requestId") is non-null here (set by upstream filter).
+    return orderService.fetchAsync(id);
+    // ... and is also non-null inside the whenComplete callback, even on a 
virtual thread.
+}
+```
+
+**Key properties:**
+
+- **Zero-cost when unused.** If the request thread's MDC is empty at dispatch 
time (the common case for services that don't set MDC), the snapshot is `null` 
and the callback is registered unchanged — no extra allocations.
+- **Clean-up on the completion thread.** After the callback returns (or 
throws), the completion thread's MDC is restored to exactly the state it was in 
before the callback. The listener removes only the keys it installed — any MDC 
state the completion thread already had is preserved. No thread-pool 
contamination.
+- **Exceptional-completion path.** If the future completes with an error, the 
`finally` block still runs — MDC is cleared even when the callback throws.
+- **SLF4J-only.** The bridge covers SLF4J MDC (`org.slf4j.MDC`). Log4j2 
`ThreadContext` direct users (without the SLF4J facade) are not covered in v1. 
Users on the SLF4J ↔ Log4j2 bridge get the correct behavior automatically.
+- **Opt out.** Set the system / environment variable 
`RestContext.mdcAsyncPropagation=false` to disable globally, or call 
`RestContext.Builder.mdcAsyncPropagation(false)` to disable on a specific 
resource.
+
+**New API surface:**
+
+- **`org.apache.juneau.rest.processor.MdcAsyncListener`** (new) — static 
utility: `snapshot()` captures the current thread's MDC; `wrap(BiConsumer, 
Map)` decorates a completion callback with snapshot-restore-clear logic. No 
SLF4J compile-time dependency; detected reflectively at class-load time.
+- **`RestContext.isMdcAsyncPropagation()`** (new) — returns `true` when MDC 
propagation is enabled. Defaults to `true`; overridden by the 
`RestContext.mdcAsyncPropagation` env-driven property.
+- **`RestContext.Builder.mdcAsyncPropagation(boolean)`** (new) — per-resource 
programmatic override.
+
+#### Per-resource async completion executor (TODO-118)
+
+Production deployments need to isolate `CompletableFuture` completion 
callbacks on a dedicated thread pool — the future's natural completion thread 
is often an I/O-driver or virtual thread that should not be blocked by 
response-handler work. A new `asyncCompletionExecutor` attribute lets you route 
callbacks to any named `Executor` bean registered with the resource.
+
+```java
+@Rest(path="/api", asyncCompletionExecutor="myCompletionPool")
+public class ApiResource extends RestServlet {
+
+    @Bean(name="myCompletionPool")
+    public Executor completionPool() {
+        return Executors.newFixedThreadPool(8, r -> new Thread(r, 
"response-handler-"));
+    }
+
+    @RestGet("/orders/{id}")
+    public CompletableFuture<Order> getOrder(@Path String id) {
+        return orderService.fetchAsync(id);
+        // whenComplete callback runs on myCompletionPool, not the I/O thread
+    }
+}
+```
+
+Key details:
+
+- **`@Rest(asyncCompletionExecutor="beanName")`** — resource-level default; 
resolves a `java.util.concurrent.Executor` bean by name from `BeanStore`. Empty 
string (default) = no override; natural completion thread is used.
+- **`@RestOp(asyncCompletionExecutor="beanName")`** and all verb annotations 
(`@RestGet`, `@RestPost`, …) — per-operation override; wins over the 
resource-level setting.
+- **`RestContext.Builder.asyncCompletionExecutor(String beanName)`** — 
programmatic knob for the same configuration.
+- **Startup-fail** — if the configured bean name does not resolve to an 
`Executor` in `BeanStore`, `RestContext` construction throws immediately 
(misconfiguration is never silently ignored).
+- **MDC propagation compatibility** — the TODO-117 `MdcAsyncListener` wraps 
the callback *before* executor routing; the MDC snapshot is taken on the 
dispatching thread and restored on whichever thread the executor selects.
+- **Virtual-thread executor** — `Executors.newVirtualThreadPerTaskExecutor()` 
(Java 21+) is a valid value; each callback runs on a fresh virtual thread.
+- **Default behavior unchanged** — when the annotation is absent, 
`whenComplete(callback)` is used (natural thread, identical to 9.4.x behavior).
+
+New API:
+
+- **`@Rest(asyncCompletionExecutor=...)`** / 
**`@RestOp(asyncCompletionExecutor=...)`** / all verb annotations — new 
`String` annotation member.
+- **`RestContext.Builder.asyncCompletionExecutor(String)`** — programmatic 
setter.
+- **`RestContext.getAsyncCompletionExecutor()`** — resource-level resolved 
executor (or `null`).
+- **`RestOpContext.getAsyncCompletionExecutor()`** — op-level resolved 
executor with resource-level fallback.
+- **`RestServerConstants.PROPERTY_asyncCompletionExecutor`** — annotation 
property key constant.
+
 #### Health Probe SPI + Resource (TODO-65)
 
 `juneau-rest-server` now includes a built-in probe SPI and aggregation 
resource under
@@ -4052,6 +4185,35 @@ A new opt-in REST module, `juneau-rest-server-jwt`, adds 
JWT bearer-token verifi
 - **Mandatory claims** — `iss` / `aud` / `exp` / `nbf` are required by 
default. A token missing any of them is rejected.
 - **Clock-skew cap** — the builder will refuse a `clockSkew(...)` value larger 
than 5 minutes (300s).
 
+#### JWKS-on-`kid`-miss eager refresh (behavior change — TODO-141)
+
+**`JwksCache` now performs a single eager JWKS refresh when a fresh-cache key 
selection returns no matching keys.** This eliminates the rotation window where 
an IdP publishes a new signing key mid-TTL and clients receive spurious 401 
responses for perfectly valid tokens (up to the full cache TTL — 5 minutes by 
default — under the old behavior).
+
+The refresh is bounded by two guards to prevent abuse:
+
+- **Per-cache cooldown** (default 10 seconds, `@Value` key 
`juneau.jwt.jwksEagerRefreshCooldown`) — at most one eager refresh fires per 
cooldown window regardless of how many concurrent `kid`-miss requests arrive.
+- **Single in-flight `CompletableFuture`** — concurrent callers experiencing 
the same miss wait for (and share) the result of the single in-flight fetch; no 
thundering herd.
+
+On JWKS-endpoint outage during an eager refresh, the last-known-good key set 
is retained (valid tokens continue to verify) and the cooldown is still 
advanced so a down endpoint is not hammered.
+
+**This feature is enabled by default** (behavior change: fewer spurious 401s 
during key rotation). To opt out and restore pre-9.5.0 behavior:
+
+```java
+JwtTokenValidator.create()
+    .jwksEagerRefreshOnKidMiss(false)  // or 
-Djuneau.jwt.jwksEagerRefreshOnKidMiss=false
+    ...
+    .build();
+```
+
+New builder setters and `@Value` keys:
+
+| Builder setter | `@Value` key | Default | Cap / validation |
+|---|---|---|---|
+| `jwksEagerRefreshOnKidMiss(boolean)` | 
`juneau.jwt.jwksEagerRefreshOnKidMiss` | `true` | — |
+| `jwksEagerRefreshCooldown(Duration)` | `juneau.jwt.jwksEagerRefreshCooldown` 
| `PT10S` | positive; `≤ jwksCacheTtl`; `≤ PT60S` |
+
+Applies only to `jwksUrl(...)`-backed caches. A caller-supplied 
`jwkSource(...)` is unaffected.
+
 #### Dependency
 
 ```xml
@@ -4145,9 +4307,9 @@ A new opt-in REST module, `juneau-rest-server-oauth`, 
adds OAuth 2.0 / OIDC bear
 
 #### Deferred
 
-- **OIDC Relying Party login flow** (`juneau-rest-server-oidc-rp`) &mdash; 
deferred to a follow-on TODO (per OQA Q3). The discovery client + auth-code 
helper are the building blocks the RP module will compose on top of.
+- **OIDC Relying Party login flow** &mdash; now shipped as the 
`juneau-rest-server-oidc-rp` module (see below); the discovery client + 
auth-code helper here are the building blocks it composes on top of.
 - **Device-code grant** (RFC 8628) &mdash; deferred to a follow-on TODO 
if/when needed.
-- **JWKS-on-`kid`-miss eager refresh** &mdash; deferred to a follow-on TODO 
targeting `juneau-rest-server-jwt`'s `JwksCache`.
+- **JWKS-on-`kid`-miss eager refresh** &mdash; now shipped; see the 
`juneau-rest-server-jwt` section below.
 
 #### Dependency
 
@@ -4164,6 +4326,49 @@ A new opt-in REST module, `juneau-rest-server-oauth`, 
adds OAuth 2.0 / OIDC bear
 </dependency>
 ```
 
+### juneau-rest-server-oidc-rp (new module)
+
+A new opt-in REST module, `juneau-rest-server-oidc-rp`, turns a Juneau REST 
server into a complete OpenID Connect **Relying Party** &mdash; the interactive 
"Log in with Google / Okta / Entra ID / Keycloak" browser flow &mdash; on top 
of `juneau-rest-server-oauth`. It orchestrates the authorization redirect 
&rarr; callback &rarr; session dance (single-use `state`/`nonce`, PKCE S256, 
strict ID-token validation), wrapping the [Nimbus OAuth 2.0 
SDK](https://connect2id.com/products/nimbus-oau [...]
+
+`mvn -pl juneau-rest/juneau-rest-server dependency:tree | grep -iE 
"(nimbusds|oauth2-oidc)"` returns nothing &mdash; the containment requirement 
is verified at build time. See [REST Server &mdash; OIDC Relying Party 
Login](/docs/topics/OidcRelyingParty) for the full reference.
+
+#### New Classes
+
+- **`org.apache.juneau.rest.auth.oidc.rp.OidcRelyingParty`** &mdash; Builder + 
facade the application's `@Rest` resource calls: `startLogin` (generate + store 
`state`/`nonce`/PKCE verifier, 302 → IdP), `completeLogin` (parse callback, 
single-use-consume `state`, exchange code, validate ID token, create session, 
302 → app), `logout` (invalidate session + clear cookie + redirect through the 
IdP `end_session_endpoint`), `refresh` (rotating-refresh-token grant), and 
`backChannelLogout` (IdP- [...]
+- **`org.apache.juneau.rest.auth.oidc.rp.SessionStore`** + **`OidcSession`** 
&mdash; Pluggable session-persistence SPI and the immutable session record 
(principal + roles + tokens + `sub`/`sid` + expiry). The SPI exposes 
`supportsServerSideRevocation()` / `invalidateBySubject(String)` / 
`invalidateBySessionId(String)` so a server-side-indexed store can satisfy 
back-channel logout.
+- **`org.apache.juneau.rest.auth.oidc.rp.SignedCookieSessionStore`** &mdash; 
**The documented default.** Stateless HMAC-signed (HS256) compact-JWT cookie 
carrying a minimal claim set (never the raw tokens); scales horizontally and 
survives restart, size-capped (~4 KB), not server-revocable.
+- **`org.apache.juneau.rest.auth.oidc.rp.InMemorySessionStore`** &mdash; 
Single-instance / dev option; bounded-LRU map indexed by `sub` and `sid`, so it 
is the bundled store that supports back-channel logout.
+- **`org.apache.juneau.rest.auth.oidc.rp.EphemeralStore`** &mdash; Single-use, 
TTL-bounded (default 5&nbsp;min) `state` &rarr; `(nonce, codeVerifier, 
redirectTarget)` store with lazy + LRU sweep (the `BoundedLruTokenCache` 
eviction shape).
+- **`org.apache.juneau.rest.auth.oidc.rp.OidcSessionAuthFilter`** &mdash; 
`AuthFilter` (FINISHED-94a) that resolves the session cookie into a 
`ClaimsPrincipal` on each request; fail-open to unauthenticated. Roles come 
from a configurable claim (default `"scope"`), matching `OAuthFilter`'s 
convention.
+- **`org.apache.juneau.rest.auth.oidc.rp.IdTokenValidatorAdapter`** &mdash; 
Wraps Nimbus's `IDTokenValidator` for full OIDC ID-token validation (signature 
vs JWKS, `iss`, `aud`/`azp`, `exp`/`iat`, `nonce`) with a strict algorithm 
allowlist (default `[RS256, ES256]`; `none` rejected) and configurable JWKS 
source (URI / `JWKSet` / `JWKSource`).
+
+#### Building-block extension (`juneau-rest-server-oauth`)
+
+- **`OAuthAuthorizationCodeFlow.buildAuthenticationUrl(state, codeChallenge, 
nonce, customizer)`** &mdash; new method emitting an OIDC 
`AuthenticationRequest` (carrying the `nonce`) alongside the existing 
plain-OAuth `buildAuthorizationUrl(...)`. Required so the relying party can 
bind a `nonce` into the authorization request and verify it on the returned ID 
token.
+
+#### Security defaults
+
+- **`state` and `nonce` are single-use** (atomically consumed on the callback) 
and TTL-bounded; a missing / replayed value fails the callback.
+- **PKCE S256 mandatory** end-to-end; **strict ID-token validation** (`iss` 
exact, `aud` contains client id, `azp` on multi-audience, `nonce` match, 
`none`/SHA-1 rejected).
+- **Session-id rotation on login** (no pre-auth fixation); auth responses set 
`Cache-Control: no-store`; tokens never logged.
+- **Session cookie** is `HttpOnly` + `Secure` + `SameSite=Lax` by default; the 
post-login `redirect` parameter is restricted to safe app-relative paths 
(open-redirect defense).
+- **Back-channel logout** requires a server-side-revocable store; the 
stateless `SignedCookieSessionStore` throws rather than silently no-op'ing.
+
+#### Dependency
+
+```xml
+<dependency>
+    <groupId>org.apache.juneau</groupId>
+    <artifactId>juneau-rest-server-oidc-rp</artifactId>
+    <version>9.5.0</version>
+</dependency>
+<dependency>
+    <groupId>com.nimbusds</groupId>
+    <artifactId>oauth2-oidc-sdk</artifactId>
+    <version>11.37.2</version>          <!-- consumer-supplied; provided scope 
on juneau-rest-server-oidc-rp -->
+</dependency>
+```
+
 ### juneau-rest-server-mcp (new module)
 
 A new REST module, `juneau-rest-server-mcp`, exposes a stateless MCP JSON-RPC 
endpoint built on `juneau-rest-server` and the `juneau-bean-mcp` wire beans. 
The implementation is transport-agnostic at its core (a pure dispatcher seam) 
with two REST adapters: a drop-in servlet, and an interface mixin that mounts 
the endpoint on any existing `@Rest` resource.
@@ -4308,6 +4513,8 @@ public class ObservabilityConfig {
 
 Incoming `traceparent` / `tracestate` request headers are extracted via the 
configured `TextMapPropagator` (default: `W3CTraceContextPropagator`) so the 
server span continues a caller-supplied distributed trace. Downstream HTTP 
calls made from inside the handler pick up the active span's context 
automatically when the outbound HTTP client honors the OTel `Context.current()` 
(the standard OTel client instrumentations do).
 
+On the **outgoing** side (TODO-114), `OtelTracerHook.startSpan(...)` now also 
renders the W3C `traceparent` / `tracestate` values from the server-started 
span context at span-start time and stashes them as request attributes 
(`juneau.traceparent` / `juneau.tracestate`). The new 
`TraceContextResponseProcessor` in `juneau-rest-server` writes those onto the 
HTTP response so callers can read the resulting trace id back — see the 
[juneau-rest-server](#juneau-rest-server) entry above.
+
 #### Dependency
 
 ```xml
diff --git a/pages/topics/02.21.07.ValueFrameworkInternal.md 
b/pages/topics/02.21.07.ValueFrameworkInternal.md
index 9eab808bf5..06bebb731e 100644
--- a/pages/topics/02.21.07.ValueFrameworkInternal.md
+++ b/pages/topics/02.21.07.ValueFrameworkInternal.md
@@ -29,6 +29,8 @@ the [`@Value` annotation](./ValueAnnotationBasics.md) and 
`BeanInstantiator`:
 | `Microservice.Builder` | `juneau.workingDir` | `${juneau.workingDir}` (via 
`@Inject` initializer) |
 | `JettyServerComponent` | `availablePort`, `juneau.serverPort` | 
`${availablePort}` / `${juneau.serverPort}` (`Optional<String>` fields) |
 | `JwtTokenValidator.Builder` | `jwksCacheTtl` | 
`${juneau.jwt.jwksCacheTtl:PT5M}` |
+| `JwtTokenValidator.Builder` | `jwksEagerRefreshOnKidMiss` | 
`${juneau.jwt.jwksEagerRefreshOnKidMiss:true}` |
+| `JwtTokenValidator.Builder` | `jwksEagerRefreshCooldown` | 
`${juneau.jwt.jwksEagerRefreshCooldown:PT10S}` |
 | `BctConfiguration.Defaults` | `sortMaps`, `sortCollections` | concat-form: 
`${" + BCT_SORT_MAPS + ":false}` / `${" + BCT_SORT_COLLECTIONS + ":false}` |
 
 Every one of these sites used to call `env(...)` or `System.getProperty(...)` 
directly. The
diff --git a/pages/topics/10.20e.RestServerAuthGuards.md 
b/pages/topics/10.20e.RestServerAuthGuards.md
index bc22967973..765ab97896 100644
--- a/pages/topics/10.20e.RestServerAuthGuards.md
+++ b/pages/topics/10.20e.RestServerAuthGuards.md
@@ -137,6 +137,8 @@ var guard = 
BearerTokenGuard.create().realm("api").validator(validator).build();
 | `algorithms(JWSAlgorithm...)` | Algorithm allowlist. Default: `RS256, 
ES256`. `HS256` opt-in only; `"none"` is permanently rejected. |
 | `clockSkew(Duration)` | `exp` / `nbf` tolerance. Default: 60s. Capped at 5 
minutes by the builder.                          |
 | `jwksCacheTtl(Duration)` | JWKS cache TTL. Default: 5 minutes. Past TTL the 
cache serves stale keys on fetch failure (warn logged). |
+| `jwksEagerRefreshOnKidMiss(boolean)` | Trigger an out-of-band JWKS refresh 
when a fresh-cache key selection returns no results (key-rotation `kid` miss). 
Default: `true`. Set `false` to restore pre-9.5.0 behavior. Applies only to 
`jwksUrl(...)`-backed caches; see below. |
+| `jwksEagerRefreshCooldown(Duration)` | Minimum spacing between eager 
refreshes. Default: 10 seconds. Must be positive, ≤ `jwksCacheTtl`, and ≤ 60 
seconds. |
 | `clock(Clock)`        | Inject a deterministic clock for tests.              
                                                |
 
 ### Security defaults
@@ -148,6 +150,7 @@ var guard = 
BearerTokenGuard.create().realm("api").validator(validator).build();
 - **Mandatory claims** — `iss`, `aud`, `exp`, `nbf` are required by default. 
Missing-claim tokens are rejected.
 - **Clock-skew cap** — the builder will refuse a `clockSkew(...)` value larger 
than 5 minutes.
 - **JWKS rotation** — keys are re-fetched after the configured TTL (default 5 
minutes). On JWKS fetch failure the cache continues serving the last-known-good 
key set with a `WARNING`-level log entry, avoiding correlated auth outages from 
transient network blips.
+- **JWKS eager refresh on `kid` miss** — when a fresh-cache key selection 
returns no matching keys (typically because an IdP rotated its signing key 
mid-TTL), the cache performs one out-of-band JWKS refresh before failing the 
request. Bounded by a 10-second cooldown and a single-in-flight guard. Enabled 
by default; opt out with `jwksEagerRefreshOnKidMiss(false)`. Applies only to 
`jwksUrl(...)`-backed caches — a caller-supplied `jwkSource(...)` is unaffected.
 
 ## Composing with `BasicAdminResource`
 
diff --git a/pages/topics/10.20g.RestServerObservability.md 
b/pages/topics/10.20g.RestServerObservability.md
index 40671d97d4..3f90f40c22 100644
--- a/pages/topics/10.20g.RestServerObservability.md
+++ b/pages/topics/10.20g.RestServerObservability.md
@@ -276,6 +276,39 @@ public class ObservabilityConfig {
 
 The bridge declares `opentelemetry-api` in `provided` scope so the consumer 
picks the version. `mvn -pl juneau-rest/juneau-rest-server dependency:tree | 
grep -i opentelemetry` returns nothing — the containment requirement is 
verified at build time.
 
+## Outgoing-response trace-context headers
+
+The incoming-side propagation above continues an upstream trace into the 
server span. The **outgoing** side writes the W3C `traceparent` — and, when 
present, the `tracestate` — header back onto the HTTP **response**, so a client 
calling the server can read the resulting trace id off the response and 
correlate its own logs without running a tracing agent of its own:
+
+```text
+Response: traceparent: 00-0af7651916cd43dd8448eb211c80319c-<server-span-id>-01
+          tracestate:  vendor1=opaqueValue1,vendor2=opaqueValue2
+```
+
+The emitted `traceparent` reflects the **server-started span's** context (a 
freshly-minted span id parented to the inbound trace), not the inbound header 
verbatim — the same span the OTel bridge opened for the request. `tracestate` 
is written **only when the active trace state is non-empty**; an empty trace 
state never produces an empty `tracestate` header.
+
+### On-when-tracer, zero-cost otherwise
+
+Response-header injection is **on by default whenever a non-no-op `TracerHook` 
is active** — registering a `@Bean TracerHook` (e.g. `OtelTracerHook`) is the 
only opt-in needed; it requires no extra wiring beyond the tracer itself. On 
the no-tracer path it stays zero-cost: the response processor's first action is 
a single request-attribute read that short-circuits when no tracer stashed a 
trace context, so the off-by-default contract above is preserved end-to-end.
+
+### How it works
+
+The OTel bridge renders the W3C header values from the server-started span's 
context at span-start time — the only point where that context is reliably 
active — and stashes them as request attributes. A 
`TraceContextResponseProcessor` in `juneau-rest-server`'s response-processor 
chain then reads those stashed strings during response rendering (after the 
span scope has closed) and writes them as response headers. This split keeps 
the processor free of any OpenTelemetry dependency: it work [...]
+
+### Already-committed responses
+
+If the response has already been committed (for example a streaming / SSE 
handler that flushed its headers before returning), the headers can no longer 
be set. The processor logs a `FINE` message and skips the write — it never 
throws.
+
+### Opting out
+
+Set the `RestContext.responseTraceparent` environment / system property to 
`false` to keep the processor out of the chain entirely, even when a tracer is 
active:
+
+```bash
+-DRestContext.responseTraceparent=false
+```
+
+The default is `true` (on-when-tracer). A per-resource `@Rest`-annotation 
opt-out is tracked as a follow-on.
+
 ## Custom `MetricsRecorder` / `TracerHook` implementations
 
 The SPIs are deliberately tiny — implement them directly when you need a 
backend the shipped bridges don't cover (Dropwizard Metrics, an internal 
time-series store, a structured-log appender, …):
diff --git a/pages/topics/10.20i.AuthFilterFramework.md 
b/pages/topics/10.20i.AuthFilterFramework.md
index 870fd85f3a..f143013953 100644
--- a/pages/topics/10.20i.AuthFilterFramework.md
+++ b/pages/topics/10.20i.AuthFilterFramework.md
@@ -234,6 +234,7 @@ Spring Security's 
[`SecurityFilterChain`](https://docs.spring.io/spring-security
 - [AuthN Guards — Bearer / API-Key / JWT](/docs/topics/RestServerAuthGuards) — 
the FINISHED-69 op-level guards that compose with this framework.
 - [SAML 2.0 AuthN Support](/docs/topics/SamlAuthSupport) — the opt-in 
`juneau-rest-server-saml` module that adds a `SamlAuthFilter` implementation.
 - [OAuth 2.0 / OIDC AuthN Support](/docs/topics/OAuthAuthSupport) — the opt-in 
`juneau-rest-server-oauth` module that adds an `OAuthFilter` + introspection / 
OIDC discovery / grant-flow helpers.
+- [OIDC Relying Party Login](/docs/topics/OidcRelyingParty) — the opt-in 
`juneau-rest-server-oidc-rp` module whose `OidcSessionAuthFilter` resolves an 
OIDC login-session cookie into a `ClaimsPrincipal`.
 - 
[`AuthFilterChain`](/site/apidocs/org/apache/juneau/rest/auth/filter/AuthFilterChain.html)
 - 
[`AuthFilter`](/site/apidocs/org/apache/juneau/rest/auth/filter/AuthFilter.html)
 - 
[`BearerTokenAuthFilter`](/site/apidocs/org/apache/juneau/rest/auth/filter/BearerTokenAuthFilter.html)
diff --git a/pages/topics/10.20k.OAuthAuthSupport.md 
b/pages/topics/10.20k.OAuthAuthSupport.md
index fe0bb209df..e11449f1a1 100644
--- a/pages/topics/10.20k.OAuthAuthSupport.md
+++ b/pages/topics/10.20k.OAuthAuthSupport.md
@@ -212,12 +212,12 @@ The Nimbus SDK pulls `nimbus-jose-jwt` transitively, 
which is fine &mdash; `june
 
 ## Deferred
 
-- **OIDC Relying Party login flow** &mdash; deferred to a follow-on TODO 
(`juneau-rest-server-oidc-rp`). The discovery client + auth-code helper here 
are the building blocks the RP module will compose on top of.
 - **Device-code grant** (RFC 8628) &mdash; deferred. File an issue if needed.
-- **JWKS-on-`kid`-miss eager refresh** &mdash; deferred to a follow-on TODO 
targeting `juneau-rest-server-jwt`'s `JwksCache`.
+- **JWKS-on-`kid`-miss eager refresh** &mdash; shipped in 9.5.0 on 
`juneau-rest-server-jwt`'s `JwksCache`; see [REST Server &mdash; AuthN Guards 
&sect; JWKS eager refresh on kid miss](/docs/topics/RestServerAuthGuards).
 
 ## See also
 
+- [OIDC Relying Party Login](/docs/topics/OidcRelyingParty) &mdash; the 
`juneau-rest-server-oidc-rp` module that composes the discovery client + 
auth-code helper here into a full interactive browser-login flow (`startLogin` 
/ `completeLogin` / `logout`).
 - [AuthN Filter Framework](/docs/topics/AuthFilterFramework) &mdash; the SPI 
both this module's filter and the bundled bearer/api-key filters implement.
 - [AuthN Guards](/docs/topics/RestServerAuthGuards) &mdash; the op-level guard 
family (`ClaimsPrincipal`, `TokenValidator`, `JwtTokenValidator`) that composes 
with this framework.
 - [SAML 2.0 AuthN Support](/docs/topics/SamlAuthSupport) &mdash; the sibling 
SAML module.
diff --git a/pages/topics/10.20l.OidcRelyingParty.md 
b/pages/topics/10.20l.OidcRelyingParty.md
new file mode 100644
index 0000000000..eeb0e6b998
--- /dev/null
+++ b/pages/topics/10.20l.OidcRelyingParty.md
@@ -0,0 +1,167 @@
+---
+title: "OIDC Relying Party Login (juneau-rest-server-oidc-rp)"
+slug: OidcRelyingParty
+---
+
+The opt-in `juneau-rest-server-oidc-rp` module turns a Juneau REST server into 
a complete OpenID Connect **Relying Party** &mdash; the "Log in with Google / 
Okta / Entra ID / Keycloak" end-to-end glue. It orchestrates the interactive 
browser login dance (authorization redirect &rarr; callback &rarr; session) on 
top of the building blocks in 
[juneau-rest-server-oauth](/docs/topics/OAuthAuthSupport), wrapping the [Nimbus 
OAuth 2.0 SDK](https://connect2id.com/products/nimbus-oauth-openid-co [...]
+
+The Nimbus SDK is declared in `provided` scope on the module's POM so the 
dependency does **not** bleed into `juneau-rest-server` &mdash; consumers 
explicitly pick the SDK version they want (default pin: `11.37.2`, the same 
line as `juneau-rest-server-oauth`).
+
+This module owns only orchestration, the session SPI, and the single-use 
`state`/`nonce` store. Every flow / parse / validate step is offloaded to 
Nimbus; there is no bespoke crypto.
+
+## At a glance
+
+| Component | Purpose |
+|-----------|---------|
+| 
[`OidcRelyingParty`](/site/apidocs/org/apache/juneau/rest/auth/oidc/rp/OidcRelyingParty.html)
 | Builder + facade: `startLogin` / `completeLogin` / `logout` / `refresh` / 
`backChannelLogout`. |
+| 
[`SessionStore`](/site/apidocs/org/apache/juneau/rest/auth/oidc/rp/SessionStore.html)
 | SPI for persisting sessions; `createSessionCookieValue` / `lookup` / 
`invalidate` + back-channel revocation hooks. |
+| 
[`OidcSession`](/site/apidocs/org/apache/juneau/rest/auth/oidc/rp/OidcSession.html)
 | Immutable record: principal + roles + tokens + `sub`/`sid` + expiry. |
+| 
[`SignedCookieSessionStore`](/site/apidocs/org/apache/juneau/rest/auth/oidc/rp/SignedCookieSessionStore.html)
 | **The documented default.** Stateless HMAC-signed cookie; scales 
horizontally, survives restart. |
+| 
[`InMemorySessionStore`](/site/apidocs/org/apache/juneau/rest/auth/oidc/rp/InMemorySessionStore.html)
 | Single-instance / dev option. Server-side indexed by `sub`/`sid` &mdash; the 
one that supports back-channel logout. |
+| 
[`EphemeralStore`](/site/apidocs/org/apache/juneau/rest/auth/oidc/rp/EphemeralStore.html)
 | Single-use, TTL-bounded `state` &rarr; `(nonce, codeVerifier, 
redirectTarget)` store. |
+| 
[`OidcSessionAuthFilter`](/site/apidocs/org/apache/juneau/rest/auth/oidc/rp/OidcSessionAuthFilter.html)
 | `AuthFilter` that resolves the session cookie into a `ClaimsPrincipal` on 
each request. |
+| 
[`IdTokenValidatorAdapter`](/site/apidocs/org/apache/juneau/rest/auth/oidc/rp/IdTokenValidatorAdapter.html)
 | Wraps Nimbus's `IDTokenValidator` (signature + 
`iss`/`aud`/`azp`/`exp`/`nonce`). |
+
+## Wiring the endpoints
+
+The module owns the request-time `OidcSessionAuthFilter`, but the login / 
callback / logout endpoints are **explicit `@RestGet` methods you mount** 
&mdash; there are no auto-mounted servlet routes, so URL ownership stays with 
your application.
+
+```java
+@Rest(path="/auth")
+public class LoginResource extends BasicRestServlet {
+
+    private final OidcRelyingParty rp = OidcRelyingParty.create()
+        .issuer(URI.create("https://accounts.google.com";))   // 
OidcDiscoveryClient under the hood
+        .clientId("web-app")
+        .clientSecret(env("OIDC_CLIENT_SECRET"))
+        .redirectUri(URI.create("https://app.example.com/auth/callback";))
+        .scope("openid", "profile", "email")
+        
.sessionStore(SignedCookieSessionStore.create().signingKey(env("SESSION_KEY")).build())
+        .build();
+
+    @RestGet(path="/login")
+    public void login(RestRequest req, RestResponse res) throws Exception {
+        rp.startLogin(req, res);     // generate state+nonce+PKCE, store 
single-use, 302 → IdP
+    }
+
+    @RestGet(path="/callback")
+    public void callback(RestRequest req, RestResponse res) throws Exception {
+        rp.completeLogin(req, res);  // verify state+nonce, exchange code, 
validate ID token, create session, 302 → app
+    }
+
+    @RestGet(path="/logout")
+    public void logout(RestRequest req, RestResponse res) throws Exception {
+        rp.logout(req, res);         // clear session + cookie, 302 → IdP 
end_session_endpoint
+    }
+}
+```
+
+`RestRequest` / `RestResponse` are accepted directly because they implement 
`HttpServletRequest` / `HttpServletResponse`.
+
+## Resolving the identity on each request
+
+Register `rp.authFilter()` in your filter chain so each request resolves the 
session cookie into a `ClaimsPrincipal`. The resolved identity rides on 
[`AuthenticatedRequestWrapper`](/docs/topics/AuthFilterFramework), so 
`RoleBasedRestGuard`, `@RestGet(roleGuard=...)`, and `@Auth Principal` 
injection all keep working with zero downstream changes.
+
+```java
+@Bean
+public AuthFilterChain authFilters(BeanStore bs) {
+    return AuthFilterChain.create(bs)
+        .append(rp.authFilter())     // OidcSessionAuthFilter — cookie → 
ClaimsPrincipal
+        .build();
+}
+```
+
+Resolution is **fail-open to unauthenticated** (not `401`): a request with no 
session cookie, or an expired / tampered cookie, passes through so your app's 
normal "not logged in" handling (typically a redirect to `/auth/login`) applies.
+
+Roles are extracted from a configurable claim (default `"scope"`, 
whitespace-split; array claims also accepted) &mdash; identical to 
[`OAuthFilter`](/docs/topics/OAuthAuthSupport)'s convention, so guard semantics 
match across the OAuth and OIDC-RP filters. Override with 
`.rolesClaim("groups")`.
+
+## Session stores
+
+| Store | Scales horizontally | Survives restart | Server-revocable 
(back-channel logout) |
+|-------|:---:|:---:|:---:|
+| `SignedCookieSessionStore` (default) | yes | yes | **no** |
+| `InMemorySessionStore` | no (needs sticky sessions) | no | **yes** |
+| Caller-supplied distributed store (Redis / JDBC) via the SPI | yes | yes | 
yes (if implemented) |
+
+`SignedCookieSessionStore` serializes a minimal claim set (subject, name, 
roles, `sid`, principal claims, expiry) into an HMAC-signed compact JWT &mdash; 
**never the raw access / refresh / ID tokens.** The payload is size-capped (~4 
KB); an over-cap payload throws rather than silently truncating. Because the 
session lives entirely client-side it cannot be force-revoked, so it does 
**not** support back-channel logout.
+
+```java
+// Stateless default (HS256; signing key must be ≥ 32 bytes):
+SignedCookieSessionStore.create().signingKey(env("SESSION_KEY")).build();
+
+// Server-side indexed (dev / single-instance / back-channel-logout-capable):
+InMemorySessionStore.create();
+```
+
+## Security
+
+The module is opt-in, security-reviewed glue. The defaults are fail-closed:
+
+- **Single-use `state` + `nonce`** &mdash; generated before the redirect, 
stored TTL-bounded (default 5&nbsp;min), and atomically consumed on the 
callback. A missing / replayed `state` fails the callback (CSRF + 
ID-token-replay defense).
+- **PKCE S256** is enforced end-to-end (verifier persisted across the 
redirect, used at exchange).
+- **Strict ID-token validation** &mdash; signature against the IdP JWKS, exact 
`iss` match, `aud` contains the client id, `azp` on multi-audience tokens, 
`exp`/`iat` within clock skew, and `nonce` match. The signing-algorithm 
allowlist defaults to `[RS256, ES256]`; `none` and SHA-1-family algorithms are 
rejected (inheriting the `juneau-rest-server-jwt` strict-default stance).
+- **Session-id rotation** &mdash; a fresh session id is generated on login; 
there is no pre-auth fixation window.
+- **Token redaction** &mdash; auth responses set `Cache-Control: no-store`; 
tokens are never logged.
+- **Cookie flags** &mdash; the session cookie is `HttpOnly` + `Secure` + 
`SameSite=Lax` by default (override via the builder).
+- **Open-redirect defense** &mdash; the post-login `redirect` parameter is 
honored only when it is a safe app-relative path; absolute / protocol-relative 
targets are dropped.
+
+## Refresh-token rotation
+
+For long-lived sessions backed by a token-retaining store, `rp.refresh(req, 
res)` exchanges the stored refresh token via `OAuthRefreshTokenFlow`, replaces 
the stored refresh token with the rotated one the IdP returns, and resets the 
session cookie. If the IdP rejects the refresh (a rotated token was reused / 
revoked), the session is invalidated (fail-closed). Session lifetime is bounded 
independently of token lifetime. (`SignedCookieSessionStore` retains no tokens, 
so `refresh` is a no-o [...]
+
+## Back-channel logout
+
+`rp.backChannelLogout(logoutToken)` accepts an IdP-pushed OpenID Connect 
`logout_token`, validates it via Nimbus's `LogoutTokenValidator` (signature, 
`iss`, `aud`, the back-channel `events` claim, and the `sub`/`sid` rule), and 
invalidates the matching server-side session(s) &mdash; by `sid` (single 
session) when present, otherwise by `sub` (all of a subject's sessions).
+
+```java
+@RestPost(path="/backchannel-logout")
+public void backchannelLogout(@Content("logout_token") String logoutToken) {
+    rp.backChannelLogout(logoutToken);   // returns the number of sessions 
invalidated
+}
+```
+
+Back-channel logout **requires a server-side-revocable store** &mdash; a 
stateless cookie is not server-revocable, so the default 
`SignedCookieSessionStore` throws `IllegalStateException` here. Use 
`InMemorySessionStore` or a caller-supplied distributed store (which must 
report `supportsServerSideRevocation()` as `true` and implement 
`invalidateBySubject` / `invalidateBySessionId`).
+
+## Escape hatches
+
+Nimbus is hidden behind the Juneau facades, but two escape hatches remain for 
advanced configuration:
+
+- `httpRequestConfigurator(Consumer<HTTPRequest>)` &mdash; proxy / TLS / 
timeout tuning applied to discovery, token, and userinfo calls.
+- `authenticationRequestCustomizer(Consumer<AuthenticationRequest.Builder>)` 
&mdash; set OIDC parameters such as `prompt`, `max_age`, or `acr_values` on the 
authorization redirect.
+
+By default the `Principal` is built from **ID-token claims** (no extra 
round-trip). Configure `userInfoClaims("email", ...)` to backfill specific 
claims from the UserInfo endpoint only when they are absent from the ID token.
+
+## Single-IdP per instance
+
+An `OidcRelyingParty` instance is bound to a single IdP. Multi-IdP support is 
achievable by instantiating several relying parties behind your own selector; 
federation / issuer-discovery across many IdPs is out of scope.
+
+## Containment verification
+
+```text
+mvn -pl juneau-rest/juneau-rest-server dependency:tree | grep -iE 
"(nimbusds|oauth2-oidc)"
+# returns nothing — provided scope contains the dep to this module only.
+```
+
+## Maven dependency
+
+```xml
+<dependency>
+    <groupId>org.apache.juneau</groupId>
+    <artifactId>juneau-rest-server-oidc-rp</artifactId>
+    <version>9.5.0</version>
+</dependency>
+<dependency>
+    <groupId>com.nimbusds</groupId>
+    <artifactId>oauth2-oidc-sdk</artifactId>
+    <version>11.37.2</version>          <!-- provided scope; consumer-supplied 
-->
+</dependency>
+```
+
+The module also pulls `juneau-rest-server-oauth` (its building-block 
dependency) transitively at compile scope.
+
+## See also
+
+- [OAuth 2.0 / OIDC AuthN Support](/docs/topics/OAuthAuthSupport) &mdash; the 
building-block module (`OidcDiscoveryClient`, `OAuthAuthorizationCodeFlow`, 
`OAuthRefreshTokenFlow`) this RP composes on top of.
+- [AuthN Filter Framework](/docs/topics/AuthFilterFramework) &mdash; the 
`AuthFilter` / `AuthFilterChain` / `AuthenticatedRequestWrapper` SPI the RP 
filter plugs into.
+- [AuthN Guards](/docs/topics/RestServerAuthGuards) &mdash; `ClaimsPrincipal`, 
`JwtTokenValidator`, and the op-level guard family.
+- [OpenID Connect Core 
1.0](https://openid.net/specs/openid-connect-core-1_0.html), [OpenID Connect 
Back-Channel Logout 
1.0](https://openid.net/specs/openid-connect-backchannel-1_0.html), [RFC 7636 
(PKCE)](https://datatracker.ietf.org/doc/html/rfc7636).

Reply via email to