|
|
|
|
@ -45,6 +45,7 @@ import org.springframework.jdbc.core.JdbcTemplate;
@@ -45,6 +45,7 @@ import org.springframework.jdbc.core.JdbcTemplate;
|
|
|
|
|
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; |
|
|
|
|
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; |
|
|
|
|
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; |
|
|
|
|
import org.springframework.lang.Nullable; |
|
|
|
|
import org.springframework.mock.http.client.MockClientHttpResponse; |
|
|
|
|
import org.springframework.mock.web.MockHttpServletResponse; |
|
|
|
|
import org.springframework.mock.web.MockHttpSession; |
|
|
|
|
@ -59,6 +60,7 @@ import org.springframework.security.core.session.SessionRegistryImpl;
@@ -59,6 +60,7 @@ import org.springframework.security.core.session.SessionRegistryImpl;
|
|
|
|
|
import org.springframework.security.crypto.password.NoOpPasswordEncoder; |
|
|
|
|
import org.springframework.security.crypto.password.PasswordEncoder; |
|
|
|
|
import org.springframework.security.oauth2.core.AuthorizationGrantType; |
|
|
|
|
import org.springframework.security.oauth2.core.OAuth2RefreshToken; |
|
|
|
|
import org.springframework.security.oauth2.core.OAuth2Token; |
|
|
|
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; |
|
|
|
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType; |
|
|
|
|
@ -441,6 +443,85 @@ public class OidcTests {
@@ -441,6 +443,85 @@ public class OidcTests {
|
|
|
|
|
verify(this.tokenGenerator, times(3)).generate(any()); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// gh-1422
|
|
|
|
|
@Test |
|
|
|
|
public void requestWhenAuthenticationRequestWithOfflineAccessScopeThenTokenResponseIncludesRefreshToken() throws Exception { |
|
|
|
|
this.spring.register(AuthorizationServerConfigurationWithCustomRefreshTokenGenerator.class).autowire(); |
|
|
|
|
|
|
|
|
|
RegisteredClient registeredClient = TestRegisteredClients.registeredClient() |
|
|
|
|
.scope(OidcScopes.OPENID) |
|
|
|
|
.scope("offline_access") |
|
|
|
|
.build(); |
|
|
|
|
this.registeredClientRepository.save(registeredClient); |
|
|
|
|
|
|
|
|
|
MultiValueMap<String, String> authorizationRequestParameters = getAuthorizationRequestParameters(registeredClient); |
|
|
|
|
MvcResult mvcResult = this.mvc.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI) |
|
|
|
|
.params(authorizationRequestParameters) |
|
|
|
|
.with(user("user"))) |
|
|
|
|
.andExpect(status().is3xxRedirection()) |
|
|
|
|
.andReturn(); |
|
|
|
|
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl(); |
|
|
|
|
String expectedRedirectUri = authorizationRequestParameters.getFirst(OAuth2ParameterNames.REDIRECT_URI); |
|
|
|
|
assertThat(redirectedUrl).matches(expectedRedirectUri + "\\?code=.{15,}&state=state"); |
|
|
|
|
|
|
|
|
|
String authorizationCode = extractParameterFromRedirectUri(redirectedUrl, "code"); |
|
|
|
|
OAuth2Authorization authorization = this.authorizationService.findByToken(authorizationCode, AUTHORIZATION_CODE_TOKEN_TYPE); |
|
|
|
|
|
|
|
|
|
this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI) |
|
|
|
|
.params(getTokenRequestParameters(registeredClient, authorization)) |
|
|
|
|
.header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth( |
|
|
|
|
registeredClient.getClientId(), registeredClient.getClientSecret()))) |
|
|
|
|
.andExpect(status().isOk()) |
|
|
|
|
.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store"))) |
|
|
|
|
.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache"))) |
|
|
|
|
.andExpect(jsonPath("$.access_token").isNotEmpty()) |
|
|
|
|
.andExpect(jsonPath("$.token_type").isNotEmpty()) |
|
|
|
|
.andExpect(jsonPath("$.expires_in").isNotEmpty()) |
|
|
|
|
.andExpect(jsonPath("$.refresh_token").isNotEmpty()) |
|
|
|
|
.andExpect(jsonPath("$.scope").isNotEmpty()) |
|
|
|
|
.andExpect(jsonPath("$.id_token").isNotEmpty()) |
|
|
|
|
.andReturn(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// gh-1422
|
|
|
|
|
@Test |
|
|
|
|
public void requestWhenAuthenticationRequestWithoutOfflineAccessScopeThenTokenResponseDoesNotIncludeRefreshToken() throws Exception { |
|
|
|
|
this.spring.register(AuthorizationServerConfigurationWithCustomRefreshTokenGenerator.class).autowire(); |
|
|
|
|
|
|
|
|
|
RegisteredClient registeredClient = TestRegisteredClients.registeredClient() |
|
|
|
|
.scope(OidcScopes.OPENID) |
|
|
|
|
.build(); |
|
|
|
|
this.registeredClientRepository.save(registeredClient); |
|
|
|
|
|
|
|
|
|
MultiValueMap<String, String> authorizationRequestParameters = getAuthorizationRequestParameters(registeredClient); |
|
|
|
|
MvcResult mvcResult = this.mvc.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI) |
|
|
|
|
.params(authorizationRequestParameters) |
|
|
|
|
.with(user("user"))) |
|
|
|
|
.andExpect(status().is3xxRedirection()) |
|
|
|
|
.andReturn(); |
|
|
|
|
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl(); |
|
|
|
|
String expectedRedirectUri = authorizationRequestParameters.getFirst(OAuth2ParameterNames.REDIRECT_URI); |
|
|
|
|
assertThat(redirectedUrl).matches(expectedRedirectUri + "\\?code=.{15,}&state=state"); |
|
|
|
|
|
|
|
|
|
String authorizationCode = extractParameterFromRedirectUri(redirectedUrl, "code"); |
|
|
|
|
OAuth2Authorization authorization = this.authorizationService.findByToken(authorizationCode, AUTHORIZATION_CODE_TOKEN_TYPE); |
|
|
|
|
|
|
|
|
|
this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI) |
|
|
|
|
.params(getTokenRequestParameters(registeredClient, authorization)) |
|
|
|
|
.header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth( |
|
|
|
|
registeredClient.getClientId(), registeredClient.getClientSecret()))) |
|
|
|
|
.andExpect(status().isOk()) |
|
|
|
|
.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store"))) |
|
|
|
|
.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache"))) |
|
|
|
|
.andExpect(jsonPath("$.access_token").isNotEmpty()) |
|
|
|
|
.andExpect(jsonPath("$.token_type").isNotEmpty()) |
|
|
|
|
.andExpect(jsonPath("$.expires_in").isNotEmpty()) |
|
|
|
|
.andExpect(jsonPath("$.refresh_token").doesNotExist()) |
|
|
|
|
.andExpect(jsonPath("$.scope").isNotEmpty()) |
|
|
|
|
.andExpect(jsonPath("$.id_token").isNotEmpty()) |
|
|
|
|
.andReturn(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private static MultiValueMap<String, String> getAuthorizationRequestParameters(RegisteredClient registeredClient) { |
|
|
|
|
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(); |
|
|
|
|
parameters.set(OAuth2ParameterNames.RESPONSE_TYPE, OAuth2AuthorizationResponseType.CODE.getValue()); |
|
|
|
|
@ -612,4 +693,57 @@ public class OidcTests {
@@ -612,4 +693,57 @@ public class OidcTests {
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@EnableWebSecurity |
|
|
|
|
@Configuration |
|
|
|
|
static class AuthorizationServerConfigurationWithCustomRefreshTokenGenerator extends AuthorizationServerConfiguration { |
|
|
|
|
|
|
|
|
|
// @formatter:off
|
|
|
|
|
@Bean |
|
|
|
|
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { |
|
|
|
|
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = |
|
|
|
|
new OAuth2AuthorizationServerConfigurer(); |
|
|
|
|
http.apply(authorizationServerConfigurer); |
|
|
|
|
|
|
|
|
|
authorizationServerConfigurer |
|
|
|
|
.tokenGenerator(tokenGenerator()) |
|
|
|
|
.oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0
|
|
|
|
|
|
|
|
|
|
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher(); |
|
|
|
|
|
|
|
|
|
http |
|
|
|
|
.securityMatcher(endpointsMatcher) |
|
|
|
|
.authorizeHttpRequests(authorize -> |
|
|
|
|
authorize.anyRequest().authenticated() |
|
|
|
|
) |
|
|
|
|
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher)); |
|
|
|
|
|
|
|
|
|
return http.build(); |
|
|
|
|
} |
|
|
|
|
// @formatter:on
|
|
|
|
|
|
|
|
|
|
@Bean |
|
|
|
|
OAuth2TokenGenerator<?> tokenGenerator() { |
|
|
|
|
JwtGenerator jwtGenerator = new JwtGenerator(new NimbusJwtEncoder(jwkSource())); |
|
|
|
|
jwtGenerator.setJwtCustomizer(jwtCustomizer()); |
|
|
|
|
OAuth2TokenGenerator<OAuth2RefreshToken> refreshTokenGenerator = new CustomRefreshTokenGenerator(); |
|
|
|
|
return new DelegatingOAuth2TokenGenerator(jwtGenerator, refreshTokenGenerator); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private static final class CustomRefreshTokenGenerator implements OAuth2TokenGenerator<OAuth2RefreshToken> { |
|
|
|
|
private final OAuth2RefreshTokenGenerator delegate = new OAuth2RefreshTokenGenerator(); |
|
|
|
|
|
|
|
|
|
@Nullable |
|
|
|
|
@Override |
|
|
|
|
public OAuth2RefreshToken generate(OAuth2TokenContext context) { |
|
|
|
|
if (context.getAuthorizedScopes().contains(OidcScopes.OPENID) && |
|
|
|
|
!context.getAuthorizedScopes().contains("offline_access")) { |
|
|
|
|
return null; |
|
|
|
|
} |
|
|
|
|
return this.delegate.generate(context); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|