OAuth 2.0 / OIDC AuthN Support (juneau-rest-server-auth-oauth)
The opt-in juneau-rest-server-auth-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 (com.nimbusds:oauth2-oidc-sdk) behind the AuthFilter / TokenValidator SPIs.
See the guide: Secure an endpoint with JWT and OAuth
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-auth-jwt. It is declared in provided scope on the module's POM so the dependency does not bleed into juneau-rest-server — consumers explicitly pick the SDK version they want (default pin: 11.37.2).
juneau-rest-auth-oauth-flowOAuthToken, OAuthClientCredentialsFlow, OAuthAuthorizationCodeFlow, OAuthRefreshTokenFlow, OidcDiscoveryClient, and OidcMetadata ship in the role-neutral juneau-rest-auth-oauth-flow module (package org.apache.juneau.rest.auth.oauth.flow / .oidc), pulled in transitively by this module. Being role-neutral, they're also consumed directly by the MCP client-side token layer (juneau-rest-client-mcp-auth). Only the server-specific pieces below (OAuthFilter, OAuthIntrospectionValidator, the token-cache SPI, and the deprecated OAuthResourceOwnerFlow) remain in juneau-rest-server-auth-oauth.
At a glance
| Component | Purpose |
|---|---|
OAuthFilter | AuthFilter that authenticates RFC 6750 Bearer tokens; delegates to any TokenValidator. |
OAuthIntrospectionValidator | TokenValidator that validates opaque tokens via RFC 7662 introspection. |
TokenCache + BoundedLruTokenCache | SPI + default impl for caching principals + tokens. |
OAuthToken | Immutable record returned by every flow helper. (juneau-rest-auth-oauth-flow) |
OAuthClientCredentialsFlow | RFC 6749 §4.4 client-credentials helper (server-to-server). (juneau-rest-auth-oauth-flow) |
OAuthAuthorizationCodeFlow | RFC 6749 §4.1 authorization-code helper with mandatory PKCE (RFC 7636). (juneau-rest-auth-oauth-flow) |
OAuthRefreshTokenFlow | RFC 6749 §6 refresh-token helper. (juneau-rest-auth-oauth-flow) |
OAuthResourceOwnerFlow | RFC 6749 §4.3 password-grant helper. Filed @Deprecated(since = "10.0.0") from day-1. |
OidcDiscoveryClient + OidcMetadata | .well-known/openid-configuration discovery. (juneau-rest-auth-oauth-flow) |
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 — validated via RFC 7662 introspection against the IdP's
/oauth2/introspectendpoint (this module'sOAuthIntrospectionValidator). - JWT access tokens — validated locally against the IdP's JWKS (use
JwtTokenValidatorfromjuneau-rest-server-auth-jwt). The OIDC default is JWT.
Both shapes plug into the same OAuthFilter:
@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 built per the JWT topic page — OAuthFilter is TokenValidator-agnostic.
Role extraction
By default OAuthFilter extracts roles from the scope claim (RFC 6749 §3.3 — space-delimited string). Override the claim name to read groups, role-array claims, etc.:
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. This centralizes "what the caller is allowed to do" with the introspection result — 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.
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):
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)
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:
.tokenCache(BoundedLruTokenCache.create())
.cacheSkew(Duration.ofSeconds(30))
Authorization-code with PKCE (browser-redirect login)
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 — the builder does not expose a "PKCE off" toggle. S256 is the only challenge method per OAuth 2.1 guidance.
Refresh-token
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 = "10.0.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:
@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:
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
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
<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-rest-server-auth-oauth</artifactId>
<version>${juneau.version}</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 — juneau-rest-server-auth-jwt already uses that JAR.
Deferred
- Device-code grant (RFC 8628) — deferred. File an issue if needed.
- JWKS-on-
kid-miss eager refresh — shipped in 10.0.0 onjuneau-rest-server-auth-jwt'sJwksCache; see REST Server — AuthN Guards § JWKS eager refresh on kid miss.
See also
- Secure an endpoint with JWT and OAuth: a guided walkthrough.
- OIDC Relying Party Login — the
juneau-rest-server-auth-oidc-rpmodule that composes the discovery client + auth-code helper here into a full interactive browser-login flow (startLogin/completeLogin/logout). - AuthN Filter Framework — the SPI both this module's filter and the bundled bearer/api-key filters implement.
- AuthN Guards — the op-level guard family (
ClaimsPrincipal,TokenValidator,JwtTokenValidator) that composes with this framework. - SAML 2.0 AuthN Support — the sibling SAML module.
- RFC 6749, RFC 6750, RFC 7636, RFC 7662.