|
@@ -18,7 +18,9 @@ package org.springframework.security.config.annotation.web.configurers.oauth2.se
|
|
|
import java.net.URLEncoder;
|
|
|
import java.nio.charset.StandardCharsets;
|
|
|
import java.util.Base64;
|
|
|
-import java.util.function.BiConsumer;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Set;
|
|
|
+import java.util.stream.Collectors;
|
|
|
|
|
|
import com.nimbusds.jose.jwk.JWKSet;
|
|
|
import com.nimbusds.jose.jwk.source.JWKSource;
|
|
@@ -33,19 +35,29 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|
|
import org.springframework.context.annotation.Bean;
|
|
|
import org.springframework.context.annotation.Import;
|
|
|
import org.springframework.http.HttpHeaders;
|
|
|
+import org.springframework.http.HttpStatus;
|
|
|
+import org.springframework.http.converter.HttpMessageConverter;
|
|
|
+import org.springframework.mock.http.client.MockClientHttpResponse;
|
|
|
+import org.springframework.mock.web.MockHttpServletResponse;
|
|
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
|
|
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
|
|
import org.springframework.security.config.test.SpringTestRule;
|
|
|
+import org.springframework.security.core.Authentication;
|
|
|
+import org.springframework.security.core.GrantedAuthority;
|
|
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
|
|
+import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
|
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
|
|
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
|
|
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
|
|
+import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
|
|
|
import org.springframework.security.oauth2.jose.TestJwks;
|
|
|
-import org.springframework.security.oauth2.jwt.JoseHeader;
|
|
|
-import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
|
|
+import org.springframework.security.oauth2.jose.TestKeys;
|
|
|
+import org.springframework.security.oauth2.jwt.Jwt;
|
|
|
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
|
|
import org.springframework.security.oauth2.jwt.NimbusJwsEncoder;
|
|
|
+import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
|
|
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
|
|
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationAttributeNames;
|
|
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
|
|
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
|
|
|
import org.springframework.security.oauth2.server.authorization.TokenType;
|
|
@@ -53,7 +65,9 @@ import org.springframework.security.oauth2.server.authorization.client.Registere
|
|
|
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
|
|
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
|
|
|
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
|
|
|
+import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
|
|
|
import org.springframework.security.oauth2.server.authorization.token.OAuth2AuthorizationCode;
|
|
|
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
|
|
|
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
|
|
|
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
|
|
|
import org.springframework.test.web.servlet.MockMvc;
|
|
@@ -90,13 +104,16 @@ public class OAuth2AuthorizationCodeGrantTests {
|
|
|
// https://tools.ietf.org/html/rfc7636#appendix-B
|
|
|
private static final String S256_CODE_VERIFIER = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
|
|
|
private static final String S256_CODE_CHALLENGE = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM";
|
|
|
+ private static final String AUTHORITIES_CLAIM = "authorities";
|
|
|
|
|
|
private static RegisteredClientRepository registeredClientRepository;
|
|
|
private static OAuth2AuthorizationService authorizationService;
|
|
|
private static JWKSource<SecurityContext> jwkSource;
|
|
|
private static NimbusJwsEncoder jwtEncoder;
|
|
|
- private static BiConsumer<JoseHeader.Builder, JwtClaimsSet.Builder> jwtCustomizer;
|
|
|
+ private static NimbusJwtDecoder jwtDecoder;
|
|
|
private static ProviderSettings providerSettings;
|
|
|
+ private static HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter =
|
|
|
+ new OAuth2AccessTokenResponseHttpMessageConverter();
|
|
|
|
|
|
@Rule
|
|
|
public final SpringTestRule spring = new SpringTestRule();
|
|
@@ -111,8 +128,7 @@ public class OAuth2AuthorizationCodeGrantTests {
|
|
|
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
|
|
|
jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
|
|
|
jwtEncoder = new NimbusJwsEncoder(jwkSource);
|
|
|
- jwtCustomizer = mock(BiConsumer.class);
|
|
|
- jwtEncoder.setJwtCustomizer(jwtCustomizer);
|
|
|
+ jwtDecoder = NimbusJwtDecoder.withPublicKey(TestKeys.DEFAULT_PUBLIC_KEY).build();
|
|
|
providerSettings = new ProviderSettings()
|
|
|
.authorizationEndpoint("/test/authorize")
|
|
|
.tokenEndpoint("/test/token");
|
|
@@ -186,8 +202,17 @@ public class OAuth2AuthorizationCodeGrantTests {
|
|
|
eq(TokenType.AUTHORIZATION_CODE)))
|
|
|
.thenReturn(authorization);
|
|
|
|
|
|
- assertTokenRequestReturnsAccessTokenResponse(
|
|
|
+ OAuth2AccessTokenResponse accessTokenResponse = assertTokenRequestReturnsAccessTokenResponse(
|
|
|
registeredClient, authorization, OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI);
|
|
|
+
|
|
|
+ // Assert user authorities was propagated as claim in JWT
|
|
|
+ Jwt jwt = jwtDecoder.decode(accessTokenResponse.getAccessToken().getTokenValue());
|
|
|
+ List<String> authoritiesClaim = jwt.getClaim(AUTHORITIES_CLAIM);
|
|
|
+ Authentication principal = authorization.getAttribute(OAuth2AuthorizationAttributeNames.PRINCIPAL);
|
|
|
+ Set<String> userAuthorities = principal.getAuthorities().stream()
|
|
|
+ .map(GrantedAuthority::getAuthority)
|
|
|
+ .collect(Collectors.toSet());
|
|
|
+ assertThat(authoritiesClaim).containsExactlyInAnyOrderElementsOf(userAuthorities);
|
|
|
}
|
|
|
|
|
|
@Test
|
|
@@ -208,10 +233,10 @@ public class OAuth2AuthorizationCodeGrantTests {
|
|
|
registeredClient, authorization, providerSettings.tokenEndpoint());
|
|
|
}
|
|
|
|
|
|
- private void assertTokenRequestReturnsAccessTokenResponse(RegisteredClient registeredClient,
|
|
|
+ private OAuth2AccessTokenResponse assertTokenRequestReturnsAccessTokenResponse(RegisteredClient registeredClient,
|
|
|
OAuth2Authorization authorization, String tokenEndpointUri) throws Exception {
|
|
|
|
|
|
- this.mvc.perform(post(tokenEndpointUri)
|
|
|
+ MvcResult mvcResult = this.mvc.perform(post(tokenEndpointUri)
|
|
|
.params(getTokenRequestParameters(registeredClient, authorization))
|
|
|
.header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth(
|
|
|
registeredClient.getClientId(), registeredClient.getClientSecret())))
|
|
@@ -222,13 +247,19 @@ public class OAuth2AuthorizationCodeGrantTests {
|
|
|
.andExpect(jsonPath("$.token_type").isNotEmpty())
|
|
|
.andExpect(jsonPath("$.expires_in").isNotEmpty())
|
|
|
.andExpect(jsonPath("$.refresh_token").isNotEmpty())
|
|
|
- .andExpect(jsonPath("$.scope").isNotEmpty());
|
|
|
+ .andExpect(jsonPath("$.scope").isNotEmpty())
|
|
|
+ .andReturn();
|
|
|
|
|
|
verify(registeredClientRepository).findByClientId(eq(registeredClient.getClientId()));
|
|
|
verify(authorizationService).findByToken(
|
|
|
eq(authorization.getTokens().getToken(OAuth2AuthorizationCode.class).getTokenValue()),
|
|
|
eq(TokenType.AUTHORIZATION_CODE));
|
|
|
verify(authorizationService).save(any());
|
|
|
+
|
|
|
+ MockHttpServletResponse servletResponse = mvcResult.getResponse();
|
|
|
+ MockClientHttpResponse httpResponse = new MockClientHttpResponse(
|
|
|
+ servletResponse.getContentAsByteArray(), HttpStatus.valueOf(servletResponse.getStatus()));
|
|
|
+ return accessTokenHttpResponseConverter.read(OAuth2AccessTokenResponse.class, httpResponse);
|
|
|
}
|
|
|
|
|
|
@Test
|
|
@@ -295,8 +326,6 @@ public class OAuth2AuthorizationCodeGrantTests {
|
|
|
.params(getTokenRequestParameters(registeredClient, authorization))
|
|
|
.header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth(
|
|
|
registeredClient.getClientId(), registeredClient.getClientSecret())));
|
|
|
-
|
|
|
- verify(jwtCustomizer).accept(any(JoseHeader.Builder.class), any(JwtClaimsSet.Builder.class));
|
|
|
}
|
|
|
|
|
|
private static MultiValueMap<String, String> getAuthorizationRequestParameters(RegisteredClient registeredClient) {
|
|
@@ -345,6 +374,20 @@ public class OAuth2AuthorizationCodeGrantTests {
|
|
|
JWKSource<SecurityContext> jwkSource() {
|
|
|
return jwkSource;
|
|
|
}
|
|
|
+
|
|
|
+ @Bean
|
|
|
+ OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
|
|
|
+ return context -> {
|
|
|
+ if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(context.getAuthorizationGrantType()) &&
|
|
|
+ TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
|
|
|
+ Authentication principal = context.getPrincipal();
|
|
|
+ Set<String> authorities = principal.getAuthorities().stream()
|
|
|
+ .map(GrantedAuthority::getAuthority)
|
|
|
+ .collect(Collectors.toSet());
|
|
|
+ context.getClaims().claim(AUTHORITIES_CLAIM, authorities);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
@EnableWebSecurity
|