Suppose we have a confidential OAuth2 Client performing authorization against the Authorization Server using authorization code grant type.
Here's a minimal reproducible example.
Client application is running on port 7070
, authorization server on 8080
.
Client configuration:
@Configuration
public class ClientsConfig {
@Bean
public ClientRegistration mainWebClient() {
return ClientRegistration
.withRegistrationId("main-client")
.clientId("test_web_client")
.clientName("test_web_client")
.clientSecret("secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.scope(OidcScopes.OPENID)
.authorizationUri("http://localhost:8080/oauth2/authorize")
.tokenUri("http://localhost:8080/oauth2/token")
.redirectUri("http://localhost:7070/login/oauth2/code/main-client")
.build();
}
@Bean
public ClientRegistrationRepository clientRegistrationRepository() {
return new InMemoryClientRegistrationRepository(
mainWebClient()
);
}
@Bean
public OAuth2AuthorizedClientManager oauth2AuthorizedClientManager(
OAuth2AuthorizedClientRepository authorizedClientRepository) {
var clientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository(),
authorizedClientRepository
);
clientManager
.setAuthorizedClientProvider(
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.build()
);
return clientManager;
}
}
SecurityFilterChain
- configured login page and for simplicity every request is permitted (OAuth2AuthorizedClient
client injected in controller as a method argument would trigger the authorization flow):
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.oauth2Client(withDefaults())
.oauth2Login(oauth2Login ->
oauth2Login.loginPage("/oauth2/authorization/main-client")
);
http.authorizeHttpRequests(authorize ->
authorize.anyRequest().permitAll()
);
return http.build();
}
}
Simplistic controller with OAuth2AuthorizedClient
injected:
@RestController
public class TokenController {
@GetMapping("/hello")
public String getToken(
@RegisteredOAuth2AuthorizedClient("main-client")
OAuth2AuthorizedClient client) {
return "access token: " +
client.getAccessToken().getTokenType() + " " +
client.getAccessToken().getTokenValue();
}
}
It's important to emphasize that the problem is rooted in the client's configuration.
Authorization server does its job flawlessly issuing codes and tokens (specifically for the client shown above, server behavior was verified using browser to obtain authorization code and Postman to exchange the code for an access token).
For some reason, the client application is not capable to complete the authorization flow. It receives an authorization code from the authorization server but fails to exchange it.
Here's what happens. Firstly, as expected, the unauthorized request gets redirected to the authorization server running on port 8080.
User credentials provided to the server, and it responds with the authorization code, after receiving the code authorization attempt fails and client application perform the second redirect to the authorization server.
In the client's logs I found that an OAuth2AuthenticationException
with the error-code authorization_request_not_found
occurred in the OAuth2LoginAuthenticationFilter
while invoking attemptAuthentication()
method.
Here are log-messages starting from the point when authorization code was received:
2023-06-15T22:28:23.268+03:00 DEBUG 7732 --- [nio-7070-exec-3] o.s.security.web.FilterChainProxy : Securing GET /login/oauth2/code/main-client?code=Dpv-xhYJZcdp4T6B3VoRQZ1UzXR7w9SR2rWYiEUUO5XtGWwJvzxV-WqR2hVwG5Sc1OmsciBkGau65d4Lf7RX5YIoTseudmI6qJbdsBMM7dN2iHnDwXLnsSCAu0WhbS6_&state=EKLnVsMKTT6jx4RrBn3wCbJ-a7mT0ee4xHwgAmEPI5o%3D 2023-06-15T22:28:23.269+03:00 TRACE 7732 --- [nio-7070-exec-3] o.s.security.web.FilterChainProxy : Invoking DisableEncodeUrlFilter (1/15) 2023-06-15T22:28:23.269+03:00 TRACE 7732 --- [nio-7070-exec-3] o.s.security.web.FilterChainProxy : Invoking WebAsyncManagerIntegrationFilter (2/15) 2023-06-15T22:28:23.269+03:00 TRACE 7732 --- [nio-7070-exec-3] o.s.security.web.FilterChainProxy : Invoking SecurityContextHolderFilter (3/15) 2023-06-15T22:28:23.269+03:00 TRACE 7732 --- [nio-7070-exec-3] o.s.security.web.FilterChainProxy : Invoking HeaderWriterFilter (4/15) 2023-06-15T22:28:23.269+03:00 TRACE 7732 --- [nio-7070-exec-3] o.s.security.web.FilterChainProxy : Invoking CsrfFilter (5/15) 2023-06-15T22:28:23.269+03:00 TRACE 7732 --- [nio-7070-exec-3] o.s.security.web.csrf.CsrfFilter : Did not protect against CSRF since request did not match CsrfNotRequired [TRACE, HEAD, GET, OPTIONS] 2023-06-15T22:28:23.269+03:00 TRACE 7732 --- [nio-7070-exec-3] o.s.security.web.FilterChainProxy : Invoking LogoutFilter (6/15) 2023-06-15T22:28:23.269+03:00 TRACE 7732 --- [nio-7070-exec-3] o.s.s.w.a.logout.LogoutFilter : Did not match request to Ant [pattern='/logout', POST] 2023-06-15T22:28:23.269+03:00 TRACE 7732 --- [nio-7070-exec-3] o.s.security.web.FilterChainProxy : Invoking OAuth2AuthorizationRequestRedirectFilter (7/15) 2023-06-15T22:28:23.269+03:00 TRACE 7732 --- [nio-7070-exec-3] o.s.security.web.FilterChainProxy : Invoking OAuth2AuthorizationRequestRedirectFilter (8/15) 2023-06-15T22:28:23.270+03:00 TRACE 7732 --- [nio-7070-exec-3] o.s.security.web.FilterChainProxy : Invoking OAuth2LoginAuthenticationFilter (9/15) 2023-06-15T22:28:23.271+03:00 TRACE 7732 --- [nio-7070-exec-3] .s.o.c.w.OAuth2LoginAuthenticationFilter : Failed to process authentication request
org.springframework.security.oauth2.core.OAuth2AuthenticationException: [authorization_request_not_found] at org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter.attemptAuthentication(OAuth2LoginAuthenticationFilter.java:173) ~[spring-security-oauth2-client-6.1.0.jar:6.1.0] at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:231) ~[spring-security-web-6.1.0.jar:6.1.0] at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:221) ~[spring-security-web-6.1.0.jar:6.1.0]
*** few lines omitted ***
2023-06-15T22:28:23.272+03:00 TRACE 7732 --- [nio-7070-exec-3] .s.o.c.w.OAuth2LoginAuthenticationFilter : Cleared SecurityContextHolder 2023-06-15T22:28:23.272+03:00 TRACE 7732 --- [nio-7070-exec-3] .s.o.c.w.OAuth2LoginAuthenticationFilter : Handling authentication failure 2023-06-15T22:28:23.272+03:00 DEBUG 7732 --- [nio-7070-exec-3] o.s.s.web.DefaultRedirectStrategy : Redirecting to /oauth2/authorization/main-client?error
*** few lines omitted ***
2023-06-15T22:28:23.304+03:00 TRACE 7732 --- [nio-7070-exec-4] o.s.security.web.FilterChainProxy : Invoking OAuth2AuthorizationRequestRedirectFilter (7/15) 2023-06-15T22:28:23.305+03:00 DEBUG 7732 --- [nio-7070-exec-4] o.s.s.web.DefaultRedirectStrategy : Redirecting to http://localhost:8080/oauth2/authorize?response_type=code&client_id=test_web_client&scope=openid&state=norrUwgtNOpX4olrhN7-nPyVoBhmGYWSU5NJppnjxGA%3D&redirect_uri=http://localhost:7070/login/oauth2/code/main-client&nonce=6fiTfM0ul1ASzKKf581SUvy092AN4Jq1Vg_a97FMMqs 2023-06-15T22:28:23.305+03:00 TRACE 7732 --- [nio-7070-exec-4] o.s.s.w.header.writers.HstsHeaderWriter : Not injecting HSTS header since it did not match request to [Is Secure]
Can you please give me hint how can it be fixed?
To verify the behavior I've described, any sort of OAuth2 authorization server can be used.
Just for the sake of completeness, here's a minimal configuration of the Spring OAuth2 Authorization Server which is aware of the client shown previously.
@Configuration
public class SecurityConfig {
@Order(1)
@Bean
public SecurityFilterChain authSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.authorizationEndpoint(endPoint -> endPoint
.authenticationProviders(LocalhostCompliantValidator::apply)
)
.oidc(Customizer.withDefaults());
http.exceptionHandling(c ->
c.authenticationEntryPoint(
new LoginUrlAuthenticationEntryPoint("/login")
)
);
return http.build();
}
@Order(2)
@Bean
public SecurityFilterChain appSecurityFilterChain(HttpSecurity http) throws Exception {
http.formLogin(Customizer.withDefaults());
http.authorizeHttpRequests(auth ->
auth.anyRequest().authenticated()
);
return http.build();
}
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().build();
}
@Bean
public UserDetailsService userDetailsService() {
var user = User.withUsername("user")
.password("password")
.authorities("read", "write")
.build();
return new InMemoryUserDetailsManager(user);
}
@Bean
@SuppressWarnings("deprecation")
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient mainClient = RegisteredClient.withId("1")
.clientId("test_web_client")
.clientSecret("secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.scope(OidcScopes.OPENID)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://localhost:7070/login/oauth2/code/main-client")
.tokenSettings(
TokenSettings.builder()
.accessTokenFormat(OAuth2TokenFormat.REFERENCE)
.accessTokenTimeToLive(Duration.ofHours(24))
.build()
)
.clientSettings(
ClientSettings.builder()
.requireProofKey(false)
.requireAuthorizationConsent(false)
.build()
)
.build();
return new InMemoryRegisteredClientRepository(mainClient);
}
}
Using localhost
as a redirect URI is against specification and therefore in order to work with the client on localhost:7070
requires implementing a custom validator, which might look like this:
public class LocalhostCompliantValidator implements Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> {
public static void apply(List<AuthenticationProvider> providers) {
for (var provider: providers) {
if (provider instanceof OAuth2AuthorizationCodeRequestAuthenticationProvider oauth2Provider) {
oauth2Provider.setAuthenticationValidator(new LocalhostCompliantValidator());
}
}
}
@Override
public void accept(OAuth2AuthorizationCodeRequestAuthenticationContext context) {
var token = getAuthenticationToken(context);
boolean clientHasRequestedUri = getRegisteredUris(context)
.contains(token.getRedirectUri());
if (!clientHasRequestedUri) {
throwAuthenticationException(token);
}
}
private static Set<String> getRegisteredUris(OAuth2AuthorizationCodeRequestAuthenticationContext context) {
return context
.getRegisteredClient()
.getRedirectUris();
}
private static OAuth2AuthorizationCodeRequestAuthenticationToken getAuthenticationToken(OAuth2AuthorizationCodeRequestAuthenticationContext context) {
return context.getAuthentication();
}
private static void throwAuthenticationException(OAuth2AuthorizationCodeRequestAuthenticationToken token) {
throw new OAuth2AuthorizationCodeRequestAuthenticationException(
new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST), token
);
}
}
That is usually because of cookie overwriting, see:
https://github.com/spring-projects/spring-security/issues/5946
Add a line to hosts file:
127.0.0.1 auth-server
And then change "localhost:8080" to "auth-server:8080" in ClientsConfig.