Add OpenID Connect 1.0 Authorization Code Flow

Closes gh-53
This commit is contained in:
Joe Grandja
2020-12-04 10:02:07 -05:00
parent 8c71e56350
commit f2bb523105
20 changed files with 484 additions and 37 deletions

View File

@@ -15,25 +15,59 @@
*/
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
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.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.crypto.key.CryptoKeySource;
import org.springframework.security.crypto.key.StaticKeyGeneratingCryptoKeySource;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.TokenType;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
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.OAuth2AuthorizationCode;
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
import org.springframework.security.oauth2.server.authorization.web.OidcProviderConfigurationEndpointFilter;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.hamcrest.CoreMatchers.containsString;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -44,6 +78,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
*/
public class OidcTests {
private static final String issuerUrl = "https://example.com/issuer1";
private static RegisteredClientRepository registeredClientRepository;
private static OAuth2AuthorizationService authorizationService;
private static CryptoKeySource keySource;
@Rule
public final SpringTestRule spring = new SpringTestRule();
@@ -51,14 +88,26 @@ public class OidcTests {
@Autowired
private MockMvc mvc;
@BeforeClass
public static void init() {
registeredClientRepository = mock(RegisteredClientRepository.class);
authorizationService = mock(OAuth2AuthorizationService.class);
keySource = new StaticKeyGeneratingCryptoKeySource();
}
@Before
public void setup() {
reset(registeredClientRepository);
reset(authorizationService);
}
@Test
public void requestWhenConfigurationRequestAndIssuerSetThenReturnConfigurationResponse() throws Exception {
this.spring.register(AuthorizationServerConfigurationWithIssuer.class).autowire();
this.mvc.perform(get(OidcProviderConfigurationEndpointFilter.DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI))
.andExpect(status().is2xxSuccessful())
.andExpect(jsonPath("issuer").value(issuerUrl))
.andReturn();
.andExpect(jsonPath("issuer").value(issuerUrl));
}
@Test
@@ -85,18 +134,98 @@ public class OidcTests {
);
}
@Test
public void requestWhenAuthenticationRequestThenTokenResponseIncludesIdToken() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build();
when(registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
.thenReturn(registeredClient);
MvcResult mvcResult = this.mvc.perform(get(OAuth2AuthorizationEndpointFilter.DEFAULT_AUTHORIZATION_ENDPOINT_URI)
.params(getAuthorizationRequestParameters(registeredClient))
.with(user("user")))
.andExpect(status().is3xxRedirection())
.andReturn();
assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://example.com\\?code=.{15,}&state=state");
verify(registeredClientRepository).findByClientId(eq(registeredClient.getClientId()));
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
verify(authorizationService).save(authorizationCaptor.capture());
OAuth2Authorization authorization = authorizationCaptor.getValue();
when(authorizationService.findByToken(
eq(authorization.getTokens().getToken(OAuth2AuthorizationCode.class).getTokenValue()),
eq(TokenType.AUTHORIZATION_CODE)))
.thenReturn(authorization);
this.mvc.perform(post(OAuth2TokenEndpointFilter.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());
verify(registeredClientRepository, times(2)).findByClientId(eq(registeredClient.getClientId()));
verify(authorizationService).findByToken(
eq(authorization.getTokens().getToken(OAuth2AuthorizationCode.class).getTokenValue()),
eq(TokenType.AUTHORIZATION_CODE));
verify(authorizationService, times(2)).save(any());
}
private static MultiValueMap<String, String> getAuthorizationRequestParameters(RegisteredClient registeredClient) {
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set(OAuth2ParameterNames.RESPONSE_TYPE, OAuth2AuthorizationResponseType.CODE.getValue());
parameters.set(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
parameters.set(OAuth2ParameterNames.REDIRECT_URI, registeredClient.getRedirectUris().iterator().next());
parameters.set(OAuth2ParameterNames.SCOPE,
StringUtils.collectionToDelimitedString(registeredClient.getScopes(), " "));
parameters.set(OAuth2ParameterNames.STATE, "state");
return parameters;
}
private static MultiValueMap<String, String> getTokenRequestParameters(RegisteredClient registeredClient,
OAuth2Authorization authorization) {
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
parameters.set(OAuth2ParameterNames.CODE, authorization.getTokens().getToken(OAuth2AuthorizationCode.class).getTokenValue());
parameters.set(OAuth2ParameterNames.REDIRECT_URI, registeredClient.getRedirectUris().iterator().next());
return parameters;
}
private static String encodeBasicAuth(String clientId, String secret) throws Exception {
clientId = URLEncoder.encode(clientId, StandardCharsets.UTF_8.name());
secret = URLEncoder.encode(secret, StandardCharsets.UTF_8.name());
String credentialsString = clientId + ":" + secret;
byte[] encodedBytes = Base64.getEncoder().encode(credentialsString.getBytes(StandardCharsets.UTF_8));
return new String(encodedBytes, StandardCharsets.UTF_8);
}
@EnableWebSecurity
@Import(OAuth2AuthorizationServerConfiguration.class)
static class AuthorizationServerConfiguration {
@Bean
RegisteredClientRepository registeredClientRepository() {
return mock(RegisteredClientRepository.class);
return registeredClientRepository;
}
@Bean
OAuth2AuthorizationService authorizationService() {
return authorizationService;
}
@Bean
CryptoKeySource keySource() {
return mock(CryptoKeySource.class);
return keySource;
}
}

View File

@@ -41,7 +41,8 @@ public class OidcProviderConfigurationTests {
.jwkSetUri("https://example.com/issuer1/oauth2/jwks")
.scope("openid")
.responseType("code")
.subjectType("public");
.subjectType("public")
.idTokenSigningAlgorithm("RS256");
@Test
public void buildWhenAllRequiredClaimsAndAdditionalClaimsThenCreated() {
@@ -55,6 +56,7 @@ public class OidcProviderConfigurationTests {
.grantType("authorization_code")
.grantType("client_credentials")
.subjectType("public")
.idTokenSigningAlgorithm("RS256")
.tokenEndpointAuthenticationMethod("client_secret_basic")
.claim("a-claim", "a-value")
.build();
@@ -67,6 +69,7 @@ public class OidcProviderConfigurationTests {
assertThat(providerConfiguration.getResponseTypes()).containsExactly("code");
assertThat(providerConfiguration.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "client_credentials");
assertThat(providerConfiguration.getSubjectTypes()).containsExactly("public");
assertThat(providerConfiguration.getIdTokenSigningAlgorithms()).containsExactly("RS256");
assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).containsExactly("client_secret_basic");
assertThat(providerConfiguration.<String>getClaim("a-claim")).isEqualTo("a-value");
}
@@ -81,6 +84,7 @@ public class OidcProviderConfigurationTests {
.scope("openid")
.responseType("code")
.subjectType("public")
.idTokenSigningAlgorithm("RS256")
.build();
assertThat(providerConfiguration.getIssuer()).isEqualTo(url("https://example.com/issuer1"));
@@ -91,6 +95,7 @@ public class OidcProviderConfigurationTests {
assertThat(providerConfiguration.getResponseTypes()).containsExactly("code");
assertThat(providerConfiguration.getGrantTypes()).isNull();
assertThat(providerConfiguration.getSubjectTypes()).containsExactly("public");
assertThat(providerConfiguration.getIdTokenSigningAlgorithms()).containsExactly("RS256");
assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).isNull();
}
@@ -104,6 +109,7 @@ public class OidcProviderConfigurationTests {
claims.put(OidcProviderMetadataClaimNames.SCOPES_SUPPORTED, Collections.singletonList("openid"));
claims.put(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, Collections.singletonList("code"));
claims.put(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED, Collections.singletonList("public"));
claims.put(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED, Collections.singletonList("RS256"));
claims.put("some-claim", "some-value");
OidcProviderConfiguration providerConfiguration = OidcProviderConfiguration.withClaims(claims).build();
@@ -116,6 +122,7 @@ public class OidcProviderConfigurationTests {
assertThat(providerConfiguration.getResponseTypes()).containsExactly("code");
assertThat(providerConfiguration.getGrantTypes()).isNull();
assertThat(providerConfiguration.getSubjectTypes()).containsExactly("public");
assertThat(providerConfiguration.getIdTokenSigningAlgorithms()).containsExactly("RS256");
assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).isNull();
assertThat(providerConfiguration.<String>getClaim("some-claim")).isEqualTo("some-value");
}
@@ -130,6 +137,7 @@ public class OidcProviderConfigurationTests {
claims.put(OidcProviderMetadataClaimNames.SCOPES_SUPPORTED, Collections.singletonList("openid"));
claims.put(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, Collections.singletonList("code"));
claims.put(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED, Collections.singletonList("public"));
claims.put(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED, Collections.singletonList("RS256"));
claims.put("some-claim", "some-value");
OidcProviderConfiguration providerConfiguration = OidcProviderConfiguration.withClaims(claims).build();
@@ -142,6 +150,7 @@ public class OidcProviderConfigurationTests {
assertThat(providerConfiguration.getResponseTypes()).containsExactly("code");
assertThat(providerConfiguration.getGrantTypes()).isNull();
assertThat(providerConfiguration.getSubjectTypes()).containsExactly("public");
assertThat(providerConfiguration.getIdTokenSigningAlgorithms()).containsExactly("RS256");
assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).isNull();
assertThat(providerConfiguration.<String>getClaim("some-claim")).isEqualTo("some-value");
}
@@ -332,6 +341,42 @@ public class OidcProviderConfigurationTests {
.hasMessageContaining("subjectTypes cannot be empty");
}
@Test
public void buildWhenMissingIdTokenSigningAlgorithmsThenThrowIllegalArgumentException() {
OidcProviderConfiguration.Builder builder = this.minimalConfigurationBuilder
.claims((claims) -> claims.remove(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED));
assertThatThrownBy(builder::build)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("idTokenSigningAlgorithms cannot be null");
}
@Test
public void buildWhenIdTokenSigningAlgorithmsNotListThenThrowIllegalArgumentException() {
OidcProviderConfiguration.Builder builder = this.minimalConfigurationBuilder
.claims((claims) -> {
claims.remove(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED);
claims.put(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED, "RS256");
});
assertThatThrownBy(builder::build)
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("idTokenSigningAlgorithms must be of type List");
}
@Test
public void buildWhenIdTokenSigningAlgorithmsEmptyListThenThrowIllegalArgumentException() {
OidcProviderConfiguration.Builder builder = this.minimalConfigurationBuilder
.claims((claims) -> {
claims.remove(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED);
claims.put(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED, Collections.emptyList());
});
assertThatThrownBy(builder::build)
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("idTokenSigningAlgorithms cannot be empty");
}
@Test
public void responseTypesWhenAddingOrRemovingThenCorrectValues() {
OidcProviderConfiguration configuration = this.minimalConfigurationBuilder
@@ -368,6 +413,19 @@ public class OidcProviderConfigurationTests {
assertThat(configuration.getSubjectTypes()).containsExactly("some-subject-type");
}
@Test
public void idTokenSigningAlgorithmsWhenAddingOrRemovingThenCorrectValues() {
OidcProviderConfiguration configuration = this.minimalConfigurationBuilder
.idTokenSigningAlgorithm("should-be-removed")
.idTokenSigningAlgorithms(signingAlgorithms -> {
signingAlgorithms.clear();
signingAlgorithms.add("ES256");
})
.build();
assertThat(configuration.getIdTokenSigningAlgorithms()).containsExactly("ES256");
}
@Test
public void scopesWhenAddingOrRemovingThenCorrectValues() {
OidcProviderConfiguration configuration = this.minimalConfigurationBuilder

View File

@@ -65,7 +65,8 @@ public class OidcProviderConfigurationHttpMessageConverterTests {
+ " \"token_endpoint\": \"https://example.com/issuer1/oauth2/token\",\n"
+ " \"jwks_uri\": \"https://example.com/issuer1/oauth2/jwks\",\n"
+ " \"response_types_supported\": [\"code\"],\n"
+ " \"subject_types_supported\": [\"public\"]\n"
+ " \"subject_types_supported\": [\"public\"],\n"
+ " \"id_token_signing_alg_values_supported\": [\"RS256\"]\n"
+ "}\n";
// @formatter:on
MockClientHttpResponse response = new MockClientHttpResponse(providerConfigurationResponse.getBytes(), HttpStatus.OK);
@@ -78,6 +79,7 @@ public class OidcProviderConfigurationHttpMessageConverterTests {
assertThat(providerConfiguration.getJwkSetUri()).isEqualTo(new URL("https://example.com/issuer1/oauth2/jwks"));
assertThat(providerConfiguration.getResponseTypes()).containsExactly("code");
assertThat(providerConfiguration.getSubjectTypes()).containsExactly("public");
assertThat(providerConfiguration.getIdTokenSigningAlgorithms()).containsExactly("RS256");
assertThat(providerConfiguration.getScopes()).isNull();
assertThat(providerConfiguration.getGrantTypes()).isNull();
assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).isNull();
@@ -95,6 +97,7 @@ public class OidcProviderConfigurationHttpMessageConverterTests {
+ " \"response_types_supported\": [\"code\"],\n"
+ " \"grant_types_supported\": [\"authorization_code\", \"client_credentials\"],\n"
+ " \"subject_types_supported\": [\"public\"],\n"
+ " \"id_token_signing_alg_values_supported\": [\"RS256\"],\n"
+ " \"token_endpoint_auth_methods_supported\": [\"client_secret_basic\"],\n"
+ " \"custom_claim\": \"value\",\n"
+ " \"custom_collection_claim\": [\"value1\", \"value2\"]\n"
@@ -112,6 +115,7 @@ public class OidcProviderConfigurationHttpMessageConverterTests {
assertThat(providerConfiguration.getResponseTypes()).containsExactly("code");
assertThat(providerConfiguration.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "client_credentials");
assertThat(providerConfiguration.getSubjectTypes()).containsExactly("public");
assertThat(providerConfiguration.getIdTokenSigningAlgorithms()).containsExactly("RS256");
assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).containsExactly("client_secret_basic");
assertThat(providerConfiguration.<String>getClaim("custom_claim")).isEqualTo("value");
assertThat(providerConfiguration.getClaimAsStringList("custom_collection_claim")).containsExactlyInAnyOrder("value1", "value2");
@@ -155,6 +159,7 @@ public class OidcProviderConfigurationHttpMessageConverterTests {
.grantType("authorization_code")
.grantType("client_credentials")
.subjectType("public")
.idTokenSigningAlgorithm("RS256")
.tokenEndpointAuthenticationMethod("client_secret_basic")
.claim("custom_claim", "value")
.claim("custom_collection_claim", Arrays.asList("value1", "value2"))
@@ -172,6 +177,7 @@ public class OidcProviderConfigurationHttpMessageConverterTests {
assertThat(providerConfigurationResponse).contains("\"response_types_supported\":[\"code\"]");
assertThat(providerConfigurationResponse).contains("\"grant_types_supported\":[\"authorization_code\",\"client_credentials\"]");
assertThat(providerConfigurationResponse).contains("\"subject_types_supported\":[\"public\"]");
assertThat(providerConfigurationResponse).contains("\"id_token_signing_alg_values_supported\":[\"RS256\"]");
assertThat(providerConfigurationResponse).contains("\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\"]");
assertThat(providerConfigurationResponse).contains("\"custom_claim\":\"value\"");
assertThat(providerConfigurationResponse).contains("\"custom_collection_claim\":[\"value1\",\"value2\"]");
@@ -194,6 +200,7 @@ public class OidcProviderConfigurationHttpMessageConverterTests {
.jwkSetUri("https://example.com/issuer1/oauth2/jwks")
.responseType("code")
.subjectType("public")
.idTokenSigningAlgorithm("RS256")
.build();
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();

View File

@@ -17,10 +17,15 @@ package org.springframework.security.oauth2.server.authorization.authentication;
import org.junit.Test;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken2;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -36,6 +41,9 @@ public class OAuth2AccessTokenAuthenticationTokenTests {
new OAuth2ClientAuthenticationToken(this.registeredClient);
private OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
"access-token", Instant.now(), Instant.now().plusSeconds(300));
private OAuth2RefreshToken refreshToken = new OAuth2RefreshToken2(
"refresh-token", Instant.now(), Instant.now().plus(1, ChronoUnit.DAYS));
private Map<String, Object> additionalParameters = Collections.singletonMap("custom-param", "custom-value");
@Test
public void constructorWhenRegisteredClientNullThenThrowIllegalArgumentException() {
@@ -58,13 +66,23 @@ public class OAuth2AccessTokenAuthenticationTokenTests {
.hasMessage("accessToken cannot be null");
}
@Test
public void constructorWhenAdditionalParametersNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new OAuth2AccessTokenAuthenticationToken(
this.registeredClient, this.clientPrincipal, this.accessToken, this.refreshToken, null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("additionalParameters cannot be null");
}
@Test
public void constructorWhenAllValuesProvidedThenCreated() {
OAuth2AccessTokenAuthenticationToken authentication = new OAuth2AccessTokenAuthenticationToken(
this.registeredClient, this.clientPrincipal, this.accessToken);
this.registeredClient, this.clientPrincipal, this.accessToken, this.refreshToken, this.additionalParameters);
assertThat(authentication.getPrincipal()).isEqualTo(this.clientPrincipal);
assertThat(authentication.getCredentials().toString()).isEmpty();
assertThat(authentication.getRegisteredClient()).isEqualTo(this.registeredClient);
assertThat(authentication.getAccessToken()).isEqualTo(this.accessToken);
assertThat(authentication.getRefreshToken()).isEqualTo(this.refreshToken);
assertThat(authentication.getAdditionalParameters()).isEqualTo(this.additionalParameters);
}
}

View File

@@ -25,6 +25,9 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.oauth2.jose.JoseHeaderNames;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
@@ -49,9 +52,11 @@ import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.entry;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -251,6 +256,43 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
assertThat(updatedAuthorization.getTokens().getTokenMetadata(authorizationCode).isInvalidated()).isTrue();
}
@Test
public void authenticateWhenValidCodeAndAuthenticationRequestThenReturnIdToken() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build();
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
.thenReturn(authorization);
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient);
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST);
OAuth2AuthorizationCodeAuthenticationToken authentication =
new OAuth2AuthorizationCodeAuthenticationToken(AUTHORIZATION_CODE, clientPrincipal, authorizationRequest.getRedirectUri(), null);
when(this.jwtEncoder.encode(any(), any())).thenReturn(createJwt());
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
(OAuth2AccessTokenAuthenticationToken) this.authenticationProvider.authenticate(authentication);
verify(this.jwtEncoder, times(2)).encode(any(), any()); // Access token and ID Token
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
verify(this.authorizationService).save(authorizationCaptor.capture());
OAuth2Authorization updatedAuthorization = authorizationCaptor.getValue();
assertThat(accessTokenAuthentication.getRegisteredClient().getId()).isEqualTo(updatedAuthorization.getRegisteredClientId());
assertThat(accessTokenAuthentication.getPrincipal()).isEqualTo(clientPrincipal);
assertThat(accessTokenAuthentication.getAccessToken()).isEqualTo(updatedAuthorization.getTokens().getAccessToken());
assertThat(accessTokenAuthentication.getRefreshToken()).isNotNull();
assertThat(accessTokenAuthentication.getRefreshToken()).isEqualTo(updatedAuthorization.getTokens().getRefreshToken());
OAuth2AuthorizationCode authorizationCode = updatedAuthorization.getTokens().getToken(OAuth2AuthorizationCode.class);
assertThat(updatedAuthorization.getTokens().getTokenMetadata(authorizationCode).isInvalidated()).isTrue();
OidcIdToken idToken = updatedAuthorization.getTokens().getToken(OidcIdToken.class);
assertThat(idToken).isNotNull();
assertThat(accessTokenAuthentication.getAdditionalParameters())
.containsExactly(entry(OidcParameterNames.ID_TOKEN, idToken.getTokenValue()));
}
@Test
public void authenticateWhenTokenTimeToLiveConfiguredThenTokenExpirySet() {
Duration accessTokenTTL = Duration.ofHours(2);

View File

@@ -148,7 +148,7 @@ public class OAuth2ClientCredentialsAuthenticationProviderTests {
public void authenticateWhenScopeRequestedThenAccessTokenContainsScope() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient2().build();
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient);
Set<String> requestedScope = Collections.singleton("openid");
Set<String> requestedScope = Collections.singleton("scope1");
OAuth2ClientCredentialsAuthenticationToken authentication =
new OAuth2ClientCredentialsAuthenticationToken(clientPrincipal, requestedScope);

View File

@@ -31,9 +31,7 @@ public class TestRegisteredClients {
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
.redirectUri("https://example.com")
.scope("openid")
.scope("profile")
.scope("email");
.scope("scope1");
}
public static RegisteredClient.Builder registeredClient2() {
@@ -46,9 +44,6 @@ public class TestRegisteredClients {
.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
.clientAuthenticationMethod(ClientAuthenticationMethod.POST)
.redirectUri("https://example.com")
.scope("openid")
.scope("profile")
.scope("email")
.scope("scope1")
.scope("scope2");
}
@@ -59,9 +54,7 @@ public class TestRegisteredClients {
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
.redirectUri("https://example.com")
.scope("openid")
.scope("profile")
.scope("email")
.scope("scope1")
.clientSettings(clientSettings -> clientSettings.requireProofKey(true));
}
}

View File

@@ -32,6 +32,7 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequ
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.oidc.OidcScopes;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationAttributeNames;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
@@ -168,6 +169,20 @@ public class OAuth2AuthorizationEndpointFilterTests {
OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
}
@Test
public void doFilterWhenAuthenticationRequestMissingRedirectUriThenInvalidRequestError() throws Exception {
// redirect_uri is REQUIRED for OpenID Connect requests
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build();
when(this.registeredClientRepository.findByClientId((eq(registeredClient.getClientId()))))
.thenReturn(registeredClient);
doFilterWhenAuthorizationRequestInvalidParameterThenError(
registeredClient,
OAuth2ParameterNames.REDIRECT_URI,
OAuth2ErrorCodes.INVALID_REQUEST,
request -> request.removeParameter(OAuth2ParameterNames.REDIRECT_URI));
}
@Test
public void doFilterWhenAuthorizationRequestInvalidRedirectUriThenInvalidRequestError() throws Exception {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
@@ -394,7 +409,7 @@ public class OAuth2AuthorizationEndpointFilterTests {
}
@Test
public void doFilterWhenAuthorizationRequestValidNotAuthenticatedThenContinueChainToCommenceAuthentication() throws Exception {
public void doFilterWhenAuthorizationRequestNotAuthenticatedThenContinueChainToCommenceAuthentication() throws Exception {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
when(this.registeredClientRepository.findByClientId((eq(registeredClient.getClientId()))))
.thenReturn(registeredClient);
@@ -411,12 +426,27 @@ public class OAuth2AuthorizationEndpointFilterTests {
}
@Test
public void doFilterWhenAuthorizationRequestValidThenAuthorizationResponse() throws Exception {
public void doFilterWhenAuthorizationRequestGetThenAuthorizationResponse() throws Exception {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
MockHttpServletRequest request = createAuthorizationRequest(registeredClient);
doFilterWhenAuthorizationRequestThenAuthorizationResponse(registeredClient, request);
}
@Test
public void doFilterWhenAuthorizationRequestPostThenAuthorizationResponse() throws Exception {
// OpenID Connect requests support POST method
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build();
MockHttpServletRequest request = createAuthorizationRequest(registeredClient);
request.setMethod("POST");
doFilterWhenAuthorizationRequestThenAuthorizationResponse(registeredClient, request);
}
private void doFilterWhenAuthorizationRequestThenAuthorizationResponse(
RegisteredClient registeredClient, MockHttpServletRequest request) throws Exception {
when(this.registeredClientRepository.findByClientId((eq(registeredClient.getClientId()))))
.thenReturn(registeredClient);
MockHttpServletRequest request = createAuthorizationRequest(registeredClient);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
@@ -455,7 +485,7 @@ public class OAuth2AuthorizationEndpointFilterTests {
}
@Test
public void doFilterWhenPkceRequiredAndAuthorizationRequestValidThenAuthorizationResponse() throws Exception {
public void doFilterWhenPkceRequiredAndAuthorizationRequestThenAuthorizationResponse() throws Exception {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.clientSettings(clientSettings -> clientSettings.requireProofKey(true))
.build();
@@ -501,7 +531,7 @@ public class OAuth2AuthorizationEndpointFilterTests {
}
@Test
public void doFilterWhenUserConsentRequiredAndAuthorizationRequestValidThenUserConsentResponse() throws Exception {
public void doFilterWhenUserConsentRequiredAndAuthorizationRequestThenUserConsentResponse() throws Exception {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.clientSettings(clientSettings -> clientSettings.requireUserConsent(true))
.build();
@@ -722,7 +752,7 @@ public class OAuth2AuthorizationEndpointFilterTests {
OAuth2ParameterNames.CLIENT_ID,
OAuth2ErrorCodes.ACCESS_DENIED,
DEFAULT_ERROR_URI,
request -> request.removeParameter("consent_action"));
request -> request.setParameter("consent_action", "cancel"));
verify(this.authorizationService).remove(eq(authorization));
}

View File

@@ -33,6 +33,7 @@ import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken2;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
@@ -53,10 +54,13 @@ import javax.servlet.http.HttpServletResponse;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.entry;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@@ -199,16 +203,19 @@ public class OAuth2TokenEndpointFilterTests {
}
@Test
public void doFilterWhenAuthorizationCodeTokenRequestValidThenAccessTokenResponse() throws Exception {
public void doFilterWhenAuthorizationCodeTokenRequestThenAccessTokenResponse() throws Exception {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
Authentication clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient);
OAuth2AccessToken accessToken = new OAuth2AccessToken(
OAuth2AccessToken.TokenType.BEARER, "token",
Instant.now(), Instant.now().plus(Duration.ofHours(1)),
new HashSet<>(Arrays.asList("scope1", "scope2")));
OAuth2RefreshToken refreshToken = new OAuth2RefreshToken2(
"refresh-token", Instant.now(), Instant.now().plus(Duration.ofDays(1)));
Map<String, Object> additionalParameters = Collections.singletonMap("custom-param", "custom-value");
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
new OAuth2AccessTokenAuthenticationToken(
registeredClient, clientPrincipal, accessToken);
registeredClient, clientPrincipal, accessToken, refreshToken, additionalParameters);
when(this.authenticationManager.authenticate(any())).thenReturn(accessTokenAuthentication);
@@ -247,6 +254,8 @@ public class OAuth2TokenEndpointFilterTests {
assertThat(accessTokenResult.getExpiresAt()).isBetween(
accessToken.getExpiresAt().minusSeconds(1), accessToken.getExpiresAt().plusSeconds(1));
assertThat(accessTokenResult.getScopes()).isEqualTo(accessToken.getScopes());
assertThat(accessTokenResponse.getRefreshToken().getTokenValue()).isEqualTo(refreshToken.getTokenValue());
assertThat(accessTokenResponse.getAdditionalParameters()).containsExactly(entry("custom-param", "custom-value"));
}
@Test
@@ -260,7 +269,7 @@ public class OAuth2TokenEndpointFilterTests {
}
@Test
public void doFilterWhenClientCredentialsTokenRequestValidThenAccessTokenResponse() throws Exception {
public void doFilterWhenClientCredentialsTokenRequestThenAccessTokenResponse() throws Exception {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient2().build();
Authentication clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient);
OAuth2AccessToken accessToken = new OAuth2AccessToken(
@@ -338,7 +347,7 @@ public class OAuth2TokenEndpointFilterTests {
}
@Test
public void doFilterWhenRefreshTokenRequestValidThenAccessTokenResponse() throws Exception {
public void doFilterWhenRefreshTokenRequestThenAccessTokenResponse() throws Exception {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
Authentication clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient);
OAuth2AccessToken accessToken = new OAuth2AccessToken(

View File

@@ -112,6 +112,7 @@ public class OidcProviderConfigurationEndpointFilterTests {
assertThat(providerConfigurationResponse).contains("\"response_types_supported\":[\"code\"]");
assertThat(providerConfigurationResponse).contains("\"grant_types_supported\":[\"authorization_code\",\"client_credentials\",\"refresh_token\"]");
assertThat(providerConfigurationResponse).contains("\"subject_types_supported\":[\"public\"]");
assertThat(providerConfigurationResponse).contains("\"id_token_signing_alg_values_supported\":[\"RS256\"]");
assertThat(providerConfigurationResponse).contains("\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\"]");
}