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`) —
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** — 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) — deferred to a follow-on TODO
if/when needed.
-- **JWKS-on-`kid`-miss eager refresh** — deferred to a follow-on TODO
targeting `juneau-rest-server-jwt`'s `JwksCache`.
+- **JWKS-on-`kid`-miss eager refresh** — 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** — the interactive
"Log in with Google / Okta / Entra ID / Keycloak" browser flow — on top
of `juneau-rest-server-oauth`. It orchestrates the authorization redirect
→ callback → 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 — the containment requirement
is verified at build time. See [REST Server — OIDC Relying Party
Login](/docs/topics/OidcRelyingParty) for the full reference.
+
+#### New Classes
+
+- **`org.apache.juneau.rest.auth.oidc.rp.OidcRelyingParty`** — 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`**
— 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`** —
**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`** —
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`** — Single-use,
TTL-bounded (default 5 min) `state` → `(nonce, codeVerifier,
redirectTarget)` store with lazy + LRU sweep (the `BoundedLruTokenCache`
eviction shape).
+- **`org.apache.juneau.rest.auth.oidc.rp.OidcSessionAuthFilter`** —
`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`** —
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)`** — 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 — `june
## Deferred
-- **OIDC Relying Party login flow** — 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) — deferred. File an issue if needed.
-- **JWKS-on-`kid`-miss eager refresh** — deferred to a follow-on TODO
targeting `juneau-rest-server-jwt`'s `JwksCache`.
+- **JWKS-on-`kid`-miss eager refresh** — shipped in 9.5.0 on
`juneau-rest-server-jwt`'s `JwksCache`; see [REST Server — AuthN Guards
§ JWKS eager refresh on kid miss](/docs/topics/RestServerAuthGuards).
## See also
+- [OIDC Relying Party Login](/docs/topics/OidcRelyingParty) — 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) — the SPI
both this module's filter and the bundled bearer/api-key filters implement.
- [AuthN Guards](/docs/topics/RestServerAuthGuards) — the op-level guard
family (`ClaimsPrincipal`, `TokenValidator`, `JwtTokenValidator`) that composes
with this framework.
- [SAML 2.0 AuthN Support](/docs/topics/SamlAuthSupport) — 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** — the "Log in with Google /
Okta / Entra ID / Keycloak" end-to-end glue. It orchestrates the interactive
browser login dance (authorization redirect → callback → 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` — 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` — the
one that supports back-channel logout. |
+|
[`EphemeralStore`](/site/apidocs/org/apache/juneau/rest/auth/oidc/rp/EphemeralStore.html)
| Single-use, TTL-bounded `state` → `(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**
— 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) — 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 —
**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`** — generated before the redirect,
stored TTL-bounded (default 5 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** — 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** — a fresh session id is generated on login;
there is no pre-auth fixation window.
+- **Token redaction** — auth responses set `Cache-Control: no-store`;
tokens are never logged.
+- **Cookie flags** — the session cookie is `HttpOnly` + `Secure` +
`SameSite=Lax` by default (override via the builder).
+- **Open-redirect defense** — 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) — 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** — 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>)` — proxy / TLS /
timeout tuning applied to discovery, token, and userinfo calls.
+- `authenticationRequestCustomizer(Consumer<AuthenticationRequest.Builder>)`
— 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) — the
building-block module (`OidcDiscoveryClient`, `OAuthAuthorizationCodeFlow`,
`OAuthRefreshTokenFlow`) this RP composes on top of.
+- [AuthN Filter Framework](/docs/topics/AuthFilterFramework) — the
`AuthFilter` / `AuthFilterChain` / `AuthenticatedRequestWrapper` SPI the RP
filter plugs into.
+- [AuthN Guards](/docs/topics/RestServerAuthGuards) — `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).