diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProvider.java index 73357a1..0d86780 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProvider.java @@ -84,6 +84,11 @@ public class OAuth2ClientAuthenticationProvider implements AuthenticationProvide boolean authenticatedCredentials = false; + if (!registeredClient.getClientAuthenticationMethods().contains( + clientAuthentication.getClientAuthenticationMethod())) { + throwInvalidClient(); + } + if (clientAuthentication.getCredentials() != null) { String clientSecret = clientAuthentication.getCredentials().toString(); // TODO Use PasswordEncoder.matches() diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationToken.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationToken.java index 921e475..a3d01f4 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationToken.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationToken.java @@ -18,6 +18,7 @@ package org.springframework.security.oauth2.server.authorization.authentication; import org.springframework.lang.Nullable; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.server.authorization.Version; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.util.Assert; @@ -30,6 +31,7 @@ import java.util.Map; * * @author Joe Grandja * @author Patryk Kostrzewa + * @author Anoop Garlapati * @since 0.0.1 * @see AbstractAuthenticationToken * @see RegisteredClient @@ -39,6 +41,7 @@ public class OAuth2ClientAuthenticationToken extends AbstractAuthenticationToken private static final long serialVersionUID = Version.SERIAL_VERSION_UID; private String clientId; private String clientSecret; + private ClientAuthenticationMethod clientAuthenticationMethod; private Map additionalParameters; private RegisteredClient registeredClient; @@ -47,13 +50,17 @@ public class OAuth2ClientAuthenticationToken extends AbstractAuthenticationToken * * @param clientId the client identifier * @param clientSecret the client secret + * @param clientAuthenticationMethod the authentication method used by the client * @param additionalParameters the additional parameters */ public OAuth2ClientAuthenticationToken(String clientId, String clientSecret, + ClientAuthenticationMethod clientAuthenticationMethod, @Nullable Map additionalParameters) { this(clientId, additionalParameters); Assert.hasText(clientSecret, "clientSecret cannot be empty"); + Assert.notNull(clientAuthenticationMethod, "clientAuthenticationMethod cannot be null"); this.clientSecret = clientSecret; + this.clientAuthenticationMethod = clientAuthenticationMethod; } /** @@ -69,6 +76,7 @@ public class OAuth2ClientAuthenticationToken extends AbstractAuthenticationToken this.clientId = clientId; this.additionalParameters = additionalParameters != null ? Collections.unmodifiableMap(additionalParameters) : null; + this.clientAuthenticationMethod = ClientAuthenticationMethod.NONE; } /** @@ -112,4 +120,13 @@ public class OAuth2ClientAuthenticationToken extends AbstractAuthenticationToken public @Nullable RegisteredClient getRegisteredClient() { return this.registeredClient; } + + /** + * Returns the {@link ClientAuthenticationMethod client authentication method}. + * + * @return the {@link ClientAuthenticationMethod} + */ + public @Nullable ClientAuthenticationMethod getClientAuthenticationMethod() { + return this.clientAuthenticationMethod; + } } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/ClientSecretBasicAuthenticationConverter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/ClientSecretBasicAuthenticationConverter.java index b0ef010..aa81808 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/ClientSecretBasicAuthenticationConverter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/ClientSecretBasicAuthenticationConverter.java @@ -17,6 +17,7 @@ package org.springframework.security.oauth2.server.authorization.web; import org.springframework.http.HttpHeaders; import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; @@ -85,7 +86,8 @@ public class ClientSecretBasicAuthenticationConverter implements AuthenticationC throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST), ex); } - return new OAuth2ClientAuthenticationToken(clientID, clientSecret, extractAdditionalParameters(request)); + return new OAuth2ClientAuthenticationToken(clientID, clientSecret, ClientAuthenticationMethod.BASIC, + extractAdditionalParameters(request)); } private static Map extractAdditionalParameters(HttpServletRequest request) { diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/ClientSecretPostAuthenticationConverter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/ClientSecretPostAuthenticationConverter.java new file mode 100644 index 0000000..1b82d3f --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/ClientSecretPostAuthenticationConverter.java @@ -0,0 +1,84 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.server.authorization.web; + +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; +import org.springframework.security.web.authentication.AuthenticationConverter; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; + +import javax.servlet.http.HttpServletRequest; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Attempts to extract client credentials from POST parameters of {@link HttpServletRequest} + * and then converts to an {@link OAuth2ClientAuthenticationToken} used for authenticating the client. + * + * @author Anoop Garlapati + * @since 0.1.0 + * @see OAuth2ClientAuthenticationToken + * @see OAuth2ClientAuthenticationFilter + * @see Section 2.3.1 Client Password + */ +public class ClientSecretPostAuthenticationConverter implements AuthenticationConverter { + + @Override + public Authentication convert(HttpServletRequest request) { + MultiValueMap parameters = OAuth2EndpointUtils.getParameters(request); + + // client_id (REQUIRED) + String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID); + if (!StringUtils.hasText(clientId)) { + return null; + } + + if (parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) { + throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST)); + } + + // client_secret (REQUIRED) + String clientSecret = parameters.getFirst(OAuth2ParameterNames.CLIENT_SECRET); + if (!StringUtils.hasText(clientSecret)) { + return null; + } + + if (parameters.get(OAuth2ParameterNames.CLIENT_SECRET).size() != 1) { + throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST)); + } + + return new OAuth2ClientAuthenticationToken(clientId, clientSecret, ClientAuthenticationMethod.POST, + extractAdditionalParameters(request)); + } + + private static Map extractAdditionalParameters(HttpServletRequest request) { + Map additionalParameters = Collections.emptyMap(); + if (OAuth2EndpointUtils.matchesPkceTokenRequest(request)) { + // Confidential clients can also leverage PKCE + additionalParameters = new HashMap<>(OAuth2EndpointUtils.getParameters(request).toSingleValueMap()); + additionalParameters.remove(OAuth2ParameterNames.CLIENT_ID); + additionalParameters.remove(OAuth2ParameterNames.CLIENT_SECRET); + } + return additionalParameters; + } +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilter.java index 280cda9..a589df1 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilter.java @@ -77,6 +77,7 @@ public class OAuth2ClientAuthenticationFilter extends OncePerRequestFilter { this.authenticationConverter = new DelegatingAuthenticationConverter( Arrays.asList( new ClientSecretBasicAuthenticationConverter(), + new ClientSecretPostAuthenticationConverter(), new PublicClientAuthenticationConverter())); this.authenticationSuccessHandler = this::onAuthenticationSuccess; this.authenticationFailureHandler = this::onAuthenticationFailure; diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationCodeGrantTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationCodeGrantTests.java index d3e0f2e..20ea649 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationCodeGrantTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationCodeGrantTests.java @@ -181,9 +181,7 @@ public class OAuth2AuthorizationCodeGrantTests { public void requestWhenPublicClientWithPkceThenReturnAccessTokenResponse() throws Exception { this.spring.register(AuthorizationServerConfiguration.class).autowire(); - RegisteredClient registeredClient = TestRegisteredClients.registeredClient() - .clientSecret(null) - .clientSettings(clientSettings -> clientSettings.requireProofKey(true)) + RegisteredClient registeredClient = TestRegisteredClients.registeredPublicClient() .tokenSettings(tokenSettings -> tokenSettings.enableRefreshTokens(false)) .build(); when(registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java index 640567f..9cccc96 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java @@ -19,6 +19,7 @@ import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; @@ -119,7 +120,7 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests { public void authenticateWhenClientPrincipalNotAuthenticatedThenThrowOAuth2AuthenticationException() { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken( - registeredClient.getClientId(), registeredClient.getClientSecret(), null); + registeredClient.getClientId(), registeredClient.getClientSecret(), ClientAuthenticationMethod.BASIC, null); OAuth2AuthorizationCodeAuthenticationToken authentication = new OAuth2AuthorizationCodeAuthenticationToken(AUTHORIZATION_CODE, clientPrincipal, null, null); assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProviderTests.java index dd3566f..36df4bd 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProviderTests.java @@ -18,6 +18,7 @@ package org.springframework.security.oauth2.server.authorization.authentication; import org.junit.Before; import org.junit.Test; import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; @@ -45,6 +46,7 @@ import static org.mockito.Mockito.when; * @author Patryk Kostrzewa * @author Joe Grandja * @author Daniel Garnier-Moiroux + * @author Anoop Garlapati */ public class OAuth2ClientAuthenticationProviderTests { private static final String PLAIN_CODE_VERIFIER = "pkce-key"; @@ -95,7 +97,7 @@ public class OAuth2ClientAuthenticationProviderTests { .thenReturn(registeredClient); OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken( - registeredClient.getClientId() + "-invalid", registeredClient.getClientSecret(), null); + registeredClient.getClientId() + "-invalid", registeredClient.getClientSecret(), ClientAuthenticationMethod.BASIC, null); assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) .isInstanceOf(OAuth2AuthenticationException.class) .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) @@ -110,7 +112,7 @@ public class OAuth2ClientAuthenticationProviderTests { .thenReturn(registeredClient); OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken( - registeredClient.getClientId(), registeredClient.getClientSecret() + "-invalid", null); + registeredClient.getClientId(), registeredClient.getClientSecret() + "-invalid", ClientAuthenticationMethod.BASIC, null); assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) .isInstanceOf(OAuth2AuthenticationException.class) .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) @@ -140,7 +142,7 @@ public class OAuth2ClientAuthenticationProviderTests { .thenReturn(registeredClient); OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken( - registeredClient.getClientId(), registeredClient.getClientSecret(), null); + registeredClient.getClientId(), registeredClient.getClientSecret(), ClientAuthenticationMethod.BASIC, null); OAuth2ClientAuthenticationToken authenticationResult = (OAuth2ClientAuthenticationToken) this.authenticationProvider.authenticate(authentication); assertThat(authenticationResult.isAuthenticated()).isTrue(); @@ -275,7 +277,7 @@ public class OAuth2ClientAuthenticationProviderTests { @Test public void authenticateWhenPkceAndPlainMethodAndValidCodeVerifierThenAuthenticated() { - RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + RegisteredClient registeredClient = TestRegisteredClients.registeredPublicClient().build(); when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) .thenReturn(registeredClient); @@ -300,7 +302,7 @@ public class OAuth2ClientAuthenticationProviderTests { @Test public void authenticateWhenPkceAndMissingMethodThenDefaultPlainMethodAndAuthenticated() { - RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + RegisteredClient registeredClient = TestRegisteredClients.registeredPublicClient().build(); when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) .thenReturn(registeredClient); @@ -327,7 +329,7 @@ public class OAuth2ClientAuthenticationProviderTests { @Test public void authenticateWhenPkceAndS256MethodAndValidCodeVerifierThenAuthenticated() { - RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + RegisteredClient registeredClient = TestRegisteredClients.registeredPublicClient().build(); when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) .thenReturn(registeredClient); @@ -352,7 +354,7 @@ public class OAuth2ClientAuthenticationProviderTests { @Test public void authenticateWhenPkceAndUnsupportedCodeChallengeMethodThenThrowOAuth2AuthenticationException() { - RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + RegisteredClient registeredClient = TestRegisteredClients.registeredPublicClient().build(); when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) .thenReturn(registeredClient); @@ -377,6 +379,21 @@ public class OAuth2ClientAuthenticationProviderTests { .isEqualTo(OAuth2ErrorCodes.SERVER_ERROR); } + @Test + public void authenticateWhenClientAuthenticationWithUnregisteredClientAuthenticationMethodThenThrowOAuth2AuthenticationException() { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) + .thenReturn(registeredClient); + + OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken( + registeredClient.getClientId(), registeredClient.getClientSecret(), ClientAuthenticationMethod.POST, null); + assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) + .isInstanceOf(OAuth2AuthenticationException.class) + .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) + .extracting("errorCode") + .isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT); + } + private static Map createPkceTokenParameters(String codeVerifier) { Map parameters = new HashMap<>(); parameters.put(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue()); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationTokenTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationTokenTests.java index f4ba363..5ce04df 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationTokenTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationTokenTests.java @@ -16,6 +16,7 @@ package org.springframework.security.oauth2.server.authorization.authentication; import org.junit.Test; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; @@ -29,23 +30,31 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; * Tests for {@link OAuth2ClientAuthenticationToken}. * * @author Joe Grandja + * @author Anoop Garlapati */ public class OAuth2ClientAuthenticationTokenTests { @Test public void constructorWhenClientIdNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> new OAuth2ClientAuthenticationToken(null, "secret", null)) + assertThatThrownBy(() -> new OAuth2ClientAuthenticationToken(null, "secret", ClientAuthenticationMethod.BASIC, null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("clientId cannot be empty"); } @Test public void constructorWhenClientSecretNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> new OAuth2ClientAuthenticationToken("clientId", null, null)) + assertThatThrownBy(() -> new OAuth2ClientAuthenticationToken("clientId", null, ClientAuthenticationMethod.BASIC, null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("clientSecret cannot be empty"); } + @Test + public void constructorWhenClientAuthenticationMethodNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new OAuth2ClientAuthenticationToken("clientId", "clientSecret", null, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("clientAuthenticationMethod cannot be null"); + } + @Test public void constructorWhenRegisteredClientNullThenThrowIllegalArgumentException() { assertThatThrownBy(() -> new OAuth2ClientAuthenticationToken(null)) @@ -55,11 +64,13 @@ public class OAuth2ClientAuthenticationTokenTests { @Test public void constructorWhenClientCredentialsProvidedThenCreated() { - OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken("clientId", "secret", null); + OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken("clientId", "secret", + ClientAuthenticationMethod.BASIC, null); assertThat(authentication.isAuthenticated()).isFalse(); assertThat(authentication.getPrincipal().toString()).isEqualTo("clientId"); assertThat(authentication.getCredentials()).isEqualTo("secret"); assertThat(authentication.getRegisteredClient()).isNull(); + assertThat(authentication.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC); } @Test @@ -72,6 +83,7 @@ public class OAuth2ClientAuthenticationTokenTests { assertThat(authentication.getCredentials()).isNull(); assertThat(authentication.getAdditionalParameters()).isEqualTo(additionalParameters); assertThat(authentication.getRegisteredClient()).isNull(); + assertThat(authentication.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.NONE); } @Test @@ -83,4 +95,15 @@ public class OAuth2ClientAuthenticationTokenTests { assertThat(authentication.getCredentials()).isNull(); assertThat(authentication.getRegisteredClient()).isEqualTo(registeredClient); } + + @Test + public void constructorWhenClientCredentialsAndClientAuthenticationMethodProvidedThenCreated() { + OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken("clientId", "secret", + ClientAuthenticationMethod.BASIC, null); + assertThat(authentication.isAuthenticated()).isFalse(); + assertThat(authentication.getPrincipal().toString()).isEqualTo("clientId"); + assertThat(authentication.getCredentials()).isEqualTo("secret"); + assertThat(authentication.getRegisteredClient()).isNull(); + assertThat(authentication.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC); + } } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProviderTests.java index dbef6db..b78fb43 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProviderTests.java @@ -20,6 +20,7 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.jose.JoseHeaderNames; @@ -104,7 +105,7 @@ public class OAuth2ClientCredentialsAuthenticationProviderTests { public void authenticateWhenClientPrincipalNotAuthenticatedThenThrowOAuth2AuthenticationException() { RegisteredClient registeredClient = TestRegisteredClients.registeredClient2().build(); OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken( - registeredClient.getClientId(), registeredClient.getClientSecret(), null); + registeredClient.getClientId(), registeredClient.getClientSecret(), ClientAuthenticationMethod.BASIC, null); OAuth2ClientCredentialsAuthenticationToken authentication = new OAuth2ClientCredentialsAuthenticationToken(clientPrincipal); assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProviderTests.java index b83b50c..628d922 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProviderTests.java @@ -20,6 +20,7 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.OAuth2RefreshToken; @@ -234,7 +235,7 @@ public class OAuth2RefreshTokenAuthenticationProviderTests { public void authenticateWhenClientPrincipalNotAuthenticatedThenThrowOAuth2AuthenticationException() { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken( - registeredClient.getClientId(), registeredClient.getClientSecret(), null); + registeredClient.getClientId(), registeredClient.getClientSecret(), ClientAuthenticationMethod.BASIC, null); OAuth2RefreshTokenAuthenticationToken authentication = new OAuth2RefreshTokenAuthenticationToken( "refresh-token", clientPrincipal); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenRevocationAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenRevocationAuthenticationProviderTests.java index 74a960c..bc09c67 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenRevocationAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenRevocationAuthenticationProviderTests.java @@ -19,6 +19,7 @@ import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; @@ -86,7 +87,7 @@ public class OAuth2TokenRevocationAuthenticationProviderTests { public void authenticateWhenClientPrincipalNotAuthenticatedThenThrowOAuth2AuthenticationException() { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken( - registeredClient.getClientId(), registeredClient.getClientSecret(), null); + registeredClient.getClientId(), registeredClient.getClientSecret(), ClientAuthenticationMethod.BASIC, null); OAuth2TokenRevocationAuthenticationToken authentication = new OAuth2TokenRevocationAuthenticationToken( "token", clientPrincipal, TokenType.ACCESS_TOKEN.getValue()); assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/client/TestRegisteredClients.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/client/TestRegisteredClients.java index 33e0001..a321ca3 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/client/TestRegisteredClients.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/client/TestRegisteredClients.java @@ -51,4 +51,16 @@ public class TestRegisteredClients { .scope("scope1") .scope("scope2"); } + + public static RegisteredClient.Builder registeredPublicClient() { + return RegisteredClient.withId("registration-3") + .clientId("client-3") + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .clientAuthenticationMethod(ClientAuthenticationMethod.NONE) + .redirectUri("https://example.com") + .scope("openid") + .scope("profile") + .scope("email") + .clientSettings(clientSettings -> clientSettings.requireProofKey(true)); + } } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/ClientSecretBasicAuthenticationConverterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/ClientSecretBasicAuthenticationConverterTests.java index a91647b..863b5c0 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/ClientSecretBasicAuthenticationConverterTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/ClientSecretBasicAuthenticationConverterTests.java @@ -20,6 +20,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; @@ -98,6 +99,7 @@ public class ClientSecretBasicAuthenticationConverterTests { OAuth2ClientAuthenticationToken authentication = (OAuth2ClientAuthenticationToken) this.converter.convert(request); assertThat(authentication.getPrincipal()).isEqualTo("clientId"); assertThat(authentication.getCredentials()).isEqualTo("secret"); + assertThat(authentication.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC); } @Test @@ -107,6 +109,7 @@ public class ClientSecretBasicAuthenticationConverterTests { OAuth2ClientAuthenticationToken authentication = (OAuth2ClientAuthenticationToken) this.converter.convert(request); assertThat(authentication.getPrincipal()).isEqualTo("clientId"); assertThat(authentication.getCredentials()).isEqualTo("secret"); + assertThat(authentication.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC); assertThat(authentication.getAdditionalParameters()) .containsOnly( entry(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue()), diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/ClientSecretPostAuthenticationConverterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/ClientSecretPostAuthenticationConverterTests.java new file mode 100644 index 0000000..c2b3ddf --- /dev/null +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/ClientSecretPostAuthenticationConverterTests.java @@ -0,0 +1,115 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.server.authorization.web; + +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.entry; + +/** + * Tests for {@link ClientSecretPostAuthenticationConverter}. + * + * @author Anoop Garlapati + */ +public class ClientSecretPostAuthenticationConverterTests { + private final ClientSecretPostAuthenticationConverter converter = new ClientSecretPostAuthenticationConverter(); + + @Test + public void convertWhenMissingClientIdThenReturnNull() { + MockHttpServletRequest request = new MockHttpServletRequest(); + Authentication authentication = this.converter.convert(request); + assertThat(authentication).isNull(); + } + + @Test + public void convertWhenMultipleClientIdsThenInvalidRequestError() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-1"); + request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-2"); + assertThatThrownBy(() -> this.converter.convert(request)) + .isInstanceOf(OAuth2AuthenticationException.class) + .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) + .extracting("errorCode") + .isEqualTo(OAuth2ErrorCodes.INVALID_REQUEST); + } + + @Test + public void convertWhenMissingClientSecretThenReturnNull() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-1"); + Authentication authentication = this.converter.convert(request); + assertThat(authentication).isNull(); + } + + @Test + public void convertWhenMultipleClientSecretsThenInvalidRequestError() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-1"); + request.addParameter(OAuth2ParameterNames.CLIENT_SECRET, "client-secret-1"); + request.addParameter(OAuth2ParameterNames.CLIENT_SECRET, "client-secret-2"); + assertThatThrownBy(() -> this.converter.convert(request)) + .isInstanceOf(OAuth2AuthenticationException.class) + .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) + .extracting("errorCode") + .isEqualTo(OAuth2ErrorCodes.INVALID_REQUEST); + } + + @Test + public void convertWhenPostWithValidCredentialsThenReturnClientAuthenticationToken() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-1"); + request.addParameter(OAuth2ParameterNames.CLIENT_SECRET, "client-secret"); + OAuth2ClientAuthenticationToken authentication = (OAuth2ClientAuthenticationToken) this.converter.convert(request); + assertThat(authentication.getPrincipal()).isEqualTo("client-1"); + assertThat(authentication.getCredentials()).isEqualTo("client-secret"); + assertThat(authentication.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.POST); + } + + @Test + public void convertWhenConfidentialClientWithPkceParametersThenAdditionalParametersIncluded() { + MockHttpServletRequest request = createPkceTokenRequest(); + request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-1"); + request.addParameter(OAuth2ParameterNames.CLIENT_SECRET, "client-secret"); + OAuth2ClientAuthenticationToken authentication = (OAuth2ClientAuthenticationToken) this.converter.convert(request); + assertThat(authentication.getPrincipal()).isEqualTo("client-1"); + assertThat(authentication.getCredentials()).isEqualTo("client-secret"); + assertThat(authentication.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.POST); + assertThat(authentication.getAdditionalParameters()) + .containsOnly( + entry(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue()), + entry(OAuth2ParameterNames.CODE, "code"), + entry(PkceParameterNames.CODE_VERIFIER, "code-verifier-1")); + } + + private static MockHttpServletRequest createPkceTokenRequest() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue()); + request.addParameter(OAuth2ParameterNames.CODE, "code"); + request.addParameter(PkceParameterNames.CODE_VERIFIER, "code-verifier-1"); + return request; + } +} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilterTests.java index 7867f31..bf778cc 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilterTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilterTests.java @@ -27,6 +27,7 @@ import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; @@ -162,7 +163,7 @@ public class OAuth2ClientAuthenticationFilterTests { @Test public void doFilterWhenRequestMatchesAndBadCredentialsThenInvalidClientError() throws Exception { when(this.authenticationConverter.convert(any(HttpServletRequest.class))).thenReturn( - new OAuth2ClientAuthenticationToken("clientId", "invalid-secret", null)); + new OAuth2ClientAuthenticationToken("clientId", "invalid-secret", ClientAuthenticationMethod.BASIC, null)); when(this.authenticationManager.authenticate(any(Authentication.class))).thenThrow( new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT))); @@ -185,7 +186,7 @@ public class OAuth2ClientAuthenticationFilterTests { public void doFilterWhenRequestMatchesAndValidCredentialsThenProcessed() throws Exception { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); when(this.authenticationConverter.convert(any(HttpServletRequest.class))).thenReturn( - new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), registeredClient.getClientSecret(), null)); + new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), registeredClient.getClientSecret(), ClientAuthenticationMethod.BASIC, null)); when(this.authenticationManager.authenticate(any(Authentication.class))).thenReturn( new OAuth2ClientAuthenticationToken(registeredClient)); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/PublicClientAuthenticationConverterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/PublicClientAuthenticationConverterTests.java index ced154b..6404889 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/PublicClientAuthenticationConverterTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/PublicClientAuthenticationConverterTests.java @@ -19,6 +19,7 @@ import org.junit.Test; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; @@ -82,6 +83,7 @@ public class PublicClientAuthenticationConverterTests { MockHttpServletRequest request = createPkceTokenRequest(); OAuth2ClientAuthenticationToken authentication = (OAuth2ClientAuthenticationToken) this.converter.convert(request); assertThat(authentication.getPrincipal()).isEqualTo("client-1"); + assertThat(authentication.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.NONE); assertThat(authentication.getAdditionalParameters()) .containsOnly( entry(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue()),