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 f1883efcc8 docs: release notes + Auth filter / SAML / OAuth topic 
pages (TODO-94a/94b/94c)
f1883efcc8 is described below

commit f1883efcc8e689ce1961e62a80c4666afb9bd551
Author: James Bognar <[email protected]>
AuthorDate: Thu May 28 07:55:46 2026 -0400

    docs: release notes + Auth filter / SAML / OAuth topic pages 
(TODO-94a/94b/94c)
    
    Co-authored-by: Cursor <[email protected]>
---
 pages/release-notes/9.5.0.md                | 124 ++++++++++++++
 pages/topics/10.20e.RestServerAuthGuards.md |   2 +-
 pages/topics/10.20i.AuthFilterFramework.md  | 241 ++++++++++++++++++++++++++++
 pages/topics/10.20j.SamlAuthSupport.md      | 139 ++++++++++++++++
 pages/topics/10.20k.OAuthAuthSupport.md     | 224 ++++++++++++++++++++++++++
 5 files changed, 729 insertions(+), 1 deletion(-)

diff --git a/pages/release-notes/9.5.0.md b/pages/release-notes/9.5.0.md
index 8b3e5592c8..674b95130f 100644
--- a/pages/release-notes/9.5.0.md
+++ b/pages/release-notes/9.5.0.md
@@ -2371,6 +2371,20 @@ A new JUnit 5 extension and `@TestBean` annotation 
enable Spring-style test-time
 - **`AuthArg` registered in `DefaultConfig.restOpArgs`** so type-driven 
`Principal` injection works out of the box.
 - **Composition with `BasicAdminResource`.** Swapping the mixin's default 
`DenyAllGuard` for a `BearerTokenGuard` is a zero-mixin-source-change migration 
— a single `@Bean RestGuardList adminGuards(...)` on the host wins via the 
existing guard-override seam (see `BasicAdminResource_AuthIntegration_Test`).
 
+#### Auth Filter Framework — Servlet-Layer AuthN (TODO-94a)
+
+`juneau-rest-server` now ships a pluggable servlet-filter authentication 
framework in the new `org.apache.juneau.rest.auth` package. Filters run at the 
**container layer** — before `RestServlet` routing — and expose the resolved 
identity via standard `HttpServletRequest` overrides so downstream 
`RoleBasedRestGuard` and `@Auth Principal` injection work with zero changes. 
See [AuthN Filter Framework](/docs/topics/AuthFilterFramework) for the full 
reference.
+
+- **`org.apache.juneau.rest.auth.AuthFilter`** — abstract base class 
implementing `jakarta.servlet.Filter`. Concrete subclasses implement 
`Optional<AuthResult> authenticate(HttpServletRequest req) throws 
AuthenticationException`. Three states: `Optional.empty()` = no credentials for 
this filter (pass through), throw `AuthenticationException` = invalid 
credentials (abort with 401), `Optional.of(AuthResult)` = success.
+- **`org.apache.juneau.rest.auth.AuthResult`** — immutable value type holding 
the resolved `Principal` and an unmodifiable `Set<String>` of roles. Factory 
methods: `AuthResult.of(Principal, String...)` and `AuthResult.of(Principal, 
Set<String>)`.
+- **`org.apache.juneau.rest.auth.AuthenticatedRequestWrapper`** (public) — 
`HttpServletRequestWrapper` that overrides `getUserPrincipal()`, 
`isUserInRole(String)`, `getRemoteUser()`, and `getAttribute(PRINCIPAL_ATTR)` 
to reflect the filter-resolved identity. Roles are the union of all successful 
filters in chain mode.
+- **`org.apache.juneau.rest.auth.AuthFilterChain`** — `jakarta.servlet.Filter` 
orchestrator. Each entry carries an `AuthFilter` and an optional 
`UrlPathMatcher` pattern. `doFilter(...)` semantics: select matching filters, 
iterate in declaration order, first-success principal wins, all-success roles 
aggregate, all-failure exceptions aggregate into a single `401` with combined 
`WWW-Authenticate`. If no filter matches the path, or all matching filters 
return `Optional.empty()`, the request  [...]
+  - Builder: 
`AuthFilterChain.create(BeanStore).append(AuthFilter).append(AuthFilter, String 
pattern).build()`.
+- **`org.apache.juneau.rest.auth.BearerTokenAuthFilter`** — concrete 
`AuthFilter` that extracts `Authorization: Bearer <token>`, delegates to a 
`TokenValidator`, and extracts roles from a `ClaimsPrincipal` claim (default: 
`"roles"`). Builder: 
`BearerTokenAuthFilter.create().validator(TokenValidator).realm(String).rolesClaim(String).build()`.
+- **`org.apache.juneau.rest.auth.ApiKeyAuthFilter`** — concrete `AuthFilter` 
that reads an API key from a header / query param / cookie (configurable; 
default: `X-API-Key` header), delegates to an `ApiKeyStore`, and extracts roles 
from a `ClaimsPrincipal` claim. Builder: 
`ApiKeyAuthFilter.create().store(ApiKeyStore).fromHeader(String).fromQuery(String).fromCookie(String).realm(String).rolesClaim(String).build()`.
+- **`@Bean AuthFilterChain` auto-mount** — `JettyServerComponent.onStart(...)` 
scans the `BeanStore` for an `AuthFilterChain` bean and registers it at `/*` 
before any servlet is mounted. No explicit `addFilter(...)` call required for 
the common case.
+- **Spring Boot note** — Spring Security's `SecurityFilterChain` is the 
recommended Spring path. `BearerTokenAuthFilter` / `ApiKeyAuthFilter` can be 
registered via `FilterRegistrationBean` if desired; the `@Bean` auto-mount is 
Jetty-only.
+
 #### Rate-Limit Guard + Request-Id Filter (TODO-66)
 
 `juneau-rest-server` now ships two opt-in operational primitives — both purely 
additive, both wired through existing `@Bean` / `@RestStartCall` extension 
points. See [REST Server — Rate-Limiting and Request-Id 
Propagation](/docs/topics/RestServerRateLimitAndRequestId) for the full 
reference.
@@ -3584,6 +3598,19 @@ public View hello(@Path String name) {
 
 ### juneau-microservice-jetty
 
+#### `JettyServerComponent.addFilter(...)` + `AuthFilterChain` auto-mount 
(TODO-94a)
+
+`JettyServerComponent` now exposes two public filter-registration methods:
+
+```java
+public JettyServerComponent addFilter(Filter filter, String urlPattern) { ... }
+public JettyServerComponent addFilter(Filter filter, String... urlPatterns) { 
... }
+```
+
+Both delegate to `ServletContextHandler.addFilter(FilterHolder, String, 
EnumSet<DispatcherType>)` for `DispatcherType.REQUEST` dispatches. Filters 
registered via these methods are mounted before any servlet, preserving 
filter-before-routing semantics.
+
+`onStart(Microservice)` now scans the `BeanStore` for an `AuthFilterChain` 
bean and auto-registers it at `/*` before servlet discovery — no explicit 
`addFilter(...)` call is needed when the chain is declared as a `@Bean`.
+
 #### `JettyServerComponent` env reads migrated to `@Value` (TODO-92)
 
 - `JettyServerComponent` previously called `env("availablePort")` and 
`env("juneau.serverPort")`
@@ -4010,6 +4037,103 @@ A new opt-in REST module, `juneau-rest-server-jwt`, 
adds JWT bearer-token verifi
 </dependency>
 ```
 
+### juneau-rest-server-saml (new module)
+
+A new opt-in REST module, `juneau-rest-server-saml`, adds SAML 2.0 
single-sign-on (Web Browser SSO Profile) to `juneau-rest-server` by wrapping 
[OpenSAML 5.x](https://shibboleth.atlassian.net/wiki/spaces/OS30) behind the 
FINISHED-94a `AuthFilter` / `TokenValidator` SPIs. The 
`org.opensaml:opensaml-*` dependency cluster is declared in `provided` scope on 
the module's POM, so consumers explicitly pick the OpenSAML 5.x patch they want 
(default pin: `5.2.2`).
+
+`mvn -pl juneau-rest/juneau-rest-server dependency:tree | grep -i opensaml` 
returns nothing &mdash; the containment requirement is verified at build time. 
See [REST Server &mdash; SAML AuthN Support](/docs/topics/SamlAuthSupport) for 
the full reference.
+
+#### New Classes
+
+- **`org.apache.juneau.rest.auth.saml.SamlAssertionValidator`** &mdash; 
Validates a SAML 2.0 `<samlp:Response>` document and returns a 
[`ClaimsPrincipal`](/docs/topics/AuthGuards) marked with `issuerType=SAML`. 
Builder: `spEntityId(String)`, `expectedIssuer(String)`, 
`metadataResolver(MetadataResolver)` or `signingCredential(Credential)` 
(mutually exclusive), `decryptionCredential(Credential)` (opt-in for encrypted 
assertions), `algorithms(String...)` (default `[rsa-sha256, ecdsa-sha256] [...]
+- **`org.apache.juneau.rest.auth.saml.SamlAuthFilter`** &mdash; `AuthFilter` 
that decodes the inbound `SAMLResponse` (base64 + DEFLATE for the Redirect 
binding) at a configurable consumer path and delegates validation. Builder: 
`validator(SamlAssertionValidator)`, `binding(SamlBinding)` (default `POST`), 
`consumerPath(String)` (default `/saml/acs`), `rolesClaim(String)` (default 
`"roles"`), `realm(String)` (default `"saml"`).
+- **`org.apache.juneau.rest.auth.saml.SamlBinding`** &mdash; Enum of supported 
HTTP bindings (`POST`, `REDIRECT`). Artifact + SOAP bindings deferred to a 
future iteration.
+- **`org.apache.juneau.rest.auth.saml.SamlMetadataResolvers`** &mdash; 
Convenience factories `file(File|Path)` and `url(String)` that produce 
initialized `MetadataResolver` instances ready to hand to 
`SamlAssertionValidator.Builder.metadataResolver(...)`. No bundled default 
singleton.
+
+#### Security defaults
+
+- **Strict signature-algorithm allowlist** &mdash; only 
`http://www.w3.org/2001/04/xmldsig-more#rsa-sha256` and 
`http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256` are accepted by default. 
SHA-1-based algorithms are permanently rejected; the builder will refuse a 
`algorithms(...)` value containing any SHA-1 URI.
+- **Mandatory signature on every assertion** &mdash; unsigned assertions are 
rejected.
+- **`SAMLSignatureProfileValidator`** &mdash; runs before crypto verification 
to catch wrapping / structural attacks.
+- **Mandatory audience restriction** &mdash; `<AudienceRestriction>` must list 
the configured SP entity ID.
+- **Clock-skew cap** &mdash; the builder refuses a `clockSkew(...)` value 
larger than 5 minutes (300s).
+- **Encrypted assertions are explicit** &mdash; a response carrying 
`<EncryptedAssertion>` is rejected with a `decryption_required` challenge when 
no `decryptionCredential(...)` is configured; a wrong key yields 
`decryption_failed` (never a bogus principal).
+
+#### Marker claim
+
+The returned `ClaimsPrincipal` carries `issuerType=SAML`, so downstream code 
can distinguish SAML-derived principals from JWT-derived principals without a 
subclass:
+
+```java
+if ("SAML".equals(principal.getClaim("issuerType", 
String.class).orElse(null))) {
+    // SAML-issued principal
+}
+```
+
+#### Maven note
+
+OpenSAML 5.x is published to the [Shibboleth Maven 
repository](https://build.shibboleth.net/maven/releases/) rather than Maven 
Central. The `juneau-rest-server-saml` POM declares that repository locally so 
consumers don't need to configure it in their own settings. Pin the version via 
the `<opensaml.version>` POM property (default `5.2.2`; do not use `5.0.0`).
+
+#### Dependency
+
+```xml
+<dependency>
+    <groupId>org.apache.juneau</groupId>
+    <artifactId>juneau-rest-server-saml</artifactId>
+    <version>9.5.0</version>
+</dependency>
+<dependency>
+    <groupId>org.opensaml</groupId>
+    <artifactId>opensaml-saml-impl</artifactId>
+    <version>5.2.2</version>          <!-- consumer-supplied; provided scope 
on juneau-rest-server-saml -->
+</dependency>
+```
+
+### juneau-rest-server-oauth (new module)
+
+A new opt-in REST module, `juneau-rest-server-oauth`, adds OAuth 2.0 / OIDC 
bearer-token validation, RFC 7662 token introspection, RFC 6749 client-side 
grant flows, and OIDC discovery to `juneau-rest-server` by wrapping the [Nimbus 
OAuth 2.0 SDK](https://connect2id.com/products/nimbus-oauth-openid-connect-sdk) 
(`com.nimbusds:oauth2-oidc-sdk`) behind the FINISHED-94a `AuthFilter` / 
`TokenValidator` SPIs. Same vendor as the `nimbus-jose-jwt` already used by 
`juneau-rest-server-jwt` &mdash; [...]
+
+`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; OAuth AuthN 
Support](/docs/topics/OAuthAuthSupport) for the full reference.
+
+#### New Classes
+
+- **`org.apache.juneau.rest.auth.oauth.OAuthIntrospectionValidator`** &mdash; 
`TokenValidator` that validates opaque OAuth 2.0 tokens via RFC 7662 
introspection. Wraps Nimbus's `TokenIntrospectionRequest` + 
`TokenIntrospectionResponse`. Builder: `introspectionEndpoint(URI)`, 
`clientId(String)`, `clientSecret(String)` / 
`clientSecretSupplier(Supplier<String>)`, `requiredScopes(String...)`, 
`tokenCache(TokenCache)` (default `BoundedLruTokenCache`), `cacheTtl(Duration)` 
(default 5m, capped  [...]
+- **`org.apache.juneau.rest.auth.oauth.OAuthFilter`** &mdash; `AuthFilter` 
that authenticates RFC 6750 `Bearer` tokens, delegating to any `TokenValidator` 
(e.g. `OAuthIntrospectionValidator` for opaque tokens or `JwtTokenValidator` 
from `juneau-rest-server-jwt` for JWT access tokens). Extracts roles from a 
configurable claim (default `"scope"`, split on whitespace per RFC 6749 
&sect;3.3).
+- **`org.apache.juneau.rest.auth.oauth.OAuthToken`** &mdash; Immutable record 
returned by every flow helper on a successful token acquisition.
+- **`org.apache.juneau.rest.auth.oauth.TokenCache`** + 
**`BoundedLruTokenCache`** &mdash; SPI for caching principals + tokens; default 
impl is a thread-safe bounded LRU (1000 entries, per-entry TTL).
+- **`org.apache.juneau.rest.auth.oauth.flow.OAuthClientCredentialsFlow`** 
&mdash; Wraps Nimbus's `ClientCredentialsGrant` (RFC 6749 &sect;4.4) with 
optional `TokenCache` reuse keyed by `(clientId, scope)`.
+- **`org.apache.juneau.rest.auth.oauth.flow.OAuthAuthorizationCodeFlow`** 
&mdash; Wraps Nimbus's `AuthorizationRequest` + `AuthorizationCodeGrant` + 
`TokenRequest` (RFC 6749 &sect;4.1) with mandatory PKCE per RFC 7636 (S256). 
Exposes `buildAuthorizationUrl(state, codeChallenge)` and `exchange(code, 
codeVerifier)`.
+- **`org.apache.juneau.rest.auth.oauth.flow.OAuthRefreshTokenFlow`** &mdash; 
Wraps Nimbus's `RefreshTokenGrant` (RFC 6749 &sect;6).
+- **`org.apache.juneau.rest.auth.oauth.flow.OAuthResourceOwnerFlow`** &mdash; 
Wraps Nimbus's `ResourceOwnerPasswordCredentialsGrant` (RFC 6749 &sect;4.3). 
**Filed `@Deprecated(since = "9.5.0")` from day-1** &mdash; the resource-owner 
password-credentials grant was removed from OAuth 2.1 due to long-standing 
security concerns; prefer `OAuthAuthorizationCodeFlow` (with PKCE).
+- **`org.apache.juneau.rest.auth.oauth.oidc.OidcDiscoveryClient`** + 
**`OidcMetadata`** &mdash; OIDC `.well-known/openid-configuration` discovery 
via Nimbus's `OIDCProviderMetadata.resolve(Issuer)`. Returns a Juneau-native 
immutable record.
+
+#### Security defaults
+
+- **`requiredScopes` enforcement lives on the validator,** not on the filter 
(per OQA Q6) &mdash; centralizes "what the caller is allowed to do" with the 
introspection result.
+- **Token cache** defaults &mdash; 5-minute TTL, capped at 1 hour, 1000-entry 
LRU eviction (per OQA Q1).
+- **PKCE is mandatory** on `OAuthAuthorizationCodeFlow` &mdash; the builder 
does not expose a "PKCE off" toggle.
+- **OAuth flow errors are mapped to `OAuthFlowException`** (unchecked); the 
offending Nimbus error code is preserved on the cause chain.
+
+#### 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.
+- **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`.
+
+#### Dependency
+
+```xml
+<dependency>
+    <groupId>org.apache.juneau</groupId>
+    <artifactId>juneau-rest-server-oauth</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-oauth -->
+</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.
diff --git a/pages/topics/10.20e.RestServerAuthGuards.md 
b/pages/topics/10.20e.RestServerAuthGuards.md
index 9f181135f3..bc22967973 100644
--- a/pages/topics/10.20e.RestServerAuthGuards.md
+++ b/pages/topics/10.20e.RestServerAuthGuards.md
@@ -189,4 +189,4 @@ public RestGuardList guards(BeanStore bs) {
 - **Constant-time comparison.** API-key stores that compare strings naively 
are vulnerable to timing attacks. Use `MessageDigest#isEqual`.
 - **Replay-attack mitigation.** Bearer tokens and JWTs are bearer tokens by 
definition. Pair short `exp` values with TLS pinning and (when feasible) DPoP / 
mutual-TLS for sensitive endpoints.
 
-See also: [Guards](/docs/topics/Guards), [Rate-Limiting and Request-Id 
Propagation](/docs/topics/RestServerRateLimitAndRequestId), 
[`BasicAdminResource`](/site/apidocs/org/apache/juneau/rest/convention/BasicAdminResource.html).
+See also: [Guards](/docs/topics/Guards), [Rate-Limiting and Request-Id 
Propagation](/docs/topics/RestServerRateLimitAndRequestId), [AuthN Filter 
Framework](/docs/topics/AuthFilterFramework), [SAML 2.0 AuthN 
Support](/docs/topics/SamlAuthSupport), [OAuth 2.0 / OIDC AuthN 
Support](/docs/topics/OAuthAuthSupport), 
[`BasicAdminResource`](/site/apidocs/org/apache/juneau/rest/convention/BasicAdminResource.html).
diff --git a/pages/topics/10.20i.AuthFilterFramework.md 
b/pages/topics/10.20i.AuthFilterFramework.md
new file mode 100644
index 0000000000..870fd85f3a
--- /dev/null
+++ b/pages/topics/10.20i.AuthFilterFramework.md
@@ -0,0 +1,241 @@
+---
+title: "AuthN Filter Framework — Servlet-Layer Authentication"
+slug: AuthFilterFramework
+---
+
+The `org.apache.juneau.rest.auth` package provides a pluggable servlet-filter 
authentication framework that runs **at the servlet container layer** — before 
Juneau's request routing — so a single URL can support multiple overlapping 
authentication schemes, short-circuit on the first success, and aggregate roles 
when more than one scheme succeeds.
+
+This framework is the natural peer of the [AuthN 
Guards](/docs/topics/RestServerAuthGuards) that ship in FINISHED-69: guards run 
inside `RestContext.handleCall(...)` (op-level AuthZ); filters run before the 
servlet is reached (container-level AuthN).
+
+## At a glance
+
+| Component | Module | Purpose |
+|-----------|--------|---------|
+| `AuthFilter` (abstract) | `juneau-rest-server` | SPI base class with 
`Optional<AuthResult> authenticate(req)` contract. |
+| `AuthResult` | `juneau-rest-server` | Immutable value: resolved `Principal` 
+ role set. |
+| `AuthFilterChain` | `juneau-rest-server` | Orchestrates multiple 
`AuthFilter` instances; first-success principal, role aggregation, all-failure 
401. |
+| `AuthenticatedRequestWrapper` | `juneau-rest-server` | 
`HttpServletRequestWrapper` that surfaces filter-resolved principal and roles 
to downstream Juneau code. |
+| `BearerTokenAuthFilter` | `juneau-rest-server` | Filter impl reusing 
FINISHED-69's `TokenValidator` SPI. |
+| `ApiKeyAuthFilter` | `juneau-rest-server` | Filter impl reusing 
FINISHED-69's `ApiKeyStore` SPI. |
+| `JettyServerComponent.addFilter(...)` | `juneau-microservice-jetty` | New 
public filter-registration surface for Jetty microservices. |
+
+## Mental model: filter-time AuthN + op-time AuthZ
+
+The framework preserves the clean separation between *authentication* (who is 
this?) and *authorization* (may they do this?):
+
+```
+HTTP request
+  │
+  ▼
+AuthFilterChain (servlet filter — runs before RestServlet)
+  │  calls authenticate() on matching filters
+  │  wraps request with AuthenticatedRequestWrapper on first success
+  ▼
+RestServlet.service(wrappedReq, ...)
+  │
+  ▼
+RestGuardList (RoleBasedRestGuard, etc.)
+  │  reads req.isUserInRole(...) from AuthenticatedRequestWrapper
+  ▼
+@RestOp handler
+  │  @Auth Principal p ← reads RestServerConstants.PRINCIPAL_ATTR
+  ▼
+```
+
+`AuthenticatedRequestWrapper` bridges the two layers: it overrides 
`getUserPrincipal()`, `isUserInRole(String)`, `getRemoteUser()`, and 
`getAttribute(PRINCIPAL_ATTR)` so both `RoleBasedRestGuard` and the `@Auth` arg 
resolver see the filter-resolved identity with zero changes.
+
+## Two-layer architecture
+
+### Layer 1 — Standalone filters
+
+Each concrete `AuthFilter` subclass (`BearerTokenAuthFilter`, 
`ApiKeyAuthFilter`, any custom impl) is also a `jakarta.servlet.Filter` by 
itself. Use the standalone mode when a single auth scheme is sufficient:
+
+```java
+// Standalone bearer-token filter registered directly with 
JettyServerComponent:
+jsc.addFilter(
+    BearerTokenAuthFilter.create().validator(jwtValidator).build(),
+    "/*");
+```
+
+In standalone mode, `AuthFilter.doFilter(req, resp, chain)`:
+- Calls `authenticate(req)`.
+- On success: wraps the request and passes it through.
+- On `Optional.empty()`: passes the request through unchanged (no credentials 
for this filter).
+- On `AuthenticationException`: writes a `401` response with 
`WWW-Authenticate` challenge.
+
+### Layer 2 — `AuthFilterChain` orchestrator
+
+Use `AuthFilterChain` when multiple auth schemes overlap on the same URL:
+
+```java
+@Bean
+public AuthFilterChain authFilters(BeanStore bs) {
+    return AuthFilterChain.create(bs)
+        .append(BearerTokenAuthFilter.create()
+            .validator(jwtValidator)
+            .build())
+        .append(ApiKeyAuthFilter.create()
+            .store(apiKeyStore)
+            .build())
+        // /sso/** accepts only SAML (provided by juneau-rest-server-saml, a 
future sibling module):
+        .append(mySamlFilter, "/sso/*")
+        .build();
+}
+```
+
+`JettyServerComponent` auto-mounts the `AuthFilterChain` bean at `/*` when it 
is present in the `BeanStore` — no explicit `addFilter(...)` call needed for 
the common case.
+
+## `Optional<AuthResult>` three-state contract
+
+`AuthFilter.authenticate(req)` returns one of three states:
+
+| State | Meaning |
+|-------|---------|
+| `Optional.empty()` | Filter does not apply (no recognizable credentials). 
Chain continues. |
+| `Optional.of(AuthResult)` | Authentication succeeded. |
+| throw `AuthenticationException` | Credentials were present but invalid (bad 
token, revoked key, etc.). |
+
+This distinction matters for the chain's failure-aggregation logic:
+- If no filter applies (all return empty) the request passes through unchanged 
— public endpoints need no credentials.
+- If at least one filter actively rejected (threw) the request, the chain 
sends `401` with aggregated `WWW-Authenticate` challenges.
+
+## Pattern matching
+
+Each filter in the chain can be scoped to a URL pattern via `append(filter, 
pattern)`. Patterns are parsed once at build time via 
`UrlPathMatcher.of(String)`. Supported forms:
+
+| Pattern | Matches |
+|---------|---------|
+| (none) | All paths |
+| `/*` | All paths |
+| `/api/*` | `/api/`, `/api/users`, `/api/users/123`, … |
+| `/foo/{id}/*` | `/foo/abc/anything`, … |
+| `*.json` | Any path ending in `.json` |
+
+Filters whose patterns do not match the request path are **not called** — they 
contribute neither successes nor failures.
+
+## Role aggregation semantics
+
+When multiple filters match and succeed, the chain aggregates roles across all 
successful results:
+
+- The **first** successful filter's `Principal` wins for identity 
(deterministic ordering by registration order).
+- **All** successful filters' role sets are unioned into a single 
`Set<String>`.
+- `AuthenticatedRequestWrapper.isUserInRole(String)` checks this union.
+
+Example: Bearer JWT grants `user`, API key grants `admin`. Both succeed on the 
same request:
+- `getUserPrincipal().getName()` returns the Bearer principal's name.
+- `isUserInRole("user")` → `true`, `isUserInRole("admin")` → `true`.
+- `RoleBasedRestGuard("user|admin")` → passes.
+
+## Registration via `@Bean`
+
+The preferred pattern mirrors FINISHED-69's `@Bean RestGuardList` idiom:
+
+```java
+public class MyMicroservice extends JettyMicroservice {
+
+    @Bean
+    public AuthFilterChain authFilters(BeanStore bs) {
+        return AuthFilterChain.create(bs)
+            .append(BearerTokenAuthFilter.create()
+                .pattern("/api/*")
+                .validator(jwtValidator)
+                .build())
+            .append(ApiKeyAuthFilter.create()
+                .pattern("/api/*")
+                .store(apiKeyStore)
+                .build())
+            .build();
+    }
+}
+```
+
+`JettyServerComponent.onStart(...)` scans the `BeanStore` for an 
`AuthFilterChain` bean and registers it at `/*` before any servlet is mounted.
+
+## Direct filter registration
+
+For simpler deployments or when bypassing the chain, register standalone 
filters directly:
+
+```java
+// In a microservice subclass or test setup:
+getBeanStore().getBean(JettyServerComponent.class).ifPresent(jsc ->
+    
jsc.addFilter(BearerTokenAuthFilter.create().validator(myValidator).build(), 
"/api/*")
+);
+```
+
+Both `addFilter(Filter, String)` (single pattern) and `addFilter(Filter, 
String...)` (multiple patterns) are available.
+
+## Worked examples
+
+### Bearer + API-key with pattern overlap
+
+```java
+// /api/* accepts Bearer JWT or API key — first to succeed wins; roles 
aggregate when both do.
+AuthFilterChain.create(bs)
+    .append(BearerTokenAuthFilter.create()
+        .pattern("/api/*")
+        .validator(jwtValidator)
+        .build())
+    .append(ApiKeyAuthFilter.create()
+        .pattern("/api/*")
+        .store(apiKeyStore)
+        .build())
+    .build();
+```
+
+### Pattern-disjoint filters
+
+```java
+// /api/* → JWT only; /internal/* → API key only; no overlap.
+AuthFilterChain.create(bs)
+    .append(BearerTokenAuthFilter.create()
+        .pattern("/api/*")
+        .validator(jwtValidator)
+        .build())
+    .append(ApiKeyAuthFilter.create()
+        .pattern("/internal/*")
+        .store(internalKeyStore)
+        .build())
+    .build();
+```
+
+### Failure-aggregation response shape
+
+When both filters apply and both reject, the response is:
+
+```
+HTTP/1.1 401 Unauthorized
+WWW-Authenticate: Bearer realm="api", ApiKey realm="api"
+
+Bearer token missing; API key missing
+```
+
+## `BearerTokenAuthFilter` — bearer tokens
+
+Mirrors `BearerTokenGuard` extraction logic. Reads `Authorization: Bearer 
<token>`, delegates to a 
[`TokenValidator`](/site/apidocs/org/apache/juneau/rest/auth/TokenValidator.html).
 Extracts roles from a 
[`ClaimsPrincipal`](/site/apidocs/org/apache/juneau/rest/auth/ClaimsPrincipal.html)
 claim (default claim name: `"roles"`).
+
+**Builder methods:** `validator(TokenValidator)` (required), `realm(String)` 
(default: `"api"`), `rolesClaim(String)` (default: `"roles"`).
+
+If absent or non-`Bearer` scheme → `Optional.empty()`.
+
+## `ApiKeyAuthFilter` — API keys
+
+Mirrors `ApiKeyGuard` extraction logic. Reads the key from a configurable 
source (header / query / cookie), delegates to an 
[`ApiKeyStore`](/site/apidocs/org/apache/juneau/rest/auth/ApiKeyStore.html). 
Extracts roles from `ClaimsPrincipal` claims.
+
+**Builder methods:** `store(ApiKeyStore)` (required), `fromHeader(String)` 
(default: `X-API-Key`), `fromQuery(String)`, `fromCookie(String)`, 
`realm(String)` (default: `"api"`), `rolesClaim(String)` (default: `"roles"`).
+
+If key absent or blank → `Optional.empty()`.
+
+## Spring Boot note
+
+Spring Security's 
[`SecurityFilterChain`](https://docs.spring.io/spring-security/reference/servlet/architecture.html)
 is the **recommended authentication path for Spring Boot users**. 
`BearerTokenAuthFilter` / `ApiKeyAuthFilter` can be registered via Spring 
Boot's `FilterRegistrationBean` if desired, but the `AuthFilterChain` `@Bean` 
auto-mount is wired for `JettyServerComponent` only. Do not run both frameworks 
on the same request path.
+
+## See also
+
+- [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.
+- 
[`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)
+- 
[`ApiKeyAuthFilter`](/site/apidocs/org/apache/juneau/rest/auth/filter/ApiKeyAuthFilter.html)
+- 
[`JettyServerComponent`](/site/apidocs/org/apache/juneau/microservice/jetty/JettyServerComponent.html)
diff --git a/pages/topics/10.20j.SamlAuthSupport.md 
b/pages/topics/10.20j.SamlAuthSupport.md
new file mode 100644
index 0000000000..60d1064a30
--- /dev/null
+++ b/pages/topics/10.20j.SamlAuthSupport.md
@@ -0,0 +1,139 @@
+---
+title: "SAML 2.0 AuthN Support (juneau-rest-server-saml)"
+slug: SamlAuthSupport
+---
+
+The opt-in `juneau-rest-server-saml` module adds SAML 2.0 single-sign-on (Web 
Browser SSO Profile) to `juneau-rest-server` by wrapping [OpenSAML 
5.x](https://shibboleth.atlassian.net/wiki/spaces/OS30) behind the 
[AuthFilter](/docs/topics/AuthFilterFramework) / 
[TokenValidator](/docs/topics/AuthGuards) SPIs that ship in 
`juneau-rest-server`.
+
+OpenSAML is an Apache 2.0-licensed library maintained by 
[Shibboleth](https://shibboleth.atlassian.net/). It 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 OpenSAML 5.x patch 
they want (default pin: `5.2.2`).
+
+## At a glance
+
+| Component | Purpose |
+|-----------|---------|
+| 
[`SamlAssertionValidator`](/site/apidocs/org/apache/juneau/rest/auth/saml/SamlAssertionValidator.html)
 | Validates a SAML 2.0 `<samlp:Response>` document and returns a 
`ClaimsPrincipal` marked with `issuerType=SAML`. |
+| 
[`SamlAuthFilter`](/site/apidocs/org/apache/juneau/rest/auth/saml/SamlAuthFilter.html)
 | `AuthFilter` that decodes the inbound `SAMLResponse` form / query parameter 
and delegates validation. |
+| 
[`SamlBinding`](/site/apidocs/org/apache/juneau/rest/auth/saml/SamlBinding.html)
 | Enum of supported HTTP bindings (`POST`, `REDIRECT`). Artifact + SOAP 
deferred. |
+| 
[`SamlMetadataResolvers`](/site/apidocs/org/apache/juneau/rest/auth/saml/SamlMetadataResolvers.html)
 | Convenience factories (`file(File|Path)`, `url(String)`) that build 
initialized OpenSAML `MetadataResolver` instances. |
+
+## Drop-in usage
+
+```java
+@Bean
+public AuthFilterChain authFilters(BeanStore bs) throws IOException {
+
+    // 1) Build the validator. Pick one of:
+    //   - 
.metadataResolver(SamlMetadataResolvers.url("https://idp.example.com/metadata";))
+    //   - .signingCredential(myStaticCredential)
+    SamlAssertionValidator validator = SamlAssertionValidator.create()
+        
.metadataResolver(SamlMetadataResolvers.url("https://idp.example.com/metadata";))
+        .spEntityId("https://sp.example.com";)
+        .expectedIssuer("https://idp.example.com";)
+        .build();
+
+    // 2) Wrap it in a SamlAuthFilter and attach to the chain.
+    return AuthFilterChain.create(bs)
+        .append(SamlAuthFilter.create()
+            .consumerPath("/saml/acs")
+            .binding(SamlBinding.POST)
+            .validator(validator)
+            .build())
+        .build();
+}
+```
+
+`JettyServerComponent` auto-mounts the `AuthFilterChain` bean at `/*` once it 
is present in the `BeanStore` &mdash; no explicit `addFilter(...)` call needed.
+
+## Bindings
+
+`SamlAuthFilter` accepts SAML responses in either HTTP-flavored binding from 
OASIS SAML 2.0:
+
+| `SamlBinding` | Wire shape | Decode steps |
+|---------------|------------|--------------|
+| `POST` (default) | `SAMLResponse` form parameter, base64-encoded XML. | 
base64-decode &rarr; UTF-8 string. |
+| `REDIRECT` | `SAMLResponse` query parameter, base64-encoded 
**DEFLATE-compressed** XML. | base64-decode &rarr; raw DEFLATE-inflate &rarr; 
UTF-8 string. |
+
+Set the binding on the filter builder:
+
+```java
+SamlAuthFilter.create()
+    .binding(SamlBinding.REDIRECT)
+    .validator(validator)
+    .build();
+```
+
+Artifact + SOAP bindings are deferred &mdash; if your IdP only emits those, 
see 
[`SamlAssertionValidator.validate(String)`](/site/apidocs/org/apache/juneau/rest/auth/saml/SamlAssertionValidator.html#validate(java.lang.String))
 and decode the wire-format yourself, then call the validator directly.
+
+## Security defaults
+
+`SamlAssertionValidator` ships with deliberately strict defaults aligned with 
current SAML 2.0 hardening guidance:
+
+| Default | Setting | Rationale |
+|---------|---------|-----------|
+| Signature algorithm allowlist | `[rsa-sha256, ecdsa-sha256]` | SHA-1 is 
permanently rejected. The builder will refuse a `algorithms(...)` value 
containing any SHA-1 URI. |
+| Mandatory signature on every assertion | yes | Unsigned assertions are 
rejected. |
+| `SAMLSignatureProfileValidator` | yes | Runs before crypto verification to 
catch wrapping / structural attacks. |
+| Audience restriction | required | `<AudienceRestriction>` must list the 
configured `spEntityId`. |
+| Clock skew | 60s default, 300s max | Mirrors the JWT validator's defaults; 
the builder refuses larger values. |
+| Encrypted assertions | opt-in via `decryptionCredential(...)` | A response 
carrying `<EncryptedAssertion>` is rejected with a `decryption_required` 
challenge when no key is configured; a wrong key yields `decryption_failed`. |
+
+## Marker claim, not subclass
+
+The returned 
[`ClaimsPrincipal`](/site/apidocs/org/apache/juneau/rest/auth/ClaimsPrincipal.html)
 is annotated with `issuerType=SAML` (rather than introducing a 
`SamlClaimsPrincipal` subclass) so downstream code can distinguish SAML-derived 
principals from JWT-derived principals without resorting to instanceof checks:
+
+```java
+@RestGet
+public String hello(@Auth Principal principal) {
+    var cp = (ClaimsPrincipal) principal;
+    if ("SAML".equals(cp.getClaim("issuerType", String.class).orElse(null))) {
+        // SAML path
+    }
+    return "Hello, " + principal.getName();
+}
+```
+
+## Encrypted assertions
+
+Per OQA Q5 (TODO-94b), v1 ships support for `<EncryptedAssertion>`:
+
+```java
+var validator = SamlAssertionValidator.create()
+    
.metadataResolver(SamlMetadataResolvers.url("https://idp.example.com/metadata";))
+    .spEntityId("https://sp.example.com";)
+    .expectedIssuer("https://idp.example.com";)
+    .decryptionCredential(myDecryptionCredential)
+    .build();
+```
+
+The decrypter uses OpenSAML's `opensaml-xmlsec-impl` chain 
(`InlineEncryptedKeyResolver` + `SimpleRetrievalMethodEncryptedKeyResolver`). 
When a response contains `<EncryptedAssertion>` but no 
`decryptionCredential(...)` is configured, validation fails fast with an 
`AuthenticationException` carrying the `WWW-Authenticate: SAML 
error="decryption_required"` challenge.
+
+## Maven dependency
+
+OpenSAML 5.x is published to the [Shibboleth Maven 
repository](https://build.shibboleth.net/maven/releases/) rather than Maven 
Central. The `juneau-rest-server-saml` POM declares that repository locally so 
consumers don't need to configure it in their own `settings.xml`. Pin the 
version via the `<opensaml.version>` POM property (default `5.2.2`; do not use 
`5.0.0`).
+
+```xml
+<dependency>
+    <groupId>org.apache.juneau</groupId>
+    <artifactId>juneau-rest-server-saml</artifactId>
+    <version>9.5.0</version>
+</dependency>
+<dependency>
+    <groupId>org.opensaml</groupId>
+    <artifactId>opensaml-saml-impl</artifactId>
+    <version>5.2.2</version>          <!-- provided scope; consumer-supplied 
-->
+</dependency>
+```
+
+`mvn -pl juneau-rest/juneau-rest-server dependency:tree | grep -i opensaml` 
returns nothing &mdash; the containment requirement is verified at build time.
+
+## Deferred
+
+- **Artifact + SOAP bindings.** Both are post-IdP-redirect server-to-server 
flows; rare in modern integrations. If your IdP requires either, file an issue 
with a worked test fixture.
+- **Single Logout (SLO).** Out of scope for v1 of this module &mdash; SLO is 
typically handled at the IdP layer rather than per-SP.
+
+## See also
+
+- [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 that composes with this framework (`ClaimsPrincipal`, `TokenValidator`, 
etc. live there).
+- [OAuth AuthN Support](/docs/topics/OAuthAuthSupport) &mdash; the sibling 
OAuth 2.0 / OIDC module.
+- [OASIS SAML 2.0 
Core](https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf), 
[SAML 2.0 
Bindings](https://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf).
diff --git a/pages/topics/10.20k.OAuthAuthSupport.md 
b/pages/topics/10.20k.OAuthAuthSupport.md
new file mode 100644
index 0000000000..fe0bb209df
--- /dev/null
+++ b/pages/topics/10.20k.OAuthAuthSupport.md
@@ -0,0 +1,224 @@
+---
+title: "OAuth 2.0 / OIDC AuthN Support (juneau-rest-server-oauth)"
+slug: OAuthAuthSupport
+---
+
+The opt-in `juneau-rest-server-oauth` module adds OAuth 2.0 + OIDC 
bearer-token validation, RFC 7662 token introspection, RFC 6749 client-side 
grant flows, and OIDC discovery to `juneau-rest-server` by wrapping the [Nimbus 
OAuth 2.0 SDK](https://connect2id.com/products/nimbus-oauth-openid-connect-sdk) 
(`com.nimbusds:oauth2-oidc-sdk`) behind the 
[AuthFilter](/docs/topics/AuthFilterFramework) / 
[TokenValidator](/docs/topics/AuthGuards) SPIs.
+
+The Nimbus SDK is Apache 2.0-licensed and is from the same vendor as the 
`nimbus-jose-jwt` already used by 
[juneau-rest-server-jwt](/docs/topics/RestServerAuthGuards). It 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`).
+
+## At a glance
+
+| Component | Purpose |
+|-----------|---------|
+| 
[`OAuthFilter`](/site/apidocs/org/apache/juneau/rest/auth/oauth/OAuthFilter.html)
 | `AuthFilter` that authenticates RFC 6750 `Bearer` tokens; delegates to any 
`TokenValidator`. |
+| 
[`OAuthIntrospectionValidator`](/site/apidocs/org/apache/juneau/rest/auth/oauth/OAuthIntrospectionValidator.html)
 | `TokenValidator` that validates **opaque** tokens via RFC 7662 
introspection. |
+| 
[`TokenCache`](/site/apidocs/org/apache/juneau/rest/auth/oauth/TokenCache.html) 
+ 
[`BoundedLruTokenCache`](/site/apidocs/org/apache/juneau/rest/auth/oauth/BoundedLruTokenCache.html)
 | SPI + default impl for caching principals + tokens. |
+| 
[`OAuthToken`](/site/apidocs/org/apache/juneau/rest/auth/oauth/OAuthToken.html) 
| Immutable record returned by every flow helper. |
+| 
[`OAuthClientCredentialsFlow`](/site/apidocs/org/apache/juneau/rest/auth/oauth/flow/OAuthClientCredentialsFlow.html)
 | RFC 6749 &sect;4.4 client-credentials helper (server-to-server). |
+| 
[`OAuthAuthorizationCodeFlow`](/site/apidocs/org/apache/juneau/rest/auth/oauth/flow/OAuthAuthorizationCodeFlow.html)
 | RFC 6749 &sect;4.1 authorization-code helper with mandatory PKCE (RFC 7636). 
|
+| 
[`OAuthRefreshTokenFlow`](/site/apidocs/org/apache/juneau/rest/auth/oauth/flow/OAuthRefreshTokenFlow.html)
 | RFC 6749 &sect;6 refresh-token helper. |
+| 
[`OAuthResourceOwnerFlow`](/site/apidocs/org/apache/juneau/rest/auth/oauth/flow/OAuthResourceOwnerFlow.html)
 | RFC 6749 &sect;4.3 password-grant helper. **Filed `@Deprecated(since = 
"9.5.0")` from day-1.** |
+| 
[`OidcDiscoveryClient`](/site/apidocs/org/apache/juneau/rest/auth/oauth/oidc/OidcDiscoveryClient.html)
 + 
[`OidcMetadata`](/site/apidocs/org/apache/juneau/rest/auth/oauth/oidc/OidcMetadata.html)
 | `.well-known/openid-configuration` discovery. |
+
+## Server-side: validate bearer tokens
+
+The most common deployment is a resource-server that validates a Bearer access 
token on every request. There are two token shapes:
+
+- **Opaque tokens** &mdash; validated via RFC 7662 introspection against the 
IdP's `/oauth2/introspect` endpoint (this module's 
`OAuthIntrospectionValidator`).
+- **JWT access tokens** &mdash; validated locally against the IdP's JWKS (use 
[`JwtTokenValidator`](/site/apidocs/org/apache/juneau/rest/auth/jwt/JwtTokenValidator.html)
 from `juneau-rest-server-jwt`). The OIDC default is JWT.
+
+Both shapes plug into the same `OAuthFilter`:
+
+```java
+@Bean
+public AuthFilterChain authFilters(BeanStore bs) {
+
+    // Opaque-token path via RFC 7662 introspection:
+    var introspection = OAuthIntrospectionValidator.create()
+        
.introspectionEndpoint(URI.create("https://idp.example.com/oauth2/introspect";))
+        .clientId("api-server")
+        .clientSecret("...")
+        .requiredScopes("read:orders")
+        .build();
+
+    return AuthFilterChain.create(bs)
+        .append(OAuthFilter.create()
+            .validator(introspection)
+            .build())
+        .build();
+}
+```
+
+For a JWT-access-token IdP, swap `introspection` for a 
[`JwtTokenValidator`](/site/apidocs/org/apache/juneau/rest/auth/jwt/JwtTokenValidator.html)
 built per the JWT topic page &mdash; `OAuthFilter` is 
`TokenValidator`-agnostic.
+
+### Role extraction
+
+By default `OAuthFilter` extracts roles from the `scope` claim (RFC 6749 
&sect;3.3 &mdash; space-delimited string). Override the claim name to read 
groups, role-array claims, etc.:
+
+```java
+OAuthFilter.create()
+    .validator(introspection)
+    .rolesClaim("groups")          // for IdPs that emit a "groups": [...] 
claim
+    .build();
+```
+
+### Required scopes (`requiredScopes` on the validator)
+
+Required-scope enforcement lives on `OAuthIntrospectionValidator`, not on 
`OAuthFilter` (per OQA Q6 in TODO-94c). This centralizes "what the caller is 
allowed to do" with the introspection result &mdash; if the IdP-returned 
`scope` claim does not include a required scope, validation fails with 
`WWW-Authenticate: Bearer error="insufficient_scope"` regardless of which 
filter is mounted.
+
+```java
+OAuthIntrospectionValidator.create()
+    .introspectionEndpoint(...)
+    .clientId("api-server")
+    .clientSecret("...")
+    .requiredScopes("read:orders", "write:orders")
+    .build();
+```
+
+### Token cache
+
+`OAuthIntrospectionValidator` short-circuits the introspection round-trip on 
cache hits. The default cache is a thread-safe bounded LRU (1000 entries, 
5-minute TTL, capped at 1 hour):
+
+```java
+OAuthIntrospectionValidator.create()
+    // ...
+    .tokenCache(BoundedLruTokenCache.create(2000))   // larger cap
+    .cacheTtl(Duration.ofMinutes(15))                 // longer TTL
+    .build();
+```
+
+The cache TTL is also capped by the token's own `exp` claim (whichever is 
shorter wins).
+
+## Client-side: acquire tokens
+
+For the client side of OAuth, this module ships four flow helpers that wrap 
the equivalent Nimbus SDK grant classes behind a uniform Juneau-friendly 
builder API.
+
+### Client-credentials (server-to-server)
+
+```java
+var token = OAuthClientCredentialsFlow.create()
+    .tokenEndpoint(URI.create("https://idp.example.com/oauth2/token";))
+    .clientId("worker-service")
+    .clientSecret("...")
+    .scope("read:orders", "write:orders")
+    .build()
+    .acquire();
+```
+
+For repeated calls, supply a `tokenCache(TokenCache)` so the flow can reuse 
the access token until it nears expiry:
+
+```java
+.tokenCache(BoundedLruTokenCache.create())
+.cacheSkew(Duration.ofSeconds(30))
+```
+
+### Authorization-code with PKCE (browser-redirect login)
+
+```java
+var flow = OAuthAuthorizationCodeFlow.create()
+    
.authorizationEndpoint(URI.create("https://idp.example.com/oauth2/authorize";))
+    .tokenEndpoint(URI.create("https://idp.example.com/oauth2/token";))
+    .clientId("web-app")
+    .clientSecret("...")          // omit for public clients (PKCE-only)
+    .redirectUri(URI.create("https://app.example.com/callback";))
+    .scope("openid", "profile")
+    .build();
+
+// 1) Redirect the user-agent to the IdP.
+var verifier = new CodeVerifier();
+var challenge = CodeChallenge.compute(CodeChallengeMethod.S256, verifier);
+var url = flow.buildAuthorizationUrl("some-state", challenge);
+// (caller stores state + verifier in a server-side session keyed by browser 
cookie)
+
+// 2) When the IdP redirects back with ?code=...
+var token = flow.exchange(code, verifier);
+```
+
+PKCE is **mandatory** &mdash; the builder does not expose a "PKCE off" toggle. 
`S256` is the only challenge method per OAuth 2.1 guidance.
+
+### Refresh-token
+
+```java
+var refreshed = OAuthRefreshTokenFlow.create()
+    .tokenEndpoint(URI.create("https://idp.example.com/oauth2/token";))
+    .clientId("web-app")
+    .clientSecret("...")          // optional
+    .refreshToken(prevToken.refreshToken().get())
+    .build()
+    .acquire();
+```
+
+### Resource-owner password grant *(discouraged)*
+
+`OAuthResourceOwnerFlow` is filed `@Deprecated(since = "9.5.0")` from day-1. 
The resource-owner password-credentials grant was removed from OAuth 2.1 due to 
long-standing security concerns (the client sees the user's credentials). 
Legitimate use is limited to first-party trusted clients mid-migration:
+
+```java
+@SuppressWarnings("deprecation")
+var token = OAuthResourceOwnerFlow.create()
+    .tokenEndpoint(URI.create("https://idp.example.com/oauth2/token";))
+    .clientId("legacy-app")
+    .clientSecret("...")
+    .username("alice")
+    .password("...")
+    .build()
+    .acquire();
+```
+
+Prefer `OAuthAuthorizationCodeFlow` (with PKCE) wherever possible.
+
+## OIDC discovery
+
+`OidcDiscoveryClient` fetches the `.well-known/openid-configuration` document 
and returns a Juneau-native `OidcMetadata` record. Useful when the IdP's 
endpoint URLs are not hard-coded:
+
+```java
+var oidc = OidcDiscoveryClient.create()
+    .issuer(URI.create("https://login.example.com/realms/api";))
+    .build()
+    .discover();
+
+var validator = OAuthIntrospectionValidator.create()
+    .introspectionEndpoint(oidc.introspectionEndpoint())
+    .clientId("api-server")
+    .clientSecret("...")
+    .build();
+```
+
+The metadata record exposes the standard OIDC endpoints (issuer, token, 
authorization, introspection, userinfo, jwks, end-session) plus an `extras` map 
for non-standard fields the IdP advertises.
+
+## 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-oauth</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 Nimbus SDK pulls `nimbus-jose-jwt` transitively, which is fine &mdash; 
`juneau-rest-server-jwt` already uses that JAR.
+
+## 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`.
+
+## See also
+
+- [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.
+- [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749), [RFC 
6750](https://datatracker.ietf.org/doc/html/rfc6750), [RFC 
7636](https://datatracker.ietf.org/doc/html/rfc7636), [RFC 
7662](https://datatracker.ietf.org/doc/html/rfc7662).


Reply via email to