OAuth-Servlets is a library of servlet filters using the Nimbus OAuth SDK and Caffeine to implement an OAuth resource server.
There's a twin library for Jakarta RS, OAuth-RS, that can be used outside a servlets environment, but can also be used alongside OAuth-Servlets.
For a few years I've been copy/pasting some code to implement bearer token introspection, most of the time using java.net.http
or OkHttp for HTTP requests to the OAuth Introspection Endpoint, Jackson for JSON parsing of the response, and Caffeine for caching of introspection responses.
I've had that need again, though with slightly different requirements, and after making OIDC-Servlets I thought I'd write a similar library for OAuth, also built on top of the Nimbus OAuth SDK.
The project requires a JDK in version 21 or higher.
You will need Docker Compose to run the tests locally (e.g. to contribute).
Add a dependency on net.ltgt.oauth:oauth-servlets
. You can also use the net.ltgt.oauth:oauth-bom
BOM to make sure the library uses the same version of its net.ltgt.oauth:oauth-common
dependency (and possibly align the version with OAuth-RS when used conjointly).
Create a TokenIntrospector
object and add it as a ServletContext
attribute:
var tokenIntrospector = new TokenIntrospector(/* … */);
servletContext.setAttribute(TokenIntrospector.CONTEXT_ATTRIBUTE_NAME, tokenIntrospector);
Note
You can also use the filters' constructors if you instantiate them yourself (or through a dependency-injection framework), rather than using ServletContext
attributes. The same is true for values passed as init parameters.
Register the TokenFilter
, most likely to all requests, and it should match early; this filter will setup the HttpServletRequest
for later filters and servlets to answer the getRemoteUser()
, getUserPrincipal()
, and isUserInRole()
methods:
// Using the ServletContext dynamic registration (e.g. from ServletContextInitializer)
servletContext.addFilter("token", TokenFilter.class)
.addMappingForUrlPatterns(null, false, "/*");
The TokenPrincipal
returned by getUserPrincipal()
also exposes an hasScope()
method to easily check whether the token is valid for a given scope value.
The implementation of isUserInRole(String)
relies on the actual TokenPrincipal
, which is derived from the Introspection response. The default implementation (SimpleTokenPrincipal
) always returns false
(the user has no known role). Other implementations can be used by configuring a TokenPrincipalProvider
as a ServletContext
attribute. Another built-in implementation reads Keycloak realm roles from the Introspection response, and can be configured by using the KeycloakTokenPrincipal.PROVIDER
provider:
servletContext.setAttribute(
TokenPrincipalFactory.CONTEXT_ATTRIBUTE_NAME, KeycloakTokenPrincipal.PROVIDER);
Custom implementations can also read additional data (e.g. from a database) to expose in their custom TokenPrincipal
. If they can't afford doing it on each request, or would just rather do it once and cache it for some time, they can extend CachedTokenPrincipalProvider
or use it to wrap any existing TokenPrincipalProvider
.
The TokenFilter
will pass all requests down the filter chain (unless their Authorization: Bearer
header is invalid or contains an invalid, expired or revoked token); it'll specifically chain down any request without an Authorization
header. To enforce authorizations, add additional filters to check the TokenPrincipal
:
- The
IsAuthenticatedFilter
for instance only checks that aTokenPrincipal
is indeed present (i.e. the request must include anAuthorization: Bearer
header with a valid token). - The
HasRoleFilter
checks whether the user has a given role; this requires using a customTokenPrincipal
(if only aKeycloakTokenPrincipal
). - The
HasScopeFilter
will also check whether the token has a given scope value, and will respond with aninsufficient_scope
error otherwise.
Note
If you use Jakarta RS in a servlets environment, you can use OAuth-RS in addition to OAuth-Servlets to apply similar authorization filters at a Jakarta RS resource level. Those Jakarta RS filters use the same TokenPrincipal
, retrieved from the Jakarta RS SecurityContext
's getUserPrincipal()
which should mirror the HttpServletRequest
's getUserPrincipal()
so the two libraries can work hand-in-hand.
The TokenIntrospector
and CachedTokenPrincipalProvider
both use a Caffeine cache to coordinate concurrent requests and cache the result. Their constructors expect a Caffeine
cache builder configured with eviction and possibly refresh durations. Those should be configured depending on the application's security needs: shorter eviction means that you detect revocations earlier but put a higher load on the Authorization Server and possibly cause some latency depending on the cache refresh policy; determining the configuration also depends on the access tokens' lifetime.
For typical tokens with a 60-minute lifetime, a good configuration could be expireAfterAccess=10m, refreshAfterWrite=5m
, meaning that you detect revocations after at most 5 minutes, and keep the Introspection Response in the cache for 10 minutes after a token has been tentatively used (even if it's invalid or after it has expired; which prevents useless introspection requests).
Depending on the cost of providing a TokenPrincipal
, a CachedTokenPrincipalProvider
could use the same cache configuration as the TokenIntrospector
or a shorter one, and/or possibly no refresh. Using a longer expiration will uselessly consume memory though.