Overview
OAuth2 assigns roles — authorization server, resource server, client — and a Java service usually plays exactly one of them. The protocol theory lives in the Security theme; the Java question is which dependency implements which role, what the token-validation machinery actually checks, and how to test any of it without a production IdP. All three major frameworks converge on the same shape: a resource server validates JWTs against the IdP's published keys; a client drives the auth-code redirect dance; almost nobody writes their own authorization server.
Key points
- Resource server (an API accepting bearer tokens): Spring Security
oauth2-resource-server, Quarkusquarkus-oidc(service mode) orquarkus-smallrye-jwt(MicroProfile JWT), Micronautmicronaut-security-jwt. Configuration is essentially one property: the issuer URL, from which JWKS discovery follows. - Client / login (an app obtaining tokens):
spring-security-oauth2-client, Quarkusquarkus-oidcinweb-appmode,micronaut-security-oauth2. All implement the OIDC auth-code flow with session-backed login. - Authorization server: Spring Authorization Server exists and is solid, but the pragmatic default is delegating to Keycloak (or a SaaS IdP — Auth0, Entra ID, Okta). Running an AS is an operational commitment, not a library choice.
- Flows that matter: Authorization Code + PKCE (all user-facing clients, SPAs and mobile included) and Client Credentials (service-to-service). Dead flows: implicit and resource-owner password — removed from OAuth 2.1; do not build new code on them.
- JWT validation = signature against a JWKS key +
iss+aud+exp/nbfwith bounded clock skew. Every framework does this for you; your job is configuring issuer and audience correctly. - Opaque tokens trade self-containment for revocability and need RFC 7662 introspection on every request (supported by all three stacks) — fine behind a gateway, costly at high fan-out.
- Propagation: pass the incoming bearer token to downstream calls (or exchange it); each stack has client-side filters for this rather than hand-built headers.
- Testing: never test against a live IdP — use mock OIDC servers or Keycloak in Testcontainers; Quarkus Dev Services gives you a dev-mode Keycloak for free.
Details
Resource server — validating JWTs
The 90% case. Per framework:
| Dependency | Key config | |
|---|---|---|
| Spring | spring-boot-starter-oauth2-resource-server |
spring.security.oauth2.resourceserver.jwt.issuer-uri |
| Quarkus | quarkus-oidc (application-type=service) or quarkus-smallrye-jwt |
quarkus.oidc.auth-server-url, quarkus.oidc.client-id / mp.jwt.verify.issuer, mp.jwt.verify.publickey.location |
| Micronaut | micronaut-security-jwt |
micronaut.security.token.jwt.signatures.jwks.<name>.url |
Mechanics common to all three: on startup (or first request) the framework fetches
/.well-known/openid-configuration from the issuer, discovers the jwks_uri, caches the
JWKS and refreshes it on unknown-kid misses — which is how IdP key rotation works without
redeploys. Validation then checks signature, iss (must equal the configured issuer),
aud (set it — a token minted for another API must not be accepted), and exp/nbf with
a small tolerated clock skew (default ~60 s in most stacks). Claims map to the security
context: Spring exposes a Jwt principal and maps scopes to SCOPE_x authorities
(customisable via a converter); Quarkus injects JsonWebToken; Micronaut exposes
Authentication.getAttributes(). Role claims rarely match defaults out of the box —
Keycloak's realm_access.roles, for instance, needs a mapper in every stack.
Client and login
For server-side web apps, the framework handles the redirect to the IdP, state/PKCE,
the code-for-token exchange and session establishment. Spring: register providers under
spring.security.oauth2.client.registration.*; Quarkus: quarkus-oidc with
application-type=web-app (cookie-backed session, token refresh handled); Micronaut:
micronaut-security-oauth2 with micronaut.security.oauth2.clients.*. For
service-to-service calls, the client-credentials grant is likewise config-driven: Spring's
OAuth2AuthorizedClientManager behind WebClient/RestClient, Quarkus
quarkus-oidc-client (with a REST-client filter that attaches and refreshes the token),
Micronaut's client-credentials support with client-credentials.enabled per client.
Running an authorization server
Spring Authorization Server is the one credible embed-it-yourself option on the JVM (successor to the deprecated Spring Security OAuth AS) — reasonable for product-embedded identity or exotic token requirements. Otherwise delegate: Keycloak is the pragmatic default (self-hosted, OIDC-certified, admin API, realm import for reproducible config), SaaS IdPs where operating one is not your business. The decision is organisational more than technical: token minting concentrates risk, and patching/HA for an IdP is real work.
Propagation between services
Passing identity along a call chain: simplest is forwarding the incoming bearer token —
Spring servlet stacks propagate via a RestClient/WebClient interceptor reading the
SecurityContext; Quarkus has quarkus-rest-client-oidc-token-propagation; Micronaut
sets micronaut.security.token.propagation.enabled with a service-selector regex. The
forwarded token's aud should cover the downstream service — where it doesn't, RFC 8693
token exchange (supported by Keycloak) is the correct, less-travelled road.
Testing
- Mock OIDC servers: Spring's
spring-security-testfakes the token layer entirely (jwt()post-processors /@WithMockUser);quarkus-test-oidc-server(WireMock-based) and libraries likemock-oauth2-server(Nav) stand up a fake issuer with a real JWKS. - Keycloak Testcontainers:
dasniko/testcontainers-keycloakboots a real Keycloak with a realm-import JSON; the test obtains real tokens via the token endpoint — highest fidelity, slower. - Quarkus Dev Services for Keycloak: with
quarkus-oidcpresent and noauth-server-urlconfigured, dev/test mode auto-starts a Keycloak container and wires it in;@QuarkusTestplusKeycloakTestClientcovers integration tests with no setup.
Examples
# Spring Boot resource server — application.yml
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://idp.example.com/realms/main
audiences: my-api
# Quarkus resource server — application.properties
quarkus.oidc.auth-server-url=https://idp.example.com/realms/main
quarkus.oidc.client-id=my-api
quarkus.oidc.token.audience=my-api
# dev mode: omit auth-server-url and Dev Services boots Keycloak for you
Related
- Security in Java applications — the map — parent overview of the Java security landscape.
- Authentication & authorization — the protocol theory: flows, JWT pitfalls, authz models.
- Web security in Java apps — CSRF/session concerns that interact with token-based auth.
- Integration testing — Testcontainers mechanics behind the Keycloak test setups.