Skip to main content

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​

ComponentModulePurpose
BearerTokenGuardjuneau-rest-serverParse Authorization: Bearer <token>, delegate to a TokenValidator, stash a Principal.
ApiKeyGuardjuneau-rest-serverRead an API key from a header / query / cookie, delegate to an ApiKeyStore, stash a Principal.
TokenValidator (SPI)juneau-rest-serverPluggable token validator (opaque, JWT, custom).
ApiKeyStore (SPI)juneau-rest-serverPluggable API-key lookup (in-memory, database, Vault, etc.).
@Auth Principaljuneau-rest-serverInject the authenticated Principal into @RestOp method parameters.
ClaimsPrincipaljuneau-rest-serverPrincipal subclass with typed claim access (used by JWT validators).
AuthenticationExceptionjuneau-rest-server401 with a fluent WWW-Authenticate setter.
JwtTokenValidatorjuneau-rest-server-auth-jwtJWKS-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. 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 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 methodPurpose
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]. 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.
  • JWKS eager refresh on kid miss — when a fresh-cache key selection returns no matching keys (typically because an IdP rotated its signing key mid-TTL), the cache performs one out-of-band JWKS refresh before failing the request. Bounded by a 10-second cooldown and a single-in-flight guard. Enabled by default; opt out with jwksEagerRefreshOnKidMiss(false). Applies only to jwksUrl(...)-backed caches — a caller-supplied jwkSource(...) is unaffected.

Composing with 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, 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, 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.