Skip to main content

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).

Flow helpers now live in juneau-rest-auth-oauth-flow

OAuthToken, 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​

ComponentPurpose
OAuthFilterAuthFilter that authenticates RFC 6750 Bearer tokens; delegates to any TokenValidator.
OAuthIntrospectionValidatorTokenValidator that validates opaque tokens via RFC 7662 introspection.
TokenCache + BoundedLruTokenCacheSPI + default impl for caching principals + tokens.
OAuthTokenImmutable record returned by every flow helper. (juneau-rest-auth-oauth-flow)
OAuthClientCredentialsFlowRFC 6749 §4.4 client-credentials helper (server-to-server). (juneau-rest-auth-oauth-flow)
OAuthAuthorizationCodeFlowRFC 6749 §4.1 authorization-code helper with mandatory PKCE (RFC 7636). (juneau-rest-auth-oauth-flow)
OAuthRefreshTokenFlowRFC 6749 §6 refresh-token helper. (juneau-rest-auth-oauth-flow)
OAuthResourceOwnerFlowRFC 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/introspect endpoint (this module's OAuthIntrospectionValidator).
  • JWT access tokens — validated locally against the IdP's JWKS (use JwtTokenValidator from juneau-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​

See also​