Spring Security's OAuth2 defaults were written for server-rendered applications. These Spring Boot starters give a Spring backend for single-page and mobile applications the OAuth2 / OpenID Connect behaviours it actually needs, as properties, and access-control tests that run the real authorities mapping.
Works with Keycloak, Auth0, Amazon Cognito, Microsoft Entra ID and any other OpenID Provider, with several of them at a time if needed. Servlet (WebMVC) and reactive (WebFlux) applications are both supported. These libs are a complement to the official spring-boot-starter-oauth2-resource-server and spring-boot-starter-oauth2-client, not a replacement: they add the beans those starters leave to the application.
Fewer lines of configuration is the visible part, not the reason. A backend consumed by JavaScript or by a mobile app hits Spring Security defaults which were designed for a browser doing navigations, and none of them fails at startup:
| What is observed | Why | With spring-addons-starter-oidc |
|---|---|---|
hasRole('ADMIN') never matches, even for an admin |
Authorities come from the scope claim only. Keycloak, Auth0 and Cognito put roles elsewhere |
authorities[].path, a JSON path in the claims |
| Every call fails with a CORS error on the preflight | CORS configured on the MVC side runs after the security filters, which reject an OPTIONS without credentials |
cors[] on the security chain |
A fetch starting the login flow dies with an opaque CORS error |
302 to the authorization server, which the browser follows cross-origin inside the fetch |
2xx with the Location header, the JavaScript navigates itself |
An API call without a session gets a login page instead of a 401 |
The entry point redirects, as it should for a navigation | authentication-entry-point: UNAUTHORIZED |
Every POST from the frontend is 403, so CSRF gets disabled |
The CSRF token lives in the session, JavaScript cannot read it | csrf: cookie-accessible-from-js |
| The frontend chooses where to land after login, and the BFF follows blindly | That destination is application code, and unvalidated it is an open redirect | Validated against post-login-allowed-uri-patterns |
| Users are logged out at random under load | One refresh_token flow per parallel request, and rotated tokens invalidate the others. Declined upstream |
One token request per session at a time |
| Tests pass with authorities production never grants | spring-security-test builds the Authentication itself and skips the application's converter |
@WithJwt runs the real converter, on any @Component |
The complete list, with what each item is (a security flaw, a broken behaviour, or a test that lies) and the honest boundary of the claim, is on what goes wrong without it. The starter keeps Spring's defaults unless a property says otherwise, and the bff sample sets all of them.
A REST API accepting access tokens issued by Keycloak, with roles mapped to Spring Security authorities, CORS, and public routes. There is no security Java configuration to write.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-starter-oidc</artifactId>
<version>${springaddons.version}</version>
</dependency>
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-starter-oidc-test</artifactId>
<version>${springaddons.version}</version>
<scope>test</scope>
</dependency>Set the springaddons.version property to the version displayed by the Maven Central badge above.
com:
c4-soft:
springaddons:
oidc:
# Trusted OpenID Providers. The "iss" claim of an access token selects the entry,
# and with it the username and authorities mapping. Add as many as needed.
ops:
- iss: http://localhost:7080/auth/realms/spring-addons
username-claim: preferred_username
authorities:
# JSON paths in the token payload: Keycloak realm roles and client roles here
- path: $.realm_access.roles
- path: $.resource_access.*.roles
resourceserver:
# Anything else requires a valid access token and is answered with a 401, not a
# redirection to a login page
permit-all:
- /greetings/public
cors:
- path: /**
allowed-origin-patterns: https://localhost:4200Access control then lives where it belongs, next to the code it protects:
@GetMapping("/greetings/me")
@PreAuthorize("hasAuthority('NICE')")
GreetingDto getGreeting(Authentication auth) { ... }And the tests run that very same authorities mapping, without decoding a token and without an authorization server:
@Test
@WithJwt("brice.json") // a JSON claim-set in test resources
void givenUserIsBrice_whenGetMe_thenOk() throws Exception {
api.get("/greetings/me").andExpect(status().isOk());
}@WithJwt builds the security context by running that claim-set through the application's own authentication converter, so the username, the authorities and the Authentication implementation are the ones the application would build at runtime. This is what spring-security-test request post-processors and mutators cannot do: they skip the converter and build a stub Authentication themselves.
From there, the five runnable samples are the fastest way in. Start with resource-server, it is the smallest.
Each line of spring-addons targets a line of Spring Boot and is built against it. Fixes land on master (Boot 4) first and are ported to the 8.5 and 8.4 branches (Boot 3.5 and 3.4) when they are not specific to Boot 4.
| spring-addons | Spring Boot | Java | Notes |
|---|---|---|---|
9.x |
4.1.x (4.0.x up to 9.1.5) |
17+ | Current line. 9.4.0 is built against Boot 4.1.1 and Spring Security 7.1.1, the samples use Spring Cloud 2025.1.3. |
8.5.x |
3.5.x |
17+ | Maintenance line on the 8.5 branch: the fixes and improvements of 9.4.x which are not specific to Boot 4, ported to Boot 3.5. 8.5.0 is built against Boot 3.5.16, September 2026. |
8.4.x |
3.4.x |
17+ | Same content as 8.5.x on the 8.4 branch. 8.4.0 is built against Boot 3.4.13, September 2026. |
7.x |
3.3.x |
17+ | Last release 7.8.12, November 2024. |
The exact version of the current line is the one in the Maven Central badge above. Changes are documented per version in the release notes, and breaking changes in the migration guides (7.0.0, 8.0.0, 8.4.0 / 8.5.0 when coming from 8.1.x, 9.2.0). The number of guides overstates the churn: 8.4.0, 8.5.0 and 9.2.0 concern spring-addons-starter-rest only, and the last breaking change in spring-addons-starter-oidc is the OAuthentication class in 8.0.0, which touched applications using that class and none of the properties.
| Module | What it is for |
|---|---|
spring-addons-starter-oidc |
Resource server and oauth2Login client security auto-configuration, driven by properties |
spring-addons-starter-rest |
RestClient / WebClient / @HttpExchange beans auto-configured from properties: authorization, proxy, SSL, timeouts |
spring-addons-oauth2-test |
Annotations populating the test security context with OAuth2 authentications, on any kind of @Component |
spring-addons-starter-oidc-test |
Test companion for applications using spring-addons-starter-oidc |
spring-addons-starter-openapi |
Makes the enum values in a springdoc-openapi spec match what the application really accepts and emits |
spring-addons-starter-recaptcha |
Server-side validation of Google reCAPTCHA v2 and v3 |
Each module is usable on its own. The complete reference is on the documentation site, one page per concern.
Cutting configuration code is the visible part. The reasons to actually depend on these starters are the cases where the framework leaves us alone:
- Several heterogeneous OpenID Providers at once, static or resolved dynamically, each with its own username claim and its own authorities mapping. One more entry in
ops, no code. - One
refresh_tokenflow at a time per session. Most authorization servers rotate refresh tokens. When a user-agent fires parallel requests with an expired access token in session, Spring Security runs one flow per request, only one succeeds, and the others are answered with a401. This was declined upstream as something the framework cannot solve generally.spring-addons-starter-oidcdecorates the authorized client provider so that the authorization server sees a single token request. - A complete BFF. Authorization code with PKCE, RP-Initiated Logout including for the providers which do not strictly implement it (Auth0, Cognito), Back-Channel Logout, a CSRF cookie readable by JavaScript, and
2xxstatuses instead of3xxso that a single-page application can follow the redirections itself. - REST clients that survive real networks. HTTP proxies from
HTTP_PROXYandNO_PROXYincluding proxy credentials on HTTPS tunnels, SSL bundles, self-signed certificates, timeouts, and switching the underlying HTTP client library, all from properties. - Tests that exercise the real mapping, as shown in the quickstart above, on
@Serviceand@Repositorytoo, not only on controllers.
We keep complete control over what is auto-configured. Almost every auto-configured component is @ConditionalOnMissingBean, so spring-addons backs off as soon as the application defines its own bean, and overriding a default means defining that one bean, not a whole Security(Web)FilterChain. The auto-configured filter chains have the lowest precedence, so an application can add its own chains with stricter security matchers. The risks, in both directions page is worth two minutes before adopting, and what you would write without spring-addons names the hand-written Spring Security equivalent of every auto-configured bean.
When not to use it: a server-rendered application with oauth2Login and no JavaScript caller is well served by Spring Security's defaults, and a team which wants to own every security bean should write them and read the two pages above as a checklist.
Auto-configuration for resource servers:
- accepting tokens issued by several trusted authorization servers
- mapping authorities from a variety of claims, including nested ones, with custom prefix and case
- CORS configuration
- allowing anonymous preflight requests using the path-matchers in CORS configuration
Auto-configuration for clients with oauth2Login:
- customizing the responses returned to the frontend during the authorization code and RP-Initiated Logout flows. The URI in the
Locationheader can be defined in properties and overridden by the frontend with headers or query parameters, and the HTTP status can be moved to the2xxrange so that the frontend observes the response and handles the redirection in JavaScript instead of letting the browser follow with a cross-origin Ajax request. - exposing the CSRF token as a cookie accessible to a single-page application
- logging out from authorization servers which do not strictly implement RP-Initiated Logout, Auth0 and Amazon Cognito for instance
- activating and configuring Back-Channel Logout
- adding extra parameters to authorization and token requests, like the
audiencerequired by Auth0 or Keycloak'skc_idp_hint - CORS configuration, and anonymous preflight requests
The reference pages for resource servers and clients document every property, with an FAQ covering Auth0 audiences, Keycloak dynamic realms, Microsoft Entra ID caveats, proxies in front of the token endpoint, and cookies.
At an age where OpenAPI specs are generated from REST API source code, and client code generated from those specs, what is left to do for inter-service calls is configuring the clients: Basic or Bearer authorization, HTTP proxies, timeouts, SSL bundles, and the choice of the underlying HTTP client library. All of it is verbose and error-prone in Java, and all of it is a few properties here:
com:
c4-soft:
springaddons:
rest:
client:
# Exposes a RestClient bean named machinClient (or a WebClient in a reactive app)
machin-client:
base-url: ${machin-api}
authorization:
oauth2:
# Authorize outgoing requests with the Bearer token in the security context
forward-bearer: true
bidule-client:
base-url: ${bidule-api}
# Expose the builder instead of an already built client, to fine tune it
expose-builder: true
authorization:
oauth2:
# Authorize outgoing requests with a token obtained from a client registration
oauth2-registration-id: bidule-registrationThese beans can be auto-wired in any @Component or used to back @HttpExchange proxies. An HTTP Service group can be backed by an auto-configured client, so that the generated proxies share its base URL, headers, authorization, and request factory:
@Configuration
@ImportHttpServices(group = "keycloak-group", types = {UsersApi.class, ClientRoleMappingsApi.class})
class HttpServicesConfiguration {
}com:
c4-soft:
springaddons:
rest:
client:
keycloak-client:
base-url: https://${cn}:3643/auth
ssl-bundle: self-signed
headers:
Accept:
- application/json
- application/problem+json
authorization:
oauth2:
oauth2-registration-id: rest-api
group:
keycloak-group:
client: keycloak-clientProxy configuration is applied by default as soon as the HTTP_PROXY and NO_PROXY environment variables are set, and can be overridden or disabled with properties. This starter is usable on its own, including in non-web applications. See the reference for the bean naming rules and the advanced options.
Testing access control requires a security context populated with a fine-tuned Authentication. spring-security-test provides MockMvc request post-processors and WebTestClient mutators, but they work only in the context of a request, which limits them to controllers. To test any kind of @Component, including @Service and @Repository, the options are to build the security context by hand, or to let annotations do it.
The notable difference with spring-security-test is the one mentioned in the quickstart: the authentication converter defined in the security configuration is ignored by the post-processors and mutators. Building the security context in a resource server with a JWT decoder has three steps:
- the Bearer string is decoded, validated, and turned into a
org.springframework.security.oauth2.jwt.Jwtby aJwtDecoder - that
Jwtis turned into anAbstractAuthenticationTokenby an authentication converter, which is where claims become authorities and where a customAuthenticationimplementation is chosen - the
Authenticationis put in the security context
@WithJwt mocks step 1 only: it builds a stub Jwt from a JSON payload in test resources and hands it to the authentication converter. The .jwt() post-processor jumps to step 3 and builds the Authentication itself, so the conversion logic is never exercised. Note that this requires the authentication converter to be exposed as a @Bean rather than inlined as a lambda in the filter chain definition, which spring-addons-starter-oidc does out of the box.
Reference: the annotations, the test slices, and this Baeldung article.
A few weeks of trial and error can save fifteen minutes of reading a page. The documentation site is the complete reference, the samples are runnable, and three tutorials from this repo now live on Baeldung:
- Getting started with Keycloak and Spring Boot
- Creating an OAuth2 BFF with
spring-cloud-gatewayand consuming it from a single-page application - Testing access control with mocked OAuth2 authentications
Also useful: the runnable samples, the release notes, llms.txt when a coding assistant is involved, contributing, and the Maven Central reminders for maintainers.