3737import org .assertj .core .matcher .AssertionMatcher ;
3838import org .junit .After ;
3939import org .junit .AfterClass ;
40+ import org .junit .Before ;
4041import org .junit .BeforeClass ;
4142import org .junit .Rule ;
4243import org .junit .Test ;
44+ import org .mockito .ArgumentCaptor ;
4345
4446import org .springframework .beans .factory .annotation .Autowired ;
4547import org .springframework .context .annotation .Bean ;
5658import org .springframework .mock .web .MockHttpServletResponse ;
5759import org .springframework .security .authentication .AuthenticationProvider ;
5860import org .springframework .security .authentication .TestingAuthenticationToken ;
61+ import org .springframework .security .authentication .UsernamePasswordAuthenticationToken ;
5962import org .springframework .security .config .annotation .web .builders .HttpSecurity ;
6063import org .springframework .security .config .annotation .web .configuration .EnableWebSecurity ;
6164import org .springframework .security .config .annotation .web .configuration .OAuth2AuthorizationServerConfiguration ;
102105import org .springframework .security .web .authentication .AuthenticationConverter ;
103106import org .springframework .security .web .authentication .AuthenticationFailureHandler ;
104107import org .springframework .security .web .authentication .AuthenticationSuccessHandler ;
108+ import org .springframework .security .web .context .HttpSessionSecurityContextRepository ;
109+ import org .springframework .security .web .context .SecurityContextRepository ;
105110import org .springframework .security .web .util .matcher .RequestMatcher ;
106111import org .springframework .test .web .servlet .MockMvc ;
107112import org .springframework .test .web .servlet .MvcResult ;
116121import static org .mockito .ArgumentMatchers .any ;
117122import static org .mockito .ArgumentMatchers .eq ;
118123import static org .mockito .Mockito .mock ;
124+ import static org .mockito .Mockito .never ;
125+ import static org .mockito .Mockito .reset ;
126+ import static org .mockito .Mockito .spy ;
127+ import static org .mockito .Mockito .times ;
119128import static org .mockito .Mockito .verify ;
120129import static org .mockito .Mockito .when ;
121130import static org .springframework .security .test .web .servlet .request .SecurityMockMvcRequestPostProcessors .user ;
@@ -154,6 +163,7 @@ public class OAuth2AuthorizationCodeGrantTests {
154163 private static AuthenticationProvider authorizationRequestAuthenticationProvider ;
155164 private static AuthenticationSuccessHandler authorizationResponseHandler ;
156165 private static AuthenticationFailureHandler authorizationErrorResponseHandler ;
166+ private static SecurityContextRepository securityContextRepository ;
157167 private static String consentPage = "/oauth2/consent" ;
158168
159169 @ Rule
@@ -187,6 +197,7 @@ public static void init() {
187197 authorizationRequestAuthenticationProvider = mock (AuthenticationProvider .class );
188198 authorizationResponseHandler = mock (AuthenticationSuccessHandler .class );
189199 authorizationErrorResponseHandler = mock (AuthenticationFailureHandler .class );
200+ securityContextRepository = spy (new HttpSessionSecurityContextRepository ());
190201 db = new EmbeddedDatabaseBuilder ()
191202 .generateUniqueName (true )
192203 .setType (EmbeddedDatabaseType .HSQL )
@@ -197,6 +208,11 @@ public static void init() {
197208 .build ();
198209 }
199210
211+ @ Before
212+ public void setup () {
213+ reset (securityContextRepository );
214+ }
215+
200216 @ After
201217 public void tearDown () {
202218 jdbcOperations .update ("truncate table oauth2_authorization" );
@@ -615,6 +631,48 @@ public void requestWhenAuthorizationEndpointCustomizedThenUsed() throws Exceptio
615631 verify (authorizationResponseHandler ).onAuthenticationSuccess (any (), any (), eq (authorizationCodeRequestAuthenticationResult ));
616632 }
617633
634+ // gh-482
635+ @ Test
636+ public void requestWhenClientObtainsAccessTokenThenClientAuthenticationNotPersisted () throws Exception {
637+ this .spring .register (AuthorizationServerConfigurationWithSecurityContextRepository .class ).autowire ();
638+
639+ RegisteredClient registeredClient = TestRegisteredClients .registeredPublicClient ().build ();
640+ this .registeredClientRepository .save (registeredClient );
641+
642+ MvcResult mvcResult = this .mvc .perform (get (DEFAULT_AUTHORIZATION_ENDPOINT_URI )
643+ .params (getAuthorizationRequestParameters (registeredClient ))
644+ .param (PkceParameterNames .CODE_CHALLENGE , S256_CODE_CHALLENGE )
645+ .param (PkceParameterNames .CODE_CHALLENGE_METHOD , "S256" )
646+ .with (user ("user" )))
647+ .andExpect (status ().is3xxRedirection ())
648+ .andReturn ();
649+
650+ ArgumentCaptor <org .springframework .security .core .context .SecurityContext > securityContextCaptor =
651+ ArgumentCaptor .forClass (org .springframework .security .core .context .SecurityContext .class );
652+ verify (securityContextRepository , times (2 )).saveContext (securityContextCaptor .capture (), any (), any ());
653+ securityContextCaptor .getAllValues ().forEach (securityContext ->
654+ assertThat (securityContext .getAuthentication ()).isInstanceOf (UsernamePasswordAuthenticationToken .class ));
655+ reset (securityContextRepository );
656+
657+ String authorizationCode = extractParameterFromRedirectUri (mvcResult .getResponse ().getRedirectedUrl (), "code" );
658+ OAuth2Authorization authorizationCodeAuthorization = this .authorizationService .findByToken (authorizationCode , AUTHORIZATION_CODE_TOKEN_TYPE );
659+
660+ this .mvc .perform (post (DEFAULT_TOKEN_ENDPOINT_URI )
661+ .params (getTokenRequestParameters (registeredClient , authorizationCodeAuthorization ))
662+ .param (OAuth2ParameterNames .CLIENT_ID , registeredClient .getClientId ())
663+ .param (PkceParameterNames .CODE_VERIFIER , S256_CODE_VERIFIER ))
664+ .andExpect (header ().string (HttpHeaders .CACHE_CONTROL , containsString ("no-store" )))
665+ .andExpect (header ().string (HttpHeaders .PRAGMA , containsString ("no-cache" )))
666+ .andExpect (status ().isOk ())
667+ .andExpect (jsonPath ("$.access_token" ).isNotEmpty ())
668+ .andExpect (jsonPath ("$.token_type" ).isNotEmpty ())
669+ .andExpect (jsonPath ("$.expires_in" ).isNotEmpty ())
670+ .andExpect (jsonPath ("$.refresh_token" ).doesNotExist ())
671+ .andExpect (jsonPath ("$.scope" ).isNotEmpty ());
672+
673+ verify (securityContextRepository , never ()).saveContext (any (), any (), any ());
674+ }
675+
618676 private static MultiValueMap <String , String > getAuthorizationRequestParameters (RegisteredClient registeredClient ) {
619677 MultiValueMap <String , String > parameters = new LinkedMultiValueMap <>();
620678 parameters .set (OAuth2ParameterNames .RESPONSE_TYPE , OAuth2AuthorizationResponseType .CODE .getValue ());
@@ -739,6 +797,29 @@ static class ParametersMapper extends JdbcOAuth2AuthorizationService.OAuth2Autho
739797
740798 }
741799
800+ @ EnableWebSecurity
801+ static class AuthorizationServerConfigurationWithSecurityContextRepository extends AuthorizationServerConfiguration {
802+ // @formatter:off
803+ @ Bean
804+ public SecurityFilterChain authorizationServerSecurityFilterChain (HttpSecurity http ) throws Exception {
805+ OAuth2AuthorizationServerConfigurer <HttpSecurity > authorizationServerConfigurer =
806+ new OAuth2AuthorizationServerConfigurer <>();
807+ RequestMatcher endpointsMatcher = authorizationServerConfigurer .getEndpointsMatcher ();
808+
809+ http
810+ .requestMatcher (endpointsMatcher )
811+ .authorizeRequests (authorizeRequests ->
812+ authorizeRequests .anyRequest ().authenticated ()
813+ )
814+ .csrf (csrf -> csrf .ignoringRequestMatchers (endpointsMatcher ))
815+ .securityContext (securityContext ->
816+ securityContext .securityContextRepository (securityContextRepository ))
817+ .apply (authorizationServerConfigurer );
818+ return http .build ();
819+ }
820+ // @formatter:on
821+ }
822+
742823 @ EnableWebSecurity
743824 @ Import (OAuth2AuthorizationServerConfiguration .class )
744825 static class AuthorizationServerConfigurationWithJwtEncoder extends AuthorizationServerConfiguration {
0 commit comments