AuthN Guards — Bearer / API-Key / JWT
Juneau REST servers ship two opt-in authentication (AuthN) guards in org.apache.juneau.rest.server.auth plus an @Auth Principal argument resolver. A separate, optional sub-module — juneau-rest-server-auth-jwt — adds JWT verification on top via nimbus-jose-jwt. The core juneau-rest-server jar stays JWT-free; teams that don't want JWT pay zero classpath cost.
See the guide: Secure an endpoint with JWT and OAuth
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-auth-jwt | JWKS-backed JWT validator with algorithm allowlisting, mandatory claim checks, and clock-skew tolerance. |
BearerTokenGuard — RFC 6750 bearer tokens
BearerTokenGuard extracts a bearer token from the Authorization header, hands it to a TokenValidator, 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
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 reads an API key from a configurable source (header / query / cookie), delegates lookup to an ApiKeyStore, and stashes the resolved Principal. The default source is the X-API-Key request header.
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.
ApiKeyStoreimplementations 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 annotation, or simply by declaring a Principal-typed parameter:
@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 is registered in DefaultConfig, so no extra wiring is required to enable type-driven resolution.
JWT verification — juneau-rest-server-auth-jwt
The optional juneau-rest-server-auth-jwt sub-module adds JwtTokenValidator — a TokenValidator that fetches keys from a JWKS endpoint, verifies signatures, and enforces iss / aud / exp / nbf with secure defaults.
Maven dependency
<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-rest-server-auth-jwt</artifactId>
<version>${juneau.version}</version>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>10.3</version> <!-- consumer-supplied; provided scope on juneau-rest-server-auth-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
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). |
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-10.0.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
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].HS256is rejected unless the caller explicitly opts in. This defeats the algorithm-confusion attack where a JWT is signed withHS256using the RSA public key as the HMAC secret. - Mandatory claims —
iss,aud,exp,nbfare 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
kidmiss — 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 withjwksEagerRefreshOnKidMiss(false). Applies only tojwksUrl(...)-backed caches — a caller-suppliedjwkSource(...)is unaffected.
Composing with AdminMixin
AdminMixin 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:
@Rest(path="/myapp", mixins=AdminMixin.class)
public class MyAppResource extends BasicRestServlet {
@Bean(name="guards") // overrides the mixin's DenyAllGuard.
public RestGuardList guards(BeanStore bs) {
return RestGuardList.create(bs)
.append(new BearerTokenGuard(myTokenValidator))
.build();
}
}
The mixin honors whatever guard the host registers via the bean-store override seam; nothing inside AdminMixin needs to change. (A routed-child flavor, AdminResource, and a standalone-servlet flavor, AdminServlet, are also available and follow the same default-deny-plus-@Bean-override pattern.)
Composing with rate-limit guards
When stacking RateLimitGuard 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:
@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,nbfwhen accepting JWTs.JwtTokenValidatorenforces this by default — don't disable it. - Allowlist algorithms explicitly. Never accept
none. TreatHS256as 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
expvalues with TLS pinning and (when feasible) DPoP / mutual-TLS for sensitive endpoints.
See also: Guards, REST Authenticator — Resource-Level Authentication, Rate-Limiting and Request-Id Propagation, AuthN Filter Framework, SAML 2.0 AuthN Support, OAuth 2.0 / OIDC AuthN Support, AdminMixin.