OAuth2 clients
OAuth2 clients are applications fetching tokens from an authorization server to later authorize queries to a resource server. They come in two shapes:
- with
oauth2Login: the application logs users in and keeps their tokens in a session, using the authorization code and refresh token flows. A Backend For Frontend is the typical example. - without
oauth2Login: the application fetches tokens for itself withclient_credentials, to call other APIs from a resource server, a scheduled job or a message listener.
The authorized client manager and provider, extra token request parameters, and concurrent client credentials flows concern both shapes. The other sections concern oauth2Login only.
When oauth2Login is configured on a filter chain, this filter chain has to be stateful (tokens are stored in sessions) and protected against CSRF.
Note that spring-addons-starter-oidc creates default (Reactive)OAuth2AuthorizedClientProvider and (Reactive)OAuth2AuthorizedClientManager for all OAuth2 clients (even when the clientSecurityFilterChain with oauth2Login is disabled) to handle additional parameters on token request. As usual, you can opt out of these default beans by exposing your own.
The (Reactive)OAuth2AuthorizedClientManager implementation is chosen from the flows declared with spring.security.oauth2.client.registration.* properties:
- only
authorization_coderegistrations: aDefault(Reactive)OAuth2AuthorizedClientManager, which stores the authorized clients in the(Server)OAuth2AuthorizedClientRepositoryand requires anHttpServletRequest(or aServerWebExchange) - no
authorization_coderegistration, which is the case of a resource server consuming another resource server withclient_credentials: anAuthorizedClientService(Reactive)OAuth2AuthorizedClientManager, which stores the authorized clients in the(Reactive)OAuth2AuthorizedClientServiceand needs no request at all. Tokens are then reused across requests instead of being requested again for each of them, which is what Spring Boot defaults lead to in a stateless application. - both: a
PerRegistration(Reactive)OAuth2AuthorizedClientManagerdelegating to one of the two above, depending on the flow of the registration to authorize
When at least one registration uses authorization_code, a HttpSessionOAuth2AuthorizedClientRepository (or WebSessionServerOAuth2AuthorizedClientRepository in a reactive application) is also exposed, in place of the Spring Boot default which keeps the authorized clients in a map local to the JVM. This matters for a BFF: tokens follow the session, which means that they are replicated by Spring Session and released when the session is closed.
Client Security(Web)FilterChain
A client filter chain with oauth2Login is created if (and only if) all the following conditions are met:
spring-boot-starter-oauth2-clientis on the classpathspring.security.oauth2.client.registrationcontains at least one entry withauthorization-grant-type=authorization_codecom.c4-soft.springaddons.oidc.client.security-matchersis not empty
This filter chain is configured with the following defaults:
@Order(Ordered.LOWEST_PRECEDENCE - 1)(just before the resource server one, which is the default chain)- the security-matcher in the conf is applied
- stateful (session and CSRF protection enabled)
- oauth2Login
- enabled RP-Initiated Logout (provided that the OpenID configuration exposes an end-session endpoint or that spring-addons properties for non-standard logout are there)
- disabled Back-Channel Logout
- disabled PKCE
- disabled CORS (as a reminder,
corsproperties configure a global filter) - allowed anonymous access to pre-flight requests and to all requests with a path matching an entry in
permit-all; all other requests requiring a valid authentication
Setting a Base URI for the Client
Authorization-code flow and RP-Initiated Logout involve some redirection to the authorization server and then back to the client.
Spring Security generates this redirection URIs, but sometimes, it is convenient to have it point to another host than the client itself: a gateway, reverse-proxy, ingress or whatever. com.c4-soft.springaddons.oidc.client.client-uri serves that purpose.
Authorization Code
It is possible to force PKCE usage even for confidential clients by setting com.c4-soft.springaddons.oidc.client.pkce-forced=true (By default, Spring enables PKCE only for “public” clients).
Other features are of interest mainly in the case of remote frontends connected to a Spring backend with oauth2Login (OAuth2 BFF).
Customization can be achieved by exposing a variety of beans:
PreAuthorizationCode(Server)RedirectStrategy: the default isSpringAddonsPreAuthorizationCode(Server)RedirectStrategywhich allows changing the status of the redirection to the authorization server when initiating an authorization code flow (com.c4-soft.springaddons.oidc.client.oauth2-redirections.pre-authorization-codeproperty,X-RESPONSE-STATUSheader or.response_http_statusrequest parameter). This can be of use for frontends wishing to switch the HTTP client on the fly (like a mobile app using a programmatic client with a session on the Spring client and a web-view or system browser with another session for login on the authorization server)(Server)OAuth2AuthorizationRequestResolver: the default isSpringAddons(Server)OAuth2AuthorizationRequestResolverwhich:- forces PKCE usage if the
pkce-forcedis set totrue - switches the client base URI (if requested)
- saves in session the post-login success and failure URI (provided as
X-POST-LOGIN-SUCCESS-URI&X-POST-LOGIN-FAILURE-URIheaders orpost_login_success_uri&post_login_failure_urirequest params) - adds optional request params defined under
com.c4-soft.springaddons.oidc.client.authorization-params.{registrationId}. Auth0 for instance, requires anaudienceparameter to be added to authorization request (see FAQ for a sample).
- forces PKCE usage if the
(Server)AuthenticationSuccessHandler: the default restores the post-login success URI saved in the session and applies thecom.c4-soft.springaddons.oidc.client.oauth2-redirections.post-authorization-code(Server)AuthenticationFailureHandler: the default restores the post-login failure URI saved in the session and applies thecom.c4-soft.springaddons.oidc.client.oauth2-redirections.post-authorization-code
To clarify, you can define default post-login URL in application properties and override it from the frontend at login time by setting a header or providing a request parameter. A frequent use case is to re-activate the current route in the frontend after the authorization-code flow is completed.
Last, the Spring default status for unauthorized requests (302 redirect to login) is kept, but it can be changed to anything else using com.c4-soft.springaddons.oidc.client.oauth2-redirections.authentication-entry-point. For instance, it could be wise to set it to UNAUTHORIZED on an OAuth2 BFF for single page or mobile applications, or in stateful REST API.
When the client must be authorized again (BFF)
OAuth2AuthorizedClientManager#authorize() runs inside request handlers: Spring Cloud Gateway’s TokenRelay= filter, a RestClient or a WebClient with an OAuth2 interceptor. It throws a ClientAuthorizationException when the client must be authorized again: a ClientAuthorizationRequiredException when there is no authorized client for the registration, a plain ClientAuthorizationException when the authorization server refuses the refresh (invalid_grant: SSO session or refresh token expired; the authorized client has already been removed by the failure handler). Spring Security answers the first with a redirection to the authorization endpoint, which a single page or mobile application can’t follow, and does not handle the second at all (it is not an AuthenticationException, so it surfaces as a 500). In both cases, the session keeps its OAuth2AuthenticationToken: the user still looks logged in (a /me endpoint keeps answering them) while every relayed call fails.
With com.c4-soft.springaddons.oidc.client.client-authorization-required.enabled=true, a ClientAuthorizationException of any subclass and with any error code, for an authorization_code registration, logs the session out (session invalidated, security context cleared) and the request is answered with 401, without Location and without resolving an authorization request. The frontend then decides when to initiate a new login, as it does for the 401 of the authentication entry point. The default is false, because server-rendered applications want the redirection. The bff sample enables it.
A few details:
- Exceptions for registrations with another grant type (
client_credentialsfor instance) are left untouched: the user logging in again would not fix a server-side credential problem. - When the user is logged in with
oauth2Login, only exceptions for the registration they logged in with log them out. For anotherauthorization_coderegistration (one needing its own consent, for instance), both subclasses are turned into aClientAuthorizationRequiredExceptionfor that registration: the session is kept and Spring Security initiates the authorization of that registration (incremental authorization), with thepre-authorization-codestatus. - The logout invalidates the session and clears the security context in the default repositories (request attribute and HTTP session). An application which sets its own
SecurityContextRepositoryon the client filter chain should not enable this property, or should clear that repository itself. - The session cookie now points to a session which no longer exists. In servlet applications, the next request presenting it is handled as an invalid session: with the default
invalid-session.status, a302to the same URI, which then proceeds anonymously. Setinvalid-session.status: UNAUTHORIZEDandinvalid-session.pass-through-patterns: /mefor/meto answer “anonymous” while the other routes answer401, as in thebffsample. - Any error code is handled, including those of a transient failure of the authorization server (
server_error, a5xxon the token endpoint): for the login registration, the user is asked to log in again rather than getting a500.
Mind that an exception thrown by a handler goes through the DispatcherServlet exception resolvers before any filter sees it: a @ControllerAdvice or a HandlerExceptionResolver handling every exception swallows it. Leave ClientAuthorizationException to the filter.
Similarly, a request carrying a post-login URI outside post-login-allowed-uri-patterns is answered with 400 (see Post login/logout URIs).
Both responses are written by the OAuth2ClientRefusalWriter (servlet) or ServerOAuth2ClientRefusalWriter (reactive) bean, which writes the status alone by default. Expose your own to write a body, an RFC 9457 problem for instance:
@Bean
OAuth2ClientRefusalWriter problemRefusalWriter(JsonMapper jsonMapper) {
return (request, response, refusal) -> {
// refusal.kind() is AUTHORIZATION_REQUIRED or INVALID_POST_LOGIN_URI, refusal.cause() the exception
final var problem = ProblemDetail.forStatusAndDetail(refusal.status(), refusal.cause().getMessage());
response.setStatus(refusal.status().value());
response.setContentType(MediaType.APPLICATION_PROBLEM_JSON_VALUE);
jsonMapper.writeValue(response.getOutputStream(), problem);
};
}
User identity after a token refresh
A token refresh updates the OidcUser and the authorities in the session, as Spring Security’s default authorized client manager does. A role granted or revoked after the login is then visible in the session (and in a /me endpoint reading it) as soon as the access token is refreshed, instead of after the next login.
In servlet applications, the refresh_token providers built by PerRegistrationOAuth2AuthorizedClientProvider publish an OAuth2AuthorizedClientRefreshedEvent through the application’s ApplicationEventPublisher, which the context sets on this provider when it is a bean (the auto-configured one, or one of your own). With oauth2Login, Spring Security listens to it in the request thread that ran the refresh (the application event multicaster must deliver events synchronously, which is the default, and rethrow listener exceptions). It validates the refreshed ID token against the login one (iss, sub, aud, iat, auth_time and nonce, as OpenID Connect Core 12.2 requires), loads the user with the OidcUserService, maps the authorities with the GrantedAuthoritiesMapper (the claims in the refreshed ID token, with this starter’s default), and saves the new OAuth2AuthenticationToken in the session. This happens only when the response to the refresh contains an ID token and the openid scope, for the registration the user logged in with, and when the authentication in the session is an OAuth2AuthenticationToken (not a subclass of it). When the registration has a user-info-uri (which Spring Boot reads from the OpenID configuration of an issuer-uri), the default OidcUserService calls the user info endpoint at each refresh.
When the refreshed identity is refused (an ID token which does not match the login one, which can’t be decoded, a failing user info request, or an OidcUserService of the application refusing the user), the AuthenticationException thrown by the listener is rethrown as a ClientAuthorizationException with the invalid_grant error (the original error is in the description, the original exception is suppressed), with no AuthenticationException in its cause chain. The request is then handled as a refused refresh: the authorization failure handler removes the authorized client and, with client-authorization-required.enabled=true, the session is logged out and the request answered with 401 (see the previous section). With the default false, the request fails with a 500, the session keeps the identity of the login, and the next request needing the authorized client is redirected to the authorization endpoint. Without this translation, Spring Security’s ExceptionTranslationFilter would start the authentication entry point and leave the session logged in, with an authorized client holding a spent refresh token.
In reactive applications, Spring Security’s RefreshTokenReactiveOAuth2AuthorizedClientProvider does the same with its RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler. The provider auto-configured by this starter sets that handler with the beans oauth2Login uses at login: the GrantedAuthoritiesMapper, the ReactiveOAuth2UserService<OidcUserRequest, OidcUser> (or a default one delegating to your ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User>), and the ReactiveJwtDecoderFactory<ClientRegistration>. They are resolved at the first refresh, and Spring Security’s defaults stand in for the missing ones. A refused refreshed identity is translated the same way as in servlet applications. A PerRegistrationReactiveOAuth2AuthorizedClientProvider bean of your own gets Spring Security’s defaults, the authorities mapper included (the refreshed authorities are not the ones mapped at login), unless you set the handler (and, if you define them, its user service and ID token decoder factory):
@Bean
ReactiveOAuth2AuthorizedClientProvider oauth2AuthorizedClientProvider(SpringAddonsOidcProperties addonsProperties,
ReactiveClientRegistrationRepository clientRegistrationRepository, GrantedAuthoritiesMapper authoritiesMapper) {
final var refreshTokenSuccessHandler = new RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler();
refreshTokenSuccessHandler.setAuthoritiesMapper(authoritiesMapper);
final var provider = new PerRegistrationReactiveOAuth2AuthorizedClientProvider(clientRegistrationRepository, addonsProperties);
provider.setRefreshTokenSuccessHandler(refreshTokenSuccessHandler);
return provider;
}
RP-Initiated Logout
Most OpenID Providers implement the RP-Initiated Logout and expose an end_session_endpoint in OpenID configuration, but some, like Auth0 and Cognito don’t: they have a logout mechanism which works mostly like RP-Initiated Logout but with an endpoint URI and request params to find in docs.
Sample for registrations called cognito-user and auth0-user:
com:
c4-soft:
springaddons:
oidc:
client:
oauth2-logout:
cognito-user:
uri: https://spring-addons.auth.us-west-2.amazoncognito.com/logout
client-id-request-param: client_id
post-logout-uri-request-param: logout_uri
auth0-user:
uri: ${auth0-issuer}v2/logout
client-id-request-param: client_id
post-logout-uri-request-param: returnTo
Back-Channel Logout
Back-Channel Logout is an OpenID standard that allows a client to be notified by the authorization server of logout events initiated by another client. It is some “single sign-out” for systems with Single Sign On (SSO). Keycloak is a sample of an OpenID Provider capable of emitting Back-Channel Logout to a Spring client (like a Gateway used as an OAuth2 BFF). It is disabled by default. The default internal logout URI and session cookie name can be overriden in properties:
com:
c4-soft:
springaddons:
oidc:
client:
back-channel-logout:
enabled: true
# Those two are optional, defaults should work in most scenarios
internal-logout-uri: ${gateway-uri}/logout/connect/back-channel/quiz-bff
cookie-name: JSESSION-ID
Authorities Converter
By default, a GrantedAuthoritiesMapper using the authorities converter bean in the application context. The default for this authorities converter is shared with resource servers: ConfigurableClaimSetAuthoritiesConverter. The configuration for this converter is resolved by the OpenidProviderPropertiesResolver in the context.
CSRF protection
Requests to an OAuth2 client are authorized with session cookies, which exposes it to CSRF attacks. Consequently, CSRF protection should always be enabled on OAuth2 clients.
The default with spring-addons-starter-oidc is the same as with spring-boot-starter-oauth2-client (session).
When setting com.c4-soft.springaddons.oidc.client.csrf=cookie-accessible-from-js, as needed by single-page and mobile applications, the CSRF token is exposed in a token and the required filter is registered.
Application configuration can get complete control over the CSRF cookie and repo by exposing a Cookie[Server]CsrfTokenRepositoryPostProcessor. Sample for a servlet app:
@Bean
CookieCsrfTokenRepositoryPostProcessor csrfCookiePostProcessor() {
return csrfCookieRepo -> {
csrfCookieRepo.setCookieCustomizer(csrfCookie -> csrfCookie.sameSite("Strict"));
return csrfCookieRepo;
};
}
In reactive applications, the CSRF token is deferred and the cookie is written only when something subscribes to it: this is done by a WebFilter bean named csrfCookieWebFilter. As WebFilter is too broad a type to back off on, this default is conditional on the bean name: to replace it, name yours csrfCookieWebFilter, otherwise both filters run. The resource server filter chain registers the same bean under the same name.
Access Control
The default access rule is set to isAuthenticated() with two exceptions:
- routes matching the path-matchers listed in
permit-allproperty for which anonymous requests are allowed - pre-flight requests, unless disabled in
corsproperties (OPTIONSrequests to routes matching the path-matchers listed in CORS properties).
The most convenient way to define fine-grained access control is probably to @Enable(Reactive)MethodSecurity and to decorate @RestController methods with @PreAuthorize.
For those preferring access control in configuration (or when you don’t write the endpoint yourself), you can expose a @Bean of type ClientExpressionInterceptUrlRegistryPostProcessor or ClientAuthorizeExchangeSpecPostProcessor.
Post-Process the Client Filer-Chain
By exposing a Client(Server)HttpSecurityPostProcessor bean, you get complete control of the (Server)HttpSecurity configured in the clientSecurityFilterChain just before it is built. This allows changing anything that was pre-configured.
Add Parameters to Token Requests
Some OpenID Providers require some extra parameters on the token endpoint. Auth0 for instance expects an audience parameter with client-credential token requests. Such parameters can be defined under com.c4-soft.springaddons.oidc.client.token-params.{registrationId} (see FAQ for a sample).
Post login/logout URIs
Post login URIs can be provided by the frontend with X-POST-LOGIN-SUCCESS-URI & X-POST-LOGIN-FAILURE-URI headers or post_login_success_uri & post_login_failure_uri with the request initiating authorization-code flow. Some defaults can be provided with post-login-redirect-host and post-login-redirect-path application properties.
Similarly, Post logout URI can be provided by the frontend with X-POST-LOGOUT-SUCCESS-URI header or post_logout_success_uri with the request initiating RP-Initiated Logout. This URI will be added as a request parameter to the Location header redirecting to the authorization server after the session is closed on the OAuth2 client. Some defaults can be provided with post-logout-redirect-host and post-logout-redirect-path application properties.
Since 8.1.13, to prevent Open Redirect (CWE-601) attacks, the post login/logout URIs must be part of whitelists defined with post-login-allowed-uri-patterns and post-logout-allowed-uri-patterns properties. For backward compatibility, if these properties are left blank, all post login/logout URIs composed of only a path (no scheme / authority) as well as those with the same scheme and authority as the client-uri property are allowed. Whatever the patterns, scheme-relative URIs (starting with //, which user-agents resolve to another host) are always refused.
Sample configuration stricter than defaults:
reverse-proxy: https://localhost
com:
c4-soft:
springaddons:
oidc:
client:
client-uri: ${reverse-proxy}/bff
post-login-allowed-uri-patterns:
# Each entry is compiled into a java.util.regex.Pattern
# The following is stricter than the defaults, which are ^${reverse-proxy}(/.*)?$ and ^/.*$
- ^${reverse-proxy}/ui(/.*)?$
- ^/ui(/.*)?$
post-login-redirect-path: /ui/greet
post-logout-allowed-uri-patterns:
- ^${reverse-proxy}/ui(/.*)?$
- ^/ui(/.*)?$
post-logout-redirect-path: /ui/
Each post-login-allowed-uri-patterns/post-logout-allowed-uri-patterns entry is compiled into a java.util.regex.Pattern.
post-login-redirect-host, post-login-redirect-path, post-logout-redirect-host, and post-logout-redirect-path are validated at startup. An exception is thrown in case of mismatch.
A post-login URI (header or request parameter) which does not match post-login-allowed-uri-patterns is answered with 400, without Location, without calling the oauth2Login failure handler and without opening or changing the session. The response is written by the OAuth2ClientRefusalWriter (servlet) or ServerOAuth2ClientRefusalWriter (reactive) bean described in When the client must be authorized again. A post-logout URI which does not match post-logout-allowed-uri-patterns is refused by the logout success handler with an InvalidRedirectionUriException.
Concurrent Refresh Token Flows
Most authorization servers rotate refresh tokens: the one which was used is revoked as soon as a new one is issued. When a user-agent sends parallel requests while the access token in session is expired, Spring Security fires one refresh_token flow per request, only one of them can succeed, and all the other requests are answered with a 401. Two requests sent in less time than a token request takes are enough for this to happen, so this is not limited to high throughput applications: a single page application refreshing a few widgets at once is a typical victim. This is a known limitation of Spring Security, see spring-security#15145.
To work around it, spring-addons-starter-oidc decorates the RefreshToken(Reactive)OAuth2AuthorizedClientProvider it builds with a SingleRefreshTokenFlow(Reactive)OAuth2AuthorizedClientProvider: the first request to reach the provider runs the flow and the others wait for its result. The authorization server sees a single token request and the refresh token is spent exactly once.
Flows are keyed with a digest of everything which defines the token request to run: the client registration ID, the principal name, the access and refresh token values, and the requested scopes, if any. Two requests share a flow if and only if they would send the exact same payload to the token endpoint, which, with a session scoped (Server)OAuth2AuthorizedClientRepository, means “requests from the same session”. Requests from other sessions hold other refresh tokens and keep being refreshed in parallel.
The result of a flow is also shared for a short while after it completed. This covers the requests which had loaded the authorized client from the session just before the refreshed one was saved there: without it, they would replay a refresh token which is already spent. Failures are shared too, because replaying a refresh token which the authorization server already rotated can have it revoke the whole token family.
Defaults should fit most applications, but everything is configurable:
com:
c4-soft:
springaddons:
oidc:
client:
single-refresh-token-flow:
# set to false to restore the Spring Security behavior (one flow per request)
enabled: true
# how long a request waits for the flow it joined before giving up with a server_error
timeout: PT30S
# how long the result of a successful flow is shared. Keep it well below the access token lifespan.
success-caching-duration: PT10S
# how long the failure of a flow is shared. Set to PT0S to have each request run its own flow after a failure.
error-caching-duration: PT10S
This applies to the (Reactive)OAuth2AuthorizedClientProvider auto-configured by this starter. If you expose your own, decorate its refresh_token provider yourself:
@Bean
OAuth2AuthorizedClientProvider oauth2AuthorizedClientProvider(SpringAddonsOidcProperties addonsProperties) {
var refreshTokenProvider = new RefreshTokenOAuth2AuthorizedClientProvider();
// further configuration of the refresh_token provider
return new DelegatingOAuth2AuthorizedClientProvider(
new AuthorizationCodeOAuth2AuthorizedClientProvider(),
new SingleRefreshTokenFlowOAuth2AuthorizedClientProvider(
refreshTokenProvider, addonsProperties.getClient().getSingleRefreshTokenFlow()));
}
Such a provider doesn’t make the user identity follow the refresh. Setting the ApplicationEventPublisher on its RefreshTokenOAuth2AuthorizedClientProvider gives Spring Security’s own behavior: a refused refreshed identity then starts the authentication entry point and leaves the session logged in. Only the refresh_token providers that PerRegistrationOAuth2AuthorizedClientProvider builds itself do both, not the custom providers passed to its constructor.
Horizontally Scaled Applications
Flows are de-duplicated inside a single JVM. Behind a load balancer without session affinity, parallel requests from one user-agent land on several instances and each runs its own flow, so the problem returns divided by the number of instances. Session affinity at the ingress removes it entirely and costs nothing, with Spring Session still earning its keep for failover and rolling restarts. That is the answer to prefer, and it is why spring-addons stops here.
Note that Spring Session cannot be pressed into service as a distributed lock: SessionRepository exposes only createSession, save, findById and deleteById, with no lock, no compare-and-swap and no version, so a “lock” written as a session attribute is a read-modify-write two instances win at once. Its default FlushMode is ON_SAVE, so the refreshed authorized client only reaches the store when the holder’s whole request ends, and SessionRepositoryFilter hands each request a snapshot it keeps reading.
If you really need cross-instance de-duplication, write your own decorator in place of SingleRefreshTokenFlow(Reactive)OAuth2AuthorizedClientProvider in the @Bean above, and back it with whatever your cluster already runs. Hints from the implementation here:
- key on the token request, not on the session: a digest of the registration ID, the principal name, the access and refresh token values, and the requested scopes. Two requests must share a flow if and only if they would send the same payload to the token endpoint. The session ID is neither available in the
OAuth2AuthorizationContextnor needed. - share the outcome, not just a lock. A lock alone leaves the other instances with nothing to read: they cannot get the result from the session for the
FlushModeand snapshot reasons above. - keep sharing that outcome for a few seconds after the flow completed. Requests which loaded the authorized client from the session just before the refreshed one was saved there would otherwise replay a refresh token which is already spent.
- share failures too, briefly. Replaying a refresh token the authorization server already rotated can have it revoke the whole token family.
- put an expiry on everything: a lease on the lock, so an instance going down does not block the others, and a retention on the outcome, so nothing outlives the tokens it holds.
- store only the principal name and the tokens, never the
ClientRegistration, whose serialization carries the client secret. Rebuild theOAuth2AuthorizedClientaround the registration you already have in the context. - treat a store failure as a miss: log it and run the flow locally. Degrading to one flow per instance is no worse than not having a store at all, and much better than failing authorization.
- raise
server_errorand notinvalid_grantwhen a request gives up waiting.invalid_grantmakesRemoveAuthorizedClientOAuth2AuthorizationFailureHandlerevict the authorized client from the session, and nothing proved the refresh token invalid.
Concurrent Client Credentials Flows
The AuthorizedClientService(Reactive)OAuth2AuthorizedClientManager used for client_credentials registrations loads the authorized client from the (Reactive)OAuth2AuthorizedClientService, asks the provider to authorize it, and saves the result. These operations are not atomic: when the access token is expired (or not acquired yet), each concurrent request sends its own token request. A batch running hundreds of jobs in parallel, or a resource server under load calling another one, fires as many calls to the token endpoint as there are concurrent requests. Spring Security leaves this synchronization to the application, see spring-security#11461.
Nothing is rotated with client_credentials, so this does not fail requests like concurrent refresh_token flows do, but it wastes token requests and can get an application rate-limited by its authorization server. spring-addons-starter-oidc decorates the ClientCredentials(Reactive)OAuth2AuthorizedClientProvider it builds with a SingleClientCredentialsFlow(Reactive)OAuth2AuthorizedClientProvider, sharing the mechanism described in the previous section: the first request to reach the provider runs the flow and the others wait for its result. Flows are keyed the same way (registration ID, principal name, current access token value if any, and requested scopes), so requests for other registrations or principals keep running in parallel. Unlike a synchronized block around authorize(), requests holding a valid token are not serialized, and in a servlet application running on virtual threads, no carrier thread is pinned while the token request is pending.
Defaults are shorter than for refresh_token, because the new authorized client is saved in the service right after the flow completes:
com:
c4-soft:
springaddons:
oidc:
client:
single-client-credentials-flow:
# set to false to restore the Spring Security behavior (one flow per request)
enabled: true
# how long a request waits for the flow it joined before giving up with a server_error
timeout: PT30S
# how long the result of a successful flow is shared with requests still holding the expired authorized client
success-caching-duration: PT1S
# how long the failure of a flow is shared, sparing a failing authorization server one token request per concurrent request
error-caching-duration: PT5S
If you expose your own OAuth2AuthorizedClientProvider, decorate its client_credentials provider yourself:
@Bean
OAuth2AuthorizedClientProvider oauth2AuthorizedClientProvider(SpringAddonsOidcProperties addonsProperties) {
var clientCredentialsProvider = new ClientCredentialsOAuth2AuthorizedClientProvider();
// further configuration of the client_credentials provider
return new SingleClientCredentialsFlowOAuth2AuthorizedClientProvider(
clientCredentialsProvider, addonsProperties.getClient().getSingleClientCredentialsFlow());
}
Flows are de-duplicated inside a single JVM: each instance of a horizontally scaled application gets its own token, which is usually fine.