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',