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 38a624dc32 docs: TODO-69 — RestServerAuthGuards topic page + release 
notes for juneau-rest-server-jwt
38a624dc32 is described below

commit 38a624dc3207336be6e16ff2dcd2a16c02a283cc
Author: James Bognar <[email protected]>
AuthorDate: Mon May 25 07:55:31 2026 -0400

    docs: TODO-69 — RestServerAuthGuards topic page + release notes for 
juneau-rest-server-jwt
    
    Co-authored-by: Cursor <[email protected]>
---
 pages/release-notes/9.5.0.md                |  50 ++++++++
 pages/topics/10.20e.RestServerAuthGuards.md | 192 ++++++++++++++++++++++++++++
 sidebars.ts                                 |   5 +
 3 files changed, 247 insertions(+)

diff --git a/pages/release-notes/9.5.0.md b/pages/release-notes/9.5.0.md
index 660fc17b12..764618aed3 100644
--- a/pages/release-notes/9.5.0.md
+++ b/pages/release-notes/9.5.0.md
@@ -1999,6 +1999,23 @@ A new JUnit 5 extension and `@TestBean` annotation 
enable Spring-style test-time
 - **Per-test and per-class scopes** via `@TestBean(scope = METHOD)` (default) 
and `@TestBean(scope = CLASS)` (must be on `static` members). Method-scope 
overlays chain on top of class-scope overlays.
 - **Named-bean qualifier** via `@TestBean(name = "...")` matching the 
framework's existing `@Bean(name = "...")` parameter resolution.
 
+#### AuthN Guards — Bearer / API-Key / `@Auth Principal` (TODO-69)
+
+`juneau-rest-server` now ships two opt-in AuthN guards plus an `@Auth 
Principal` argument resolver. The core jar stays JWT-free — a separate 
`juneau-rest-server-jwt` sub-module (see below) carries the nimbus-jose-jwt 
integration. See [REST Server — AuthN 
Guards](/docs/topics/RestServerAuthGuards) for the full reference.
+
+- **`org.apache.juneau.rest.auth.BearerTokenGuard`** — RFC 6750 bearer-token 
guard with a fluent builder:
+  `realm(String)` (default: `"api"`), `validator(TokenValidator)` (required). 
Stashes the resolved `Principal` on `RequestAttributes` under 
`RestServerConstants.PRINCIPAL_ATTR`. Throws 401 with `WWW-Authenticate: Bearer 
realm="<realm>"` on missing / malformed / rejected tokens — preserves richer 
challenges supplied by the validator.
+- **`org.apache.juneau.rest.auth.ApiKeyGuard`** — opaque API-key guard with a 
fluent builder:
+  `store(ApiKeyStore)` (required), `fromHeader(String)` (default: 
`X-API-Key`), `fromQuery(String)`, `fromCookie(String)`, `realm(String)` 
(default: `"api"`). Falls back to manual `Cookie` header parsing when the 
underlying container (e.g. `MockRestRequest`) doesn't populate `getCookies()`.
+- **`org.apache.juneau.rest.auth.TokenValidator`** SPI — single-method 
functional interface (`Principal validate(String token) throws 
AuthenticationException`) for plugging in opaque-token, JWT, or custom 
validators.
+- **`org.apache.juneau.rest.auth.ApiKeyStore`** SPI — single-method functional 
interface (`Optional<Principal> lookup(String key)`) for plugging in in-memory, 
database, or Vault-backed key stores.
+- **`@org.apache.juneau.rest.auth.Auth`** annotation — marks an 
`@RestOp`-method parameter for principal injection. The new **`AuthArg`** 
`RestOpArg` resolver also supports type-driven resolution: a bare `Principal` 
(or `ClaimsPrincipal`) parameter is injected with no annotation needed.
+- **`org.apache.juneau.rest.auth.ClaimsPrincipal`** — `Principal` subclass 
with typed claim access (`getClaim(String, Class<T>)`, `getClaims()`). Used by 
JWT validators; lives in core so the API surface is reachable without the JWT 
module.
+- **`org.apache.juneau.rest.auth.AuthenticationException`** — `Unauthorized` 
(401) subclass with fluent `wwwAuthenticate(String)` setter (RFC 7235 §4.1).
+- **`RestServerConstants`** additions: `PRINCIPAL_ATTR` (request-attribute key 
for the stashed principal), `API_KEY_HEADER` (default header name 
`"X-API-Key"`).
+- **`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`).
+
 #### 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.
@@ -3212,6 +3229,39 @@ JsonRpcRequest back = JsonParser.DEFAULT.parse(wire, 
JsonRpcRequest.class);
 
 See [juneau-bean-mcp](/docs/topics/JuneauBeanMcp) for the full topic.
 
+### juneau-rest-server-jwt (new module)
+
+A new opt-in REST module, `juneau-rest-server-jwt`, adds JWT bearer-token 
verification to `juneau-rest-server` without bleeding the 
[nimbus-jose-jwt](https://connect2id.com/products/nimbus-jose-jwt) dependency 
into the core. The nimbus dep is declared in `provided` scope on the module's 
POM, so consumers explicitly pick the nimbus version they want.
+
+`mvn -pl juneau-rest/juneau-rest-server dependency:tree | grep -i nimbus` 
returns nothing — the containment requirement is verified at build time. See 
[REST Server — AuthN Guards § JWT 
verification](/docs/topics/RestServerAuthGuards) for the full reference.
+
+#### New Classes
+
+- **`org.apache.juneau.rest.auth.jwt.JwtTokenValidator`** — `TokenValidator` 
that fetches keys from a JWKS endpoint, verifies signatures, and enforces `iss` 
/ `aud` / `exp` / `nbf` with secure defaults. Fluent builder: `jwksUrl(URI)` 
(or `jwkSource(JWKSource)`), `issuer(String)`, `audience(String)`, 
`algorithms(JWSAlgorithm...)` (default `[RS256, ES256]`), `clockSkew(Duration)` 
(default 60s, capped at 5m), `jwksCacheTtl(Duration)` (default 5m), 
`clock(Clock)` (test-time injection).
+- **`org.apache.juneau.rest.auth.jwt.JwksCache`** (package-private) — 
Single-slot TTL cache wrapping any `JWKSource<SecurityContext>`. Continues 
serving the last-known-good key set when the upstream fetch fails past TTL, 
logging a `WARNING` so transient JWKS-endpoint outages don't cascade into 
correlated auth outages.
+
+#### Security defaults
+
+- **`alg: none` is permanently rejected** — there is no opt-in. The builder 
also refuses `algorithms(JWSAlgorithm.NONE)`.
+- **`HS256` is opt-in only** — defaults to `[RS256, ES256]`. Mixing HMAC and 
asymmetric algorithms in the same validator (the algorithm-confusion attack 
vector) is rejected by the allowlist check.
+- **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).
+
+#### Dependency
+
+```xml
+<dependency>
+    <groupId>org.apache.juneau</groupId>
+    <artifactId>juneau-rest-server-jwt</artifactId>
+    <version>9.5.0</version>
+</dependency>
+<dependency>
+    <groupId>com.nimbusds</groupId>
+    <artifactId>nimbus-jose-jwt</artifactId>
+    <version>10.3</version>          <!-- consumer-supplied; provided scope on 
juneau-rest-server-jwt -->
+</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
new file mode 100644
index 0000000000..9f181135f3
--- /dev/null
+++ b/pages/topics/10.20e.RestServerAuthGuards.md
@@ -0,0 +1,192 @@
+---
+title: "AuthN Guards — Bearer / API-Key / JWT"
+slug: RestServerAuthGuards
+---
+
+Juneau REST servers ship two opt-in authentication (AuthN) guards in 
`org.apache.juneau.rest.auth` plus an `@Auth Principal` argument resolver. A 
separate, optional sub-module — `juneau-rest-server-jwt` — adds JWT 
verification on top via 
[nimbus-jose-jwt](https://connect2id.com/products/nimbus-jose-jwt). The core 
`juneau-rest-server` jar stays JWT-free; teams that don't want JWT pay zero 
classpath cost.
+
+All three pieces are purely additive — they are wired in through the existing 
`@Bean` / `RestGuardList` extension point and do not change the behavior of any 
pre-existing handler.
+
+## At a glance
+
+| Component                | Module                    | Purpose               
                                                                    |
+|--------------------------|---------------------------|-------------------------------------------------------------------------------------------|
+| `BearerTokenGuard`       | `juneau-rest-server`      | Parse `Authorization: 
Bearer <token>`, delegate to a `TokenValidator`, stash a `Principal`. |
+| `ApiKeyGuard`            | `juneau-rest-server`      | Read an API key from 
a header / query / cookie, delegate to an `ApiKeyStore`, stash a `Principal`. |
+| `TokenValidator` (SPI)   | `juneau-rest-server`      | Pluggable token 
validator (opaque, JWT, custom).                                          |
+| `ApiKeyStore` (SPI)      | `juneau-rest-server`      | Pluggable API-key 
lookup (in-memory, database, Vault, etc.).                              |
+| `@Auth Principal`        | `juneau-rest-server`      | Inject the 
authenticated `Principal` into `@RestOp` method parameters.                    |
+| `ClaimsPrincipal`        | `juneau-rest-server`      | `Principal` subclass 
with typed claim access (used by JWT validators).                    |
+| `AuthenticationException`| `juneau-rest-server`      | 401 with a fluent 
`WWW-Authenticate` setter.                                              |
+| `JwtTokenValidator`      | `juneau-rest-server-jwt`  | JWKS-backed JWT 
validator with algorithm allowlisting, mandatory claim checks, and clock-skew 
tolerance. |
+
+## `BearerTokenGuard` — RFC 6750 bearer tokens
+
+[`BearerTokenGuard`](/site/apidocs/org/apache/juneau/rest/auth/BearerTokenGuard.html)
 extracts a bearer token from the `Authorization` header, hands it to a 
[`TokenValidator`](/site/apidocs/org/apache/juneau/rest/auth/TokenValidator.html),
 and stashes the resulting `Principal` on the request attributes under 
`RestServerConstants.PRINCIPAL_ATTR`. On failure it throws a 401 with 
`WWW-Authenticate: Bearer realm="<realm>"` set on the response (RFC 7235 §4.1).
+
+### Custom `TokenValidator`
+
+```java
+TokenValidator opaqueValidator = token -> {
+    var principal = revocationCheckedLookup(token);
+    if (principal == null)
+        throw new AuthenticationException("Token revoked or unknown");
+    return principal;
+};
+
+@Rest(path="/api")
+public class ApiResource extends RestServlet {
+
+    @Bean
+    public RestGuardList guards(BeanStore bs) {
+        return RestGuardList.create(bs)
+            
.append(BearerTokenGuard.create().realm("api").validator(opaqueValidator).build())
+            .build();
+    }
+
+    @RestGet(path="/me")
+    public String me(@Auth Principal p) { return p.getName(); }
+}
+```
+
+The validator is allowed to throw `AuthenticationException` with a richer 
`WWW-Authenticate` value (e.g. `Bearer error="invalid_token", 
error_description="..."`) — the guard preserves it.
+
+## `ApiKeyGuard` — opaque API keys
+
+[`ApiKeyGuard`](/site/apidocs/org/apache/juneau/rest/auth/ApiKeyGuard.html) 
reads an API key from a configurable source (header / query / cookie), 
delegates lookup to an 
[`ApiKeyStore`](/site/apidocs/org/apache/juneau/rest/auth/ApiKeyStore.html), 
and stashes the resolved `Principal`. The default source is the `X-API-Key` 
request header.
+
+```java
+ApiKeyStore store = key -> {
+    var principal = lookupByKey(key);
+    return Optional.ofNullable(principal);
+};
+
+@Bean
+public RestGuardList guards(BeanStore bs) {
+    return RestGuardList.create(bs)
+        .append(ApiKeyGuard.create().store(store).build())                     
  // default: X-API-Key header
+        .build();
+}
+
+// Alternatives:
+ApiKeyGuard.create().store(store).fromHeader("X-Acme-Token").build();          
   // custom header
+ApiKeyGuard.create().store(store).fromQuery("apiKey").build();                 
   // query string
+ApiKeyGuard.create().store(store).fromCookie("api_key").build();               
   // cookie
+```
+
+> **Security note** — query-string keys leak into proxy / access logs and 
browser history. Prefer header or cookie sources for production traffic; the 
query-string source exists for legacy / debugging use only. `ApiKeyStore` 
implementations should compare keys in constant time 
(`java.security.MessageDigest#isEqual(byte[],byte[])`) to defeat timing 
side-channels.
+
+## `@Auth Principal` injection
+
+Once a guard has stashed a principal, you can inject it into any `@RestOp` 
method via the [`@Auth`](/site/apidocs/org/apache/juneau/rest/auth/Auth.html) 
annotation, or simply by declaring a `Principal`-typed parameter:
+
+```java
+@RestGet(path="/me")
+public String me(@Auth Principal p) { return p.getName(); }
+
+@RestGet(path="/bare")
+public String bare(Principal p) { return p.getName(); }    // type-driven; no 
annotation needed
+
+@RestGet(path="/claims")
+public String claims(@Auth ClaimsPrincipal cp) {
+    return cp.getClaim("scope", String.class).orElse("none");
+}
+```
+
+[`AuthArg`](/site/apidocs/org/apache/juneau/rest/auth/AuthArg.html) is 
registered in 
[`DefaultConfig`](/site/apidocs/org/apache/juneau/rest/config/DefaultConfig.html),
 so no extra wiring is required to enable type-driven resolution.
+
+## JWT verification — `juneau-rest-server-jwt`
+
+The optional `juneau-rest-server-jwt` sub-module adds 
[`JwtTokenValidator`](/site/apidocs/org/apache/juneau/rest/auth/jwt/JwtTokenValidator.html)
 — a `TokenValidator` that fetches keys from a JWKS endpoint, verifies 
signatures, and enforces `iss` / `aud` / `exp` / `nbf` with secure defaults.
+
+### Maven dependency
+
+```xml
+<dependency>
+    <groupId>org.apache.juneau</groupId>
+    <artifactId>juneau-rest-server-jwt</artifactId>
+    <version>9.5.0</version>
+</dependency>
+<dependency>
+    <groupId>com.nimbusds</groupId>
+    <artifactId>nimbus-jose-jwt</artifactId>
+    <version>10.3</version>          <!-- consumer-supplied; provided scope on 
juneau-rest-server-jwt -->
+</dependency>
+```
+
+The nimbus dependency is declared `provided` on the sub-module by design, so 
it does not bleed transitively into upstream `juneau-rest-server` consumers. 
Pick the nimbus version you want.
+
+### Building a validator
+
+```java
+var validator = JwtTokenValidator.create()
+    .jwksUrl(URI.create("https://issuer.example.com/.well-known/jwks.json";))
+    .issuer("https://issuer.example.com/";)
+    .audience("https://api.example.com";)
+    .build();
+
+var guard = 
BearerTokenGuard.create().realm("api").validator(validator).build();
+```
+
+| Builder method        | Purpose                                              
                                                |
+|-----------------------|------------------------------------------------------------------------------------------------------|
+| `jwksUrl(URI)`        | JWKS endpoint URL. Mutually exclusive with 
`jwkSource(...)`.                                         |
+| `jwkSource(JWKSource)`| Plug in a custom key source (HSM, federation, 
tests). Mutually exclusive with `jwksUrl(...)`.       |
+| `issuer(String)`      | Required exact-match `iss` claim.                    
                                                |
+| `audience(String)`    | Required `aud` claim (token's `aud` must contain 
this value).                                         |
+| `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). |
+| `clock(Clock)`        | Inject a deterministic clock for tests.              
                                                |
+
+### Security defaults
+
+`JwtTokenValidator` ships with deliberately strict defaults:
+
+- **No `alg: none`** — unsigned JWTs are rejected unconditionally. There is no 
opt-in.
+- **Algorithm allowlisting** — defaults to `[RS256, ES256]`. `HS256` is 
rejected unless the caller explicitly opts in. This defeats the 
**algorithm-confusion attack** where a JWT is signed with `HS256` using the RSA 
public key as the HMAC secret.
+- **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.
+
+## Composing with `BasicAdminResource`
+
+`BasicAdminResource` ships with `DenyAllGuard` as its default — the secure 
default is "no admin access until you wire something up." Swapping in 
`BearerTokenGuard` (or any other guard) is a zero-mixin-source-change migration:
+
+```java
+@Rest(path="/myapp")
+public class MyAppResource extends BasicRestServlet implements 
BasicAdminResource {
+
+    @Bean                                              // overrides the 
mixin's DenyAllGuard.
+    public RestGuardList adminGuards(BeanStore bs) {
+        return RestGuardList.of(new BearerTokenGuard(myTokenValidator));
+    }
+}
+```
+
+The mixin honors whatever guard the host registers; nothing inside 
`BasicAdminResource` needs to change.
+
+## Composing with rate-limit guards
+
+When stacking 
[`RateLimitGuard`](/site/apidocs/org/apache/juneau/rest/guard/RateLimitGuard.html)
 and AuthN guards, **run AuthN first** so unauthenticated requests don't 
consume a rate-limit budget intended for legitimate clients. `RestGuardList` 
runs guards in declaration order:
+
+```java
+@Bean
+public RestGuardList guards(BeanStore bs) {
+    return RestGuardList.create(bs)
+        .append(BearerTokenGuard.create().validator(myValidator).build())      
       // 1. AuthN
+        .append(RateLimitGuard.create().permitsPerSecond(50).build())          
       // 2. throttle
+        .build();
+}
+```
+
+## Security checklist
+
+- **Always require `iss`, `aud`, `exp`, `nbf`** when accepting JWTs. 
`JwtTokenValidator` enforces this by default — don't disable it.
+- **Allowlist algorithms explicitly.** Never accept `none`. Treat `HS256` as 
the special case it is (shared-secret signing); avoid mixing it with asymmetric 
keys in the same validator.
+- **Pin a JWKS rotation strategy.** Even with `JwtTokenValidator`'s 
graceful-degradation, you want real-world clients to roll new keys before old 
ones expire.
+- **TLS everywhere.** Bearer tokens and API keys are credentials — never log 
them and never echo them in response bodies.
+- **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).
diff --git a/sidebars.ts b/sidebars.ts
index 5bb78b8d23..0a6de7d053 100644
--- a/sidebars.ts
+++ b/sidebars.ts
@@ -1521,6 +1521,11 @@ const sidebars: SidebarsConfig = {
                                                        id: 
'topics/10.20d.RestServerTestBeanInjection',
                                                        label: '10.20d. 
Test-time Bean Injection',
                                                },
+                                               {
+                                                       type: 'doc',
+                                                       id: 
'topics/10.20e.RestServerAuthGuards',
+                                                       label: '10.20e. AuthN 
Guards — Bearer / API-Key / JWT',
+                                               },
                                                {
                                                        type: 'doc',
                                                        id: 
'topics/10.21.BuiltInParameters',

Reply via email to