diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java index 1a10cb4b..201387e3 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java @@ -19,6 +19,14 @@ import java.net.URI; import java.util.LinkedHashMap; import java.util.Map; +import javax.servlet.AsyncContext; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.core.annotation.AnnotationUtils; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.security.authentication.AuthenticationManager; @@ -26,6 +34,9 @@ import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.Transient; +import org.springframework.security.core.context.SecurityContext; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationProvider; @@ -39,6 +50,10 @@ import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenR import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.authentication.HttpStatusEntryPoint; import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter; +import org.springframework.security.web.context.HttpRequestResponseHolder; +import org.springframework.security.web.context.HttpSessionSecurityContextRepository; +import org.springframework.security.web.context.SaveContextOnUpdateOrErrorResponseWrapper; +import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; @@ -212,6 +227,105 @@ public final class OAuth2AuthorizationServerConfigurer securityContextCaptor = + ArgumentCaptor.forClass(org.springframework.security.core.context.SecurityContext.class); + verify(securityContextRepository, times(2)).saveContext(securityContextCaptor.capture(), any(), any()); + securityContextCaptor.getAllValues().forEach(securityContext -> + assertThat(securityContext.getAuthentication()).isInstanceOf(UsernamePasswordAuthenticationToken.class)); + reset(securityContextRepository); + + String authorizationCode = extractParameterFromRedirectUri(mvcResult.getResponse().getRedirectedUrl(), "code"); + OAuth2Authorization authorizationCodeAuthorization = this.authorizationService.findByToken(authorizationCode, AUTHORIZATION_CODE_TOKEN_TYPE); + + this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI) + .params(getTokenRequestParameters(registeredClient, authorizationCodeAuthorization)) + .param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId()) + .param(PkceParameterNames.CODE_VERIFIER, S256_CODE_VERIFIER)) + .andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store"))) + .andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache"))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.access_token").isNotEmpty()) + .andExpect(jsonPath("$.token_type").isNotEmpty()) + .andExpect(jsonPath("$.expires_in").isNotEmpty()) + .andExpect(jsonPath("$.refresh_token").doesNotExist()) + .andExpect(jsonPath("$.scope").isNotEmpty()); + + verify(securityContextRepository, never()).saveContext(any(), any(), any()); + } + private static MultiValueMap getAuthorizationRequestParameters(RegisteredClient registeredClient) { MultiValueMap parameters = new LinkedMultiValueMap<>(); parameters.set(OAuth2ParameterNames.RESPONSE_TYPE, OAuth2AuthorizationResponseType.CODE.getValue()); @@ -739,6 +797,29 @@ public class OAuth2AuthorizationCodeGrantTests { } + @EnableWebSecurity + static class AuthorizationServerConfigurationWithSecurityContextRepository extends AuthorizationServerConfiguration { + // @formatter:off + @Bean + public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { + OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = + new OAuth2AuthorizationServerConfigurer<>(); + RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher(); + + http + .requestMatcher(endpointsMatcher) + .authorizeRequests(authorizeRequests -> + authorizeRequests.anyRequest().authenticated() + ) + .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher)) + .securityContext(securityContext -> + securityContext.securityContextRepository(securityContextRepository)) + .apply(authorizationServerConfigurer); + return http.build(); + } + // @formatter:on + } + @EnableWebSecurity @Import(OAuth2AuthorizationServerConfiguration.class) static class AuthorizationServerConfigurationWithJwtEncoder extends AuthorizationServerConfiguration { diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcUserInfoTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcUserInfoTests.java index b1569f96..d5336832 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcUserInfoTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcUserInfoTests.java @@ -26,6 +26,8 @@ import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.source.ImmutableJWKSet; import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.proc.SecurityContext; +import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; @@ -62,10 +64,17 @@ import org.springframework.security.oauth2.server.authorization.oidc.authenticat import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationToken; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.context.HttpSessionSecurityContextRepository; +import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultMatcher; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.ResultMatcher.matchAll; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -79,6 +88,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. */ public class OidcUserInfoTests { private static final String DEFAULT_OIDC_USER_INFO_ENDPOINT_URI = "/userinfo"; + private static SecurityContextRepository securityContextRepository; @Rule public final SpringTestRule spring = new SpringTestRule(); @@ -92,6 +102,16 @@ public class OidcUserInfoTests { @Autowired private OAuth2AuthorizationService authorizationService; + @BeforeClass + public static void init() { + securityContextRepository = spy(new HttpSessionSecurityContextRepository()); + } + + @Before + public void setup() { + reset(securityContextRepository); + } + @Test public void requestWhenUserInfoRequestGetThenUserInfoResponse() throws Exception { this.spring.register(AuthorizationServerConfiguration.class).autowire(); @@ -140,6 +160,25 @@ public class OidcUserInfoTests { // @formatter:on } + // gh-482 + @Test + public void requestWhenUserInfoRequestThenBearerTokenAuthenticationNotPersisted() throws Exception { + this.spring.register(AuthorizationServerConfigurationWithSecurityContextRepository.class).autowire(); + + OAuth2Authorization authorization = createAuthorization(); + this.authorizationService.save(authorization); + + OAuth2AccessToken accessToken = authorization.getAccessToken().getToken(); + // @formatter:off + this.mvc.perform(get(DEFAULT_OIDC_USER_INFO_ENDPOINT_URI) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken.getTokenValue())) + .andExpect(status().is2xxSuccessful()) + .andExpect(userInfoResponse()); + // @formatter:on + + verify(securityContextRepository, never()).saveContext(any(), any(), any()); + } + private static ResultMatcher userInfoResponse() { // @formatter:off return matchAll( @@ -257,6 +296,34 @@ public class OidcUserInfoTests { } } + @EnableWebSecurity + static class AuthorizationServerConfigurationWithSecurityContextRepository extends AuthorizationServerConfiguration { + + @Bean + @Override + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = + new OAuth2AuthorizationServerConfigurer<>(); + RequestMatcher endpointsMatcher = authorizationServerConfigurer + .getEndpointsMatcher(); + + // @formatter:off + http + .requestMatcher(endpointsMatcher) + .authorizeRequests(authorizeRequests -> + authorizeRequests.anyRequest().authenticated() + ) + .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher)) + .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt) + .securityContext(securityContext -> + securityContext.securityContextRepository(securityContextRepository)) + .apply(authorizationServerConfigurer); + // @formatter:on + + return http.build(); + } + } + @EnableWebSecurity static class AuthorizationServerConfiguration {