Move PKCE to OAuth2ClientAuthenticationProvider
PR gh-93
This commit is contained in:
parent
e5fdee3034
commit
5c31fb1b7e
@ -120,7 +120,8 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
|
|||||||
public void init(B builder) {
|
public void init(B builder) {
|
||||||
OAuth2ClientAuthenticationProvider clientAuthenticationProvider =
|
OAuth2ClientAuthenticationProvider clientAuthenticationProvider =
|
||||||
new OAuth2ClientAuthenticationProvider(
|
new OAuth2ClientAuthenticationProvider(
|
||||||
getRegisteredClientRepository(builder));
|
getRegisteredClientRepository(builder),
|
||||||
|
getAuthorizationService(builder));
|
||||||
builder.authenticationProvider(postProcess(clientAuthenticationProvider));
|
builder.authenticationProvider(postProcess(clientAuthenticationProvider));
|
||||||
|
|
||||||
NimbusJwsEncoder jwtEncoder = new NimbusJwsEncoder(getKeyManager(builder));
|
NimbusJwsEncoder jwtEncoder = new NimbusJwsEncoder(getKeyManager(builder));
|
||||||
|
@ -24,7 +24,6 @@ import org.springframework.security.oauth2.core.OAuth2Error;
|
|||||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
|
||||||
import org.springframework.security.oauth2.jose.JoseHeader;
|
import org.springframework.security.oauth2.jose.JoseHeader;
|
||||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||||
import org.springframework.security.oauth2.jwt.Jwt;
|
import org.springframework.security.oauth2.jwt.Jwt;
|
||||||
@ -42,12 +41,8 @@ import org.springframework.util.StringUtils;
|
|||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.Base64;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -92,22 +87,13 @@ public class OAuth2AuthorizationCodeAuthenticationProvider implements Authentica
|
|||||||
(OAuth2AuthorizationCodeAuthenticationToken) authentication;
|
(OAuth2AuthorizationCodeAuthenticationToken) authentication;
|
||||||
|
|
||||||
OAuth2ClientAuthenticationToken clientPrincipal = null;
|
OAuth2ClientAuthenticationToken clientPrincipal = null;
|
||||||
RegisteredClient registeredClient;
|
|
||||||
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authorizationCodeAuthentication.getPrincipal().getClass())) {
|
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authorizationCodeAuthentication.getPrincipal().getClass())) {
|
||||||
clientPrincipal = (OAuth2ClientAuthenticationToken) authorizationCodeAuthentication.getPrincipal();
|
clientPrincipal = (OAuth2ClientAuthenticationToken) authorizationCodeAuthentication.getPrincipal();
|
||||||
if (!clientPrincipal.isAuthenticated()) {
|
}
|
||||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
|
if (clientPrincipal == null || !clientPrincipal.isAuthenticated()) {
|
||||||
}
|
|
||||||
registeredClient = clientPrincipal.getRegisteredClient();
|
|
||||||
} else if (StringUtils.hasText(authorizationCodeAuthentication.getClientId())) {
|
|
||||||
String clientId = authorizationCodeAuthentication.getClientId();
|
|
||||||
registeredClient = this.registeredClientRepository.findByClientId(clientId);
|
|
||||||
if (registeredClient == null) {
|
|
||||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
|
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
|
||||||
}
|
}
|
||||||
|
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
|
||||||
|
|
||||||
OAuth2Authorization authorization = this.authorizationService.findByToken(
|
OAuth2Authorization authorization = this.authorizationService.findByToken(
|
||||||
authorizationCodeAuthentication.getCode(), TokenType.AUTHORIZATION_CODE);
|
authorizationCodeAuthentication.getCode(), TokenType.AUTHORIZATION_CODE);
|
||||||
@ -127,26 +113,6 @@ public class OAuth2AuthorizationCodeAuthenticationProvider implements Authentica
|
|||||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
|
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate PKCE parameters
|
|
||||||
String codeChallenge = (String) authorizationRequest
|
|
||||||
.getAdditionalParameters()
|
|
||||||
.get(PkceParameterNames.CODE_CHALLENGE);
|
|
||||||
if (StringUtils.hasText(codeChallenge)) {
|
|
||||||
String codeChallengeMethod = (String) authorizationRequest
|
|
||||||
.getAdditionalParameters()
|
|
||||||
.get(PkceParameterNames.CODE_CHALLENGE_METHOD);
|
|
||||||
|
|
||||||
String codeVerifier = (String) authorizationCodeAuthentication
|
|
||||||
.getAdditionalParameters()
|
|
||||||
.get(PkceParameterNames.CODE_VERIFIER);
|
|
||||||
|
|
||||||
if (!pkceCodeVerifierValid(codeVerifier, codeChallenge, codeChallengeMethod)) {
|
|
||||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
|
|
||||||
}
|
|
||||||
} else if (registeredClient.getClientSettings().requireProofKey()) {
|
|
||||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
|
|
||||||
}
|
|
||||||
|
|
||||||
JoseHeader joseHeader = JoseHeader.withAlgorithm(SignatureAlgorithm.RS256).build();
|
JoseHeader joseHeader = JoseHeader.withAlgorithm(SignatureAlgorithm.RS256).build();
|
||||||
|
|
||||||
// TODO Allow configuration for issuer claim
|
// TODO Allow configuration for issuer claim
|
||||||
@ -179,28 +145,7 @@ public class OAuth2AuthorizationCodeAuthenticationProvider implements Authentica
|
|||||||
.build();
|
.build();
|
||||||
this.authorizationService.save(authorization);
|
this.authorizationService.save(authorization);
|
||||||
|
|
||||||
return clientPrincipal != null ?
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken);
|
||||||
new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken) :
|
|
||||||
new OAuth2AccessTokenAuthenticationToken(registeredClient, new OAuth2ClientAuthenticationToken(registeredClient), accessToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean pkceCodeVerifierValid(String codeVerifier, String codeChallenge, String codeChallengeMethod) {
|
|
||||||
if (!StringUtils.hasText(codeVerifier)) {
|
|
||||||
return false;
|
|
||||||
} else if (!StringUtils.hasText(codeChallengeMethod) || "plain".equals(codeChallengeMethod)) {
|
|
||||||
return codeVerifier.equals(codeChallenge);
|
|
||||||
} else if ("S256".equals(codeChallengeMethod)) {
|
|
||||||
try {
|
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
|
||||||
byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));
|
|
||||||
String encodedVerifier = Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
|
|
||||||
return encodedVerifier.equals(codeChallenge);
|
|
||||||
} catch (NoSuchAlgorithmException ex) {
|
|
||||||
// It is unlikely that SHA-256 is not available on the server. If it is not available,
|
|
||||||
// there will likely be bigger issues as well. We default to SERVER_ERROR.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -39,7 +39,6 @@ public class OAuth2AuthorizationCodeAuthenticationToken extends AbstractAuthenti
|
|||||||
private static final long serialVersionUID = SpringSecurityCoreVersion2.SERIAL_VERSION_UID;
|
private static final long serialVersionUID = SpringSecurityCoreVersion2.SERIAL_VERSION_UID;
|
||||||
private final String code;
|
private final String code;
|
||||||
private final Authentication clientPrincipal;
|
private final Authentication clientPrincipal;
|
||||||
private final String clientId;
|
|
||||||
private final String redirectUri;
|
private final String redirectUri;
|
||||||
private final Map<String, Object> additionalParameters;
|
private final Map<String, Object> additionalParameters;
|
||||||
|
|
||||||
@ -58,32 +57,6 @@ public class OAuth2AuthorizationCodeAuthenticationToken extends AbstractAuthenti
|
|||||||
Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
|
Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
|
||||||
this.code = code;
|
this.code = code;
|
||||||
this.clientPrincipal = clientPrincipal;
|
this.clientPrincipal = clientPrincipal;
|
||||||
this.clientId = OAuth2ClientAuthenticationToken.class.isAssignableFrom(this.clientPrincipal.getClass()) ?
|
|
||||||
(String) this.clientPrincipal.getPrincipal() :
|
|
||||||
null;
|
|
||||||
this.redirectUri = redirectUri;
|
|
||||||
this.additionalParameters = Collections.unmodifiableMap(
|
|
||||||
additionalParameters != null ?
|
|
||||||
additionalParameters :
|
|
||||||
Collections.emptyMap());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs an {@code OAuth2AuthorizationCodeAuthenticationToken} using the provided parameters.
|
|
||||||
*
|
|
||||||
* @param code the authorization code
|
|
||||||
* @param clientId the client identifier
|
|
||||||
* @param redirectUri the redirect uri
|
|
||||||
* @param additionalParameters the additional parameters
|
|
||||||
*/
|
|
||||||
public OAuth2AuthorizationCodeAuthenticationToken(String code, String clientId,
|
|
||||||
@Nullable String redirectUri, @Nullable Map<String, Object> additionalParameters) {
|
|
||||||
super(Collections.emptyList());
|
|
||||||
Assert.hasText(code, "code cannot be empty");
|
|
||||||
Assert.hasText(clientId, "clientId cannot be empty");
|
|
||||||
this.code = code;
|
|
||||||
this.clientPrincipal = null;
|
|
||||||
this.clientId = clientId;
|
|
||||||
this.redirectUri = redirectUri;
|
this.redirectUri = redirectUri;
|
||||||
this.additionalParameters = Collections.unmodifiableMap(
|
this.additionalParameters = Collections.unmodifiableMap(
|
||||||
additionalParameters != null ?
|
additionalParameters != null ?
|
||||||
@ -93,7 +66,7 @@ public class OAuth2AuthorizationCodeAuthenticationToken extends AbstractAuthenti
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object getPrincipal() {
|
public Object getPrincipal() {
|
||||||
return this.clientPrincipal != null ? this.clientPrincipal : this.clientId;
|
return this.clientPrincipal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -110,15 +83,6 @@ public class OAuth2AuthorizationCodeAuthenticationToken extends AbstractAuthenti
|
|||||||
return this.code;
|
return this.code;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the client identifier
|
|
||||||
*
|
|
||||||
* @return the client identifier
|
|
||||||
*/
|
|
||||||
public @Nullable String getClientId() {
|
|
||||||
return this.clientId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the redirect uri.
|
* Returns the redirect uri.
|
||||||
*
|
*
|
||||||
|
@ -18,49 +18,80 @@ package org.springframework.security.oauth2.server.authorization.authentication;
|
|||||||
import org.springframework.security.authentication.AuthenticationProvider;
|
import org.springframework.security.authentication.AuthenticationProvider;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.AuthenticationException;
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
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.endpoint.PkceParameterNames;
|
||||||
|
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.TokenType;
|
||||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
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.RegisteredClientRepository;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An {@link AuthenticationProvider} implementation that validates {@link OAuth2ClientAuthenticationToken}'s.
|
* An {@link AuthenticationProvider} implementation used for authenticating an OAuth 2.0 Client.
|
||||||
*
|
*
|
||||||
* @author Joe Grandja
|
* @author Joe Grandja
|
||||||
* @author Patryk Kostrzewa
|
* @author Patryk Kostrzewa
|
||||||
|
* @author Daniel Garnier-Moiroux
|
||||||
* @since 0.0.1
|
* @since 0.0.1
|
||||||
* @see AuthenticationProvider
|
* @see AuthenticationProvider
|
||||||
* @see OAuth2ClientAuthenticationToken
|
* @see OAuth2ClientAuthenticationToken
|
||||||
* @see RegisteredClientRepository
|
* @see RegisteredClientRepository
|
||||||
|
* @see OAuth2AuthorizationService
|
||||||
*/
|
*/
|
||||||
public class OAuth2ClientAuthenticationProvider implements AuthenticationProvider {
|
public class OAuth2ClientAuthenticationProvider implements AuthenticationProvider {
|
||||||
private final RegisteredClientRepository registeredClientRepository;
|
private final RegisteredClientRepository registeredClientRepository;
|
||||||
|
private final OAuth2AuthorizationService authorizationService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs an {@code OAuth2ClientAuthenticationProvider} using the provided parameters.
|
* Constructs an {@code OAuth2ClientAuthenticationProvider} using the provided parameters.
|
||||||
*
|
*
|
||||||
* @param registeredClientRepository the repository of registered clients
|
* @param registeredClientRepository the repository of registered clients
|
||||||
|
* @param authorizationService the authorization service
|
||||||
*/
|
*/
|
||||||
public OAuth2ClientAuthenticationProvider(RegisteredClientRepository registeredClientRepository) {
|
public OAuth2ClientAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
|
||||||
|
OAuth2AuthorizationService authorizationService) {
|
||||||
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
|
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
|
||||||
|
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||||
this.registeredClientRepository = registeredClientRepository;
|
this.registeredClientRepository = registeredClientRepository;
|
||||||
|
this.authorizationService = authorizationService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||||
String clientId = authentication.getPrincipal().toString();
|
OAuth2ClientAuthenticationToken clientAuthentication =
|
||||||
|
(OAuth2ClientAuthenticationToken) authentication;
|
||||||
|
|
||||||
|
String clientId = clientAuthentication.getPrincipal().toString();
|
||||||
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
|
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
|
||||||
if (registeredClient == null) {
|
if (registeredClient == null) {
|
||||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
|
throwInvalidClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
String clientSecret = authentication.getCredentials().toString();
|
if (clientAuthentication.getCredentials() != null) {
|
||||||
if (!registeredClient.getClientSecret().equals(clientSecret)) {
|
String clientSecret = clientAuthentication.getCredentials().toString();
|
||||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
|
// TODO Use PasswordEncoder.matches()
|
||||||
|
if (!registeredClient.getClientSecret().equals(clientSecret)) {
|
||||||
|
throwInvalidClient();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authenticatePkceIfAvailable(clientAuthentication, registeredClient);
|
||||||
|
|
||||||
return new OAuth2ClientAuthenticationToken(registeredClient);
|
return new OAuth2ClientAuthenticationToken(registeredClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,4 +99,65 @@ public class OAuth2ClientAuthenticationProvider implements AuthenticationProvide
|
|||||||
public boolean supports(Class<?> authentication) {
|
public boolean supports(Class<?> authentication) {
|
||||||
return OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication);
|
return OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void authenticatePkceIfAvailable(OAuth2ClientAuthenticationToken clientAuthentication,
|
||||||
|
RegisteredClient registeredClient) {
|
||||||
|
|
||||||
|
Map<String, Object> parameters = clientAuthentication.getAdditionalParameters();
|
||||||
|
if (CollectionUtils.isEmpty(parameters) || !authorizationCodeGrant(parameters)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
OAuth2Authorization authorization = this.authorizationService.findByToken(
|
||||||
|
(String) parameters.get(OAuth2ParameterNames.CODE),
|
||||||
|
TokenType.AUTHORIZATION_CODE);
|
||||||
|
if (authorization == null) {
|
||||||
|
throwInvalidClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
|
||||||
|
OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST);
|
||||||
|
|
||||||
|
String codeChallenge = (String) authorizationRequest.getAdditionalParameters()
|
||||||
|
.get(PkceParameterNames.CODE_CHALLENGE);
|
||||||
|
if (StringUtils.hasText(codeChallenge)) {
|
||||||
|
String codeChallengeMethod = (String) authorizationRequest.getAdditionalParameters()
|
||||||
|
.get(PkceParameterNames.CODE_CHALLENGE_METHOD);
|
||||||
|
String codeVerifier = (String) parameters.get(PkceParameterNames.CODE_VERIFIER);
|
||||||
|
if (!codeVerifierValid(codeVerifier, codeChallenge, codeChallengeMethod)) {
|
||||||
|
throwInvalidClient();
|
||||||
|
}
|
||||||
|
} else if (registeredClient.getClientSettings().requireProofKey()) {
|
||||||
|
throwInvalidClient();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean authorizationCodeGrant(Map<String, Object> parameters) {
|
||||||
|
return AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(
|
||||||
|
parameters.get(OAuth2ParameterNames.GRANT_TYPE)) &&
|
||||||
|
parameters.get(OAuth2ParameterNames.CODE) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean codeVerifierValid(String codeVerifier, String codeChallenge, String codeChallengeMethod) {
|
||||||
|
if (!StringUtils.hasText(codeVerifier)) {
|
||||||
|
return false;
|
||||||
|
} else if (!StringUtils.hasText(codeChallengeMethod) || "plain".equals(codeChallengeMethod)) {
|
||||||
|
return codeVerifier.equals(codeChallenge);
|
||||||
|
} else if ("S256".equals(codeChallengeMethod)) {
|
||||||
|
try {
|
||||||
|
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||||
|
byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));
|
||||||
|
String encodedVerifier = Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
|
||||||
|
return encodedVerifier.equals(codeChallenge);
|
||||||
|
} catch (NoSuchAlgorithmException ex) {
|
||||||
|
// It is unlikely that SHA-256 is not available on the server. If it is not available,
|
||||||
|
// there will likely be bigger issues as well. We default to SERVER_ERROR.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void throwInvalidClient() {
|
||||||
|
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ import org.springframework.security.oauth2.server.authorization.client.Registere
|
|||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An {@link Authentication} implementation used for OAuth 2.0 Client Authentication.
|
* An {@link Authentication} implementation used for OAuth 2.0 Client Authentication.
|
||||||
@ -38,6 +39,7 @@ public class OAuth2ClientAuthenticationToken extends AbstractAuthenticationToken
|
|||||||
private static final long serialVersionUID = SpringSecurityCoreVersion2.SERIAL_VERSION_UID;
|
private static final long serialVersionUID = SpringSecurityCoreVersion2.SERIAL_VERSION_UID;
|
||||||
private String clientId;
|
private String clientId;
|
||||||
private String clientSecret;
|
private String clientSecret;
|
||||||
|
private Map<String, Object> additionalParameters;
|
||||||
private RegisteredClient registeredClient;
|
private RegisteredClient registeredClient;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -45,13 +47,28 @@ public class OAuth2ClientAuthenticationToken extends AbstractAuthenticationToken
|
|||||||
*
|
*
|
||||||
* @param clientId the client identifier
|
* @param clientId the client identifier
|
||||||
* @param clientSecret the client secret
|
* @param clientSecret the client secret
|
||||||
|
* @param additionalParameters the additional parameters
|
||||||
*/
|
*/
|
||||||
public OAuth2ClientAuthenticationToken(String clientId, String clientSecret) {
|
public OAuth2ClientAuthenticationToken(String clientId, String clientSecret,
|
||||||
|
@Nullable Map<String, Object> additionalParameters) {
|
||||||
|
this(clientId, additionalParameters);
|
||||||
|
Assert.hasText(clientSecret, "clientSecret cannot be empty");
|
||||||
|
this.clientSecret = clientSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an {@code OAuth2ClientAuthenticationToken} using the provided parameters.
|
||||||
|
*
|
||||||
|
* @param clientId the client identifier
|
||||||
|
* @param additionalParameters the additional parameters
|
||||||
|
*/
|
||||||
|
public OAuth2ClientAuthenticationToken(String clientId,
|
||||||
|
@Nullable Map<String, Object> additionalParameters) {
|
||||||
super(Collections.emptyList());
|
super(Collections.emptyList());
|
||||||
Assert.hasText(clientId, "clientId cannot be empty");
|
Assert.hasText(clientId, "clientId cannot be empty");
|
||||||
Assert.hasText(clientSecret, "clientSecret cannot be empty");
|
|
||||||
this.clientId = clientId;
|
this.clientId = clientId;
|
||||||
this.clientSecret = clientSecret;
|
this.additionalParameters = additionalParameters != null ?
|
||||||
|
Collections.unmodifiableMap(additionalParameters) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -78,6 +95,15 @@ public class OAuth2ClientAuthenticationToken extends AbstractAuthenticationToken
|
|||||||
return this.clientSecret;
|
return this.clientSecret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the additional parameters
|
||||||
|
*
|
||||||
|
* @return the additional parameters
|
||||||
|
*/
|
||||||
|
public @Nullable Map<String, Object> getAdditionalParameters() {
|
||||||
|
return this.additionalParameters;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the {@link RegisteredClient registered client}.
|
* Returns the {@link RegisteredClient registered client}.
|
||||||
*
|
*
|
||||||
|
@ -28,6 +28,9 @@ import javax.servlet.http.HttpServletRequest;
|
|||||||
import java.net.URLDecoder;
|
import java.net.URLDecoder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts to extract HTTP Basic credentials from {@link HttpServletRequest}
|
* Attempts to extract HTTP Basic credentials from {@link HttpServletRequest}
|
||||||
@ -82,6 +85,15 @@ public class ClientSecretBasicAuthenticationConverter implements AuthenticationC
|
|||||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST), ex);
|
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST), ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new OAuth2ClientAuthenticationToken(clientID, clientSecret);
|
return new OAuth2ClientAuthenticationToken(clientID, clientSecret, extractAdditionalParameters(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Object> extractAdditionalParameters(HttpServletRequest request) {
|
||||||
|
Map<String, Object> additionalParameters = Collections.emptyMap();
|
||||||
|
if (OAuth2EndpointUtils.matchesPkceTokenRequest(request)) {
|
||||||
|
// Confidential clients can also leverage PKCE
|
||||||
|
additionalParameters = new HashMap<>(OAuth2EndpointUtils.getParameters(request).toSingleValueMap());
|
||||||
|
}
|
||||||
|
return additionalParameters;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* 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.web.authentication.AuthenticationConverter;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An {@link AuthenticationConverter} that simply delegates to it's
|
||||||
|
* internal {@code List} of {@link AuthenticationConverter}(s).
|
||||||
|
* <p>
|
||||||
|
* Each {@link AuthenticationConverter} is given a chance to
|
||||||
|
* {@link AuthenticationConverter#convert(HttpServletRequest)}
|
||||||
|
* with the first {@code non-null} {@link Authentication} being returned.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 0.0.2
|
||||||
|
* @see AuthenticationConverter
|
||||||
|
*/
|
||||||
|
public final class DelegatingAuthenticationConverter implements AuthenticationConverter {
|
||||||
|
private final List<AuthenticationConverter> converters;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a {@code DelegatingAuthenticationConverter} using the provided parameters.
|
||||||
|
*
|
||||||
|
* @param converters a {@code List} of {@link AuthenticationConverter}(s)
|
||||||
|
*/
|
||||||
|
public DelegatingAuthenticationConverter(List<AuthenticationConverter> converters) {
|
||||||
|
Assert.notEmpty(converters, "converters cannot be empty");
|
||||||
|
this.converters = Collections.unmodifiableList(new LinkedList<>(converters));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Authentication convert(HttpServletRequest request) {
|
||||||
|
Assert.notNull(request, "request cannot be null");
|
||||||
|
// @formatter:off
|
||||||
|
return this.converters.stream()
|
||||||
|
.map(converter -> converter.convert(request))
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
}
|
@ -41,6 +41,7 @@ import javax.servlet.ServletException;
|
|||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@code Filter} that processes an authentication request for an OAuth 2.0 Client.
|
* A {@code Filter} that processes an authentication request for an OAuth 2.0 Client.
|
||||||
@ -73,7 +74,10 @@ public class OAuth2ClientAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
Assert.notNull(requestMatcher, "requestMatcher cannot be null");
|
Assert.notNull(requestMatcher, "requestMatcher cannot be null");
|
||||||
this.authenticationManager = authenticationManager;
|
this.authenticationManager = authenticationManager;
|
||||||
this.requestMatcher = requestMatcher;
|
this.requestMatcher = requestMatcher;
|
||||||
this.authenticationConverter = new ClientSecretBasicAuthenticationConverter();
|
this.authenticationConverter = new DelegatingAuthenticationConverter(
|
||||||
|
Arrays.asList(
|
||||||
|
new ClientSecretBasicAuthenticationConverter(),
|
||||||
|
new PublicClientAuthenticationConverter()));
|
||||||
this.authenticationSuccessHandler = this::onAuthenticationSuccess;
|
this.authenticationSuccessHandler = this::onAuthenticationSuccess;
|
||||||
this.authenticationFailureHandler = this::onAuthenticationFailure;
|
this.authenticationFailureHandler = this::onAuthenticationFailure;
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,9 @@
|
|||||||
*/
|
*/
|
||||||
package org.springframework.security.oauth2.server.authorization.web;
|
package org.springframework.security.oauth2.server.authorization.web;
|
||||||
|
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
||||||
import org.springframework.util.LinkedMultiValueMap;
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
import org.springframework.util.MultiValueMap;
|
import org.springframework.util.MultiValueMap;
|
||||||
|
|
||||||
@ -46,4 +49,11 @@ final class OAuth2EndpointUtils {
|
|||||||
});
|
});
|
||||||
return parameters;
|
return parameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static boolean matchesPkceTokenRequest(HttpServletRequest request) {
|
||||||
|
return AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(
|
||||||
|
request.getParameter(OAuth2ParameterNames.GRANT_TYPE)) &&
|
||||||
|
request.getParameter(OAuth2ParameterNames.CODE) != null &&
|
||||||
|
request.getParameter(PkceParameterNames.CODE_VERIFIER) != null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,13 +30,11 @@ import org.springframework.security.oauth2.core.OAuth2Error;
|
|||||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
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.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
|
||||||
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
|
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
|
||||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
|
||||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationToken;
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationToken;
|
||||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
|
|
||||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationToken;
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationToken;
|
||||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||||
@ -189,12 +187,6 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
|
|||||||
throw new OAuth2AuthenticationException(error);
|
throw new OAuth2AuthenticationException(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isClientAuthenticated(Authentication clientPrincipal) {
|
|
||||||
return clientPrincipal != null &&
|
|
||||||
OAuth2ClientAuthenticationToken.class.isAssignableFrom(clientPrincipal.getClass()) &&
|
|
||||||
clientPrincipal.isAuthenticated();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class AuthorizationCodeAuthenticationConverter implements Converter<HttpServletRequest, Authentication> {
|
private static class AuthorizationCodeAuthenticationConverter implements Converter<HttpServletRequest, Authentication> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -205,6 +197,8 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
|
||||||
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
|
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
|
||||||
|
|
||||||
// code (REQUIRED)
|
// code (REQUIRED)
|
||||||
@ -222,25 +216,6 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
|
|||||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI);
|
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI);
|
||||||
}
|
}
|
||||||
|
|
||||||
// client_id (REQUIRED)
|
|
||||||
// Required only if the client did not authenticate
|
|
||||||
String clientId = null;
|
|
||||||
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
|
|
||||||
if (!isClientAuthenticated(clientPrincipal)) {
|
|
||||||
clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);
|
|
||||||
if (!StringUtils.hasText(clientId) ||
|
|
||||||
parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {
|
|
||||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID);
|
|
||||||
}
|
|
||||||
|
|
||||||
// code_verifier (REQUIRED for public clients)
|
|
||||||
String codeVerifier = parameters.getFirst(PkceParameterNames.CODE_VERIFIER);
|
|
||||||
if (!StringUtils.hasText(codeVerifier) ||
|
|
||||||
parameters.get(PkceParameterNames.CODE_VERIFIER).size() != 1) {
|
|
||||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_VERIFIER);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, Object> additionalParameters = parameters
|
Map<String, Object> additionalParameters = parameters
|
||||||
.entrySet()
|
.entrySet()
|
||||||
.stream()
|
.stream()
|
||||||
@ -250,10 +225,7 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
|
|||||||
!e.getKey().equals(OAuth2ParameterNames.REDIRECT_URI))
|
!e.getKey().equals(OAuth2ParameterNames.REDIRECT_URI))
|
||||||
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));
|
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));
|
||||||
|
|
||||||
|
return new OAuth2AuthorizationCodeAuthenticationToken(code, clientPrincipal, redirectUri, additionalParameters);
|
||||||
return clientId != null ?
|
|
||||||
new OAuth2AuthorizationCodeAuthenticationToken(code, clientId, redirectUri, additionalParameters) :
|
|
||||||
new OAuth2AuthorizationCodeAuthenticationToken(code, clientPrincipal, redirectUri, additionalParameters);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* 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.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.core.endpoint.PkceParameterNames;
|
||||||
|
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.HashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to extract the parameters from {@link HttpServletRequest}
|
||||||
|
* used for authenticating public clients using Proof Key for Code Exchange (PKCE).
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 0.0.2
|
||||||
|
* @see AuthenticationConverter
|
||||||
|
* @see OAuth2ClientAuthenticationToken
|
||||||
|
* @see OAuth2ClientAuthenticationFilter
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636">Proof Key for Code Exchange by OAuth Public Clients</a>
|
||||||
|
*/
|
||||||
|
public class PublicClientAuthenticationConverter implements AuthenticationConverter {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Authentication convert(HttpServletRequest request) {
|
||||||
|
if (!OAuth2EndpointUtils.matchesPkceTokenRequest(request)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
|
||||||
|
|
||||||
|
// client_id (REQUIRED for public clients)
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
// code_verifier (REQUIRED)
|
||||||
|
if (parameters.get(PkceParameterNames.CODE_VERIFIER).size() != 1) {
|
||||||
|
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST));
|
||||||
|
}
|
||||||
|
|
||||||
|
parameters.remove(OAuth2ParameterNames.CLIENT_ID);
|
||||||
|
|
||||||
|
return new OAuth2ClientAuthenticationToken(
|
||||||
|
clientId, new HashMap<>(parameters.toSingleValueMap()));
|
||||||
|
}
|
||||||
|
}
|
@ -207,13 +207,12 @@ public class OAuth2AuthorizationCodeGrantTests {
|
|||||||
this.mvc.perform(post(OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI)
|
this.mvc.perform(post(OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI)
|
||||||
.params(getTokenRequestParameters(registeredClient, authorization))
|
.params(getTokenRequestParameters(registeredClient, authorization))
|
||||||
.param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
|
.param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
|
||||||
.param(PkceParameterNames.CODE_VERIFIER, S256_CODE_VERIFIER)
|
.param(PkceParameterNames.CODE_VERIFIER, S256_CODE_VERIFIER))
|
||||||
.with(user("user"))) // TODO Remove after PKCE authentication is moved to OAuth2ClientAuthenticationProvider
|
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(jsonPath("$.access_token").isNotEmpty());
|
.andExpect(jsonPath("$.access_token").isNotEmpty());
|
||||||
|
|
||||||
verify(registeredClientRepository, times(2)).findByClientId(eq(registeredClient.getClientId()));
|
verify(registeredClientRepository, times(2)).findByClientId(eq(registeredClient.getClientId()));
|
||||||
verify(authorizationService).findByToken(
|
verify(authorizationService, times(2)).findByToken(
|
||||||
eq(authorization.getAttribute(OAuth2AuthorizationAttributeNames.CODE)),
|
eq(authorization.getAttribute(OAuth2AuthorizationAttributeNames.CODE)),
|
||||||
eq(TokenType.AUTHORIZATION_CODE));
|
eq(TokenType.AUTHORIZATION_CODE));
|
||||||
verify(authorizationService, times(2)).save(any());
|
verify(authorizationService, times(2)).save(any());
|
||||||
|
@ -22,7 +22,6 @@ import org.springframework.security.authentication.TestingAuthenticationToken;
|
|||||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||||
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
|
||||||
import org.springframework.security.oauth2.jose.JoseHeaderNames;
|
import org.springframework.security.oauth2.jose.JoseHeaderNames;
|
||||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||||
import org.springframework.security.oauth2.jwt.Jwt;
|
import org.springframework.security.oauth2.jwt.Jwt;
|
||||||
@ -36,13 +35,9 @@ import org.springframework.security.oauth2.server.authorization.client.InMemoryR
|
|||||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
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.RegisteredClientRepository;
|
||||||
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
|
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
|
||||||
import org.springframework.security.oauth2.server.authorization.config.ClientSettings;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
@ -59,19 +54,8 @@ import static org.mockito.Mockito.when;
|
|||||||
* @author Daniel Garnier-Moiroux
|
* @author Daniel Garnier-Moiroux
|
||||||
*/
|
*/
|
||||||
public class OAuth2AuthorizationCodeAuthenticationProviderTests {
|
public class OAuth2AuthorizationCodeAuthenticationProviderTests {
|
||||||
private static final String PLAIN_CODE_VERIFIER = "pkce-key";
|
|
||||||
private static final String PLAIN_CODE_CHALLENGE = PLAIN_CODE_VERIFIER;
|
|
||||||
|
|
||||||
// See RFC 7636: Appendix B. Example for the S256 code_challenge_method
|
|
||||||
// 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 AUTHORIZATION_CODE = "code";
|
private static final String AUTHORIZATION_CODE = "code";
|
||||||
|
|
||||||
private RegisteredClient registeredClient;
|
private RegisteredClient registeredClient;
|
||||||
private RegisteredClient otherRegisteredClient;
|
|
||||||
private RegisteredClient registeredClientRequiresProofKey;
|
|
||||||
private RegisteredClientRepository registeredClientRepository;
|
private RegisteredClientRepository registeredClientRepository;
|
||||||
private OAuth2AuthorizationService authorizationService;
|
private OAuth2AuthorizationService authorizationService;
|
||||||
private JwtEncoder jwtEncoder;
|
private JwtEncoder jwtEncoder;
|
||||||
@ -80,17 +64,7 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
|
|||||||
@Before
|
@Before
|
||||||
public void setUp() {
|
public void setUp() {
|
||||||
this.registeredClient = TestRegisteredClients.registeredClient().build();
|
this.registeredClient = TestRegisteredClients.registeredClient().build();
|
||||||
this.otherRegisteredClient = TestRegisteredClients.registeredClient2().build();
|
this.registeredClientRepository = new InMemoryRegisteredClientRepository(this.registeredClient);
|
||||||
this.registeredClientRequiresProofKey = TestRegisteredClients.registeredClient()
|
|
||||||
.id("registration-3")
|
|
||||||
.clientId("client-3")
|
|
||||||
.clientSettings(new ClientSettings().requireProofKey(true))
|
|
||||||
.build();
|
|
||||||
this.registeredClientRepository = new InMemoryRegisteredClientRepository(
|
|
||||||
this.registeredClient,
|
|
||||||
this.otherRegisteredClient,
|
|
||||||
this.registeredClientRequiresProofKey
|
|
||||||
);
|
|
||||||
this.authorizationService = mock(OAuth2AuthorizationService.class);
|
this.authorizationService = mock(OAuth2AuthorizationService.class);
|
||||||
this.jwtEncoder = mock(JwtEncoder.class);
|
this.jwtEncoder = mock(JwtEncoder.class);
|
||||||
this.authenticationProvider = new OAuth2AuthorizationCodeAuthenticationProvider(
|
this.authenticationProvider = new OAuth2AuthorizationCodeAuthenticationProvider(
|
||||||
@ -139,7 +113,7 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
|
|||||||
@Test
|
@Test
|
||||||
public void authenticateWhenClientPrincipalNotAuthenticatedThenThrowOAuth2AuthenticationException() {
|
public void authenticateWhenClientPrincipalNotAuthenticatedThenThrowOAuth2AuthenticationException() {
|
||||||
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(
|
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(
|
||||||
this.registeredClient.getClientId(), this.registeredClient.getClientSecret());
|
this.registeredClient.getClientId(), this.registeredClient.getClientSecret(), null);
|
||||||
OAuth2AuthorizationCodeAuthenticationToken authentication =
|
OAuth2AuthorizationCodeAuthenticationToken authentication =
|
||||||
new OAuth2AuthorizationCodeAuthenticationToken(AUTHORIZATION_CODE, clientPrincipal, null, null);
|
new OAuth2AuthorizationCodeAuthenticationToken(AUTHORIZATION_CODE, clientPrincipal, null, null);
|
||||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||||
@ -149,31 +123,6 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
|
|||||||
.isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
|
.isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void authenticateWhenPublicClientAndInvalidClientIdThenThrowOAuth2AuthenticationException() {
|
|
||||||
OAuth2Authorization authorization = TestOAuth2Authorizations
|
|
||||||
.authorization(this.registeredClient, createPkceParametersPlain())
|
|
||||||
.build();
|
|
||||||
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
|
|
||||||
.thenReturn(authorization);
|
|
||||||
|
|
||||||
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
|
|
||||||
OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST);
|
|
||||||
OAuth2AuthorizationCodeAuthenticationToken authentication =
|
|
||||||
new OAuth2AuthorizationCodeAuthenticationToken(
|
|
||||||
AUTHORIZATION_CODE,
|
|
||||||
"invalid-client-id",
|
|
||||||
authorizationRequest.getRedirectUri(),
|
|
||||||
Collections.singletonMap(PkceParameterNames.CODE_VERIFIER, PLAIN_CODE_CHALLENGE)
|
|
||||||
);
|
|
||||||
|
|
||||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
|
||||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
|
||||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
|
||||||
.extracting("errorCode")
|
|
||||||
.isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authenticateWhenInvalidCodeThenThrowOAuth2AuthenticationException() {
|
public void authenticateWhenInvalidCodeThenThrowOAuth2AuthenticationException() {
|
||||||
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(this.registeredClient);
|
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(this.registeredClient);
|
||||||
@ -203,30 +152,6 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
|
|||||||
.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
|
.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void authenticateWhenPublicClientAndClientIdNotMatchThenThrowOAuth2AuthenticationException() {
|
|
||||||
OAuth2Authorization authorization = TestOAuth2Authorizations
|
|
||||||
.authorization(this.registeredClient, createPkceParametersPlain())
|
|
||||||
.build();
|
|
||||||
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
|
|
||||||
.thenReturn(authorization);
|
|
||||||
|
|
||||||
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
|
|
||||||
OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST);
|
|
||||||
OAuth2AuthorizationCodeAuthenticationToken authentication =
|
|
||||||
new OAuth2AuthorizationCodeAuthenticationToken(
|
|
||||||
AUTHORIZATION_CODE,
|
|
||||||
this.otherRegisteredClient.getClientId(),
|
|
||||||
authorizationRequest.getRedirectUri(),
|
|
||||||
Collections.singletonMap(PkceParameterNames.CODE_VERIFIER, PLAIN_CODE_VERIFIER)
|
|
||||||
);
|
|
||||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
|
||||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
|
||||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
|
||||||
.extracting("errorCode")
|
|
||||||
.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authenticateWhenInvalidRedirectUriThenThrowOAuth2AuthenticationException() {
|
public void authenticateWhenInvalidRedirectUriThenThrowOAuth2AuthenticationException() {
|
||||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization().build();
|
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization().build();
|
||||||
@ -272,240 +197,6 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
|
|||||||
assertThat(accessTokenAuthentication.getAccessToken()).isEqualTo(updatedAuthorization.getAccessToken());
|
assertThat(accessTokenAuthentication.getAccessToken()).isEqualTo(updatedAuthorization.getAccessToken());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void authenticateWhenRequireProofKeyAndMissingCodeChallengeThenThrowOAuth2AuthenticationException() {
|
|
||||||
OAuth2Authorization authorization = TestOAuth2Authorizations
|
|
||||||
.authorization(this.registeredClientRequiresProofKey)
|
|
||||||
.build();
|
|
||||||
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
|
|
||||||
.thenReturn(authorization);
|
|
||||||
|
|
||||||
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
|
|
||||||
OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST);
|
|
||||||
OAuth2AuthorizationCodeAuthenticationToken authentication =
|
|
||||||
new OAuth2AuthorizationCodeAuthenticationToken(
|
|
||||||
AUTHORIZATION_CODE,
|
|
||||||
this.registeredClientRequiresProofKey.getClientId(),
|
|
||||||
authorizationRequest.getRedirectUri(),
|
|
||||||
Collections.singletonMap(PkceParameterNames.CODE_VERIFIER, PLAIN_CODE_VERIFIER)
|
|
||||||
);
|
|
||||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
|
||||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
|
||||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
|
||||||
.extracting("errorCode")
|
|
||||||
.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void authenticateWhenPublicClientAndMissingCodeVerifierThenThrowOAuth2AuthenticationException() {
|
|
||||||
OAuth2Authorization authorization = TestOAuth2Authorizations
|
|
||||||
.authorization(this.registeredClient, createPkceParametersPlain())
|
|
||||||
.build();
|
|
||||||
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
|
|
||||||
.thenReturn(authorization);
|
|
||||||
|
|
||||||
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
|
|
||||||
OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST);
|
|
||||||
OAuth2AuthorizationCodeAuthenticationToken authentication =
|
|
||||||
new OAuth2AuthorizationCodeAuthenticationToken(
|
|
||||||
AUTHORIZATION_CODE,
|
|
||||||
authorizationRequest.getClientId(),
|
|
||||||
authorizationRequest.getRedirectUri(),
|
|
||||||
null
|
|
||||||
);
|
|
||||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
|
||||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
|
||||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
|
||||||
.extracting("errorCode")
|
|
||||||
.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void authenticateWhenConfidentialClientRequireProofKeyAndMissingCodeVerifierThenThrowOAuth2AuthenticationException() {
|
|
||||||
OAuth2Authorization authorization = TestOAuth2Authorizations
|
|
||||||
.authorization(this.registeredClientRequiresProofKey, createPkceParametersPlain())
|
|
||||||
.build();
|
|
||||||
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
|
|
||||||
.thenReturn(authorization);
|
|
||||||
|
|
||||||
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(
|
|
||||||
this.registeredClientRequiresProofKey);
|
|
||||||
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
|
|
||||||
OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST);
|
|
||||||
OAuth2AuthorizationCodeAuthenticationToken authentication =
|
|
||||||
new OAuth2AuthorizationCodeAuthenticationToken(
|
|
||||||
AUTHORIZATION_CODE,
|
|
||||||
clientPrincipal,
|
|
||||||
authorizationRequest.getRedirectUri(),
|
|
||||||
null
|
|
||||||
);
|
|
||||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
|
||||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
|
||||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
|
||||||
.extracting("errorCode")
|
|
||||||
.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void authenticateWhenPublicClientAndPlainMethodAndInvalidCodeVerifierThenThrowOAuth2AuthenticationException() {
|
|
||||||
OAuth2Authorization authorization = TestOAuth2Authorizations
|
|
||||||
.authorization(this.registeredClient, createPkceParametersPlain())
|
|
||||||
.build();
|
|
||||||
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
|
|
||||||
.thenReturn(authorization);
|
|
||||||
|
|
||||||
OAuth2AuthorizationCodeAuthenticationToken authentication = createAuthorizationCodeAuthentication(
|
|
||||||
this.registeredClient, "invalid-code-verifier");
|
|
||||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
|
||||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
|
||||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
|
||||||
.extracting("errorCode")
|
|
||||||
.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void authenticateWhenPublicClientAndS256MethodAndInvalidCodeVerifierThenThrowOAuth2AuthenticationException() {
|
|
||||||
OAuth2Authorization authorization = TestOAuth2Authorizations
|
|
||||||
.authorization(this.registeredClient, createPkceParametersS256())
|
|
||||||
.build();
|
|
||||||
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
|
|
||||||
.thenReturn(authorization);
|
|
||||||
|
|
||||||
OAuth2AuthorizationCodeAuthenticationToken authentication = createAuthorizationCodeAuthentication(
|
|
||||||
this.registeredClient, "invalid-code-verifier");
|
|
||||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
|
||||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
|
||||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
|
||||||
.extracting("errorCode")
|
|
||||||
.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void authenticateWhenRequireProofKeyAndUnsupportedCodeChallengeMethodThenThrowOAuth2AuthenticationException() {
|
|
||||||
Map<String, Object> pkceParameters = createPkceParametersPlain();
|
|
||||||
// This should never happen: the Authorization endpoint should not allow it
|
|
||||||
pkceParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "unsupported-challenge-method");
|
|
||||||
OAuth2Authorization authorization = TestOAuth2Authorizations
|
|
||||||
.authorization(this.registeredClientRequiresProofKey, pkceParameters)
|
|
||||||
.build();
|
|
||||||
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
|
|
||||||
.thenReturn(authorization);
|
|
||||||
|
|
||||||
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
|
|
||||||
OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST);
|
|
||||||
OAuth2AuthorizationCodeAuthenticationToken authentication =
|
|
||||||
new OAuth2AuthorizationCodeAuthenticationToken(
|
|
||||||
AUTHORIZATION_CODE,
|
|
||||||
this.registeredClientRequiresProofKey.getClientId(),
|
|
||||||
authorizationRequest.getRedirectUri(),
|
|
||||||
Collections.singletonMap(PkceParameterNames.CODE_VERIFIER, PLAIN_CODE_VERIFIER)
|
|
||||||
);
|
|
||||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
|
||||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
|
||||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
|
||||||
.extracting("errorCode")
|
|
||||||
.isEqualTo(OAuth2ErrorCodes.SERVER_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void authenticateWhenPublicClientAndPlainMethodAndValidCodeVerifierThenReturnAccessToken() {
|
|
||||||
OAuth2Authorization authorization = TestOAuth2Authorizations
|
|
||||||
.authorization(this.registeredClient, createPkceParametersPlain())
|
|
||||||
.build();
|
|
||||||
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
|
|
||||||
.thenReturn(authorization);
|
|
||||||
when(this.jwtEncoder.encode(any(), any())).thenReturn(createJwt());
|
|
||||||
|
|
||||||
OAuth2AuthorizationCodeAuthenticationToken authentication = createAuthorizationCodeAuthentication(
|
|
||||||
this.registeredClient, PLAIN_CODE_VERIFIER);
|
|
||||||
|
|
||||||
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
|
|
||||||
(OAuth2AccessTokenAuthenticationToken) this.authenticationProvider.authenticate(authentication);
|
|
||||||
|
|
||||||
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
|
|
||||||
verify(this.authorizationService).save(authorizationCaptor.capture());
|
|
||||||
OAuth2Authorization updatedAuthorization = authorizationCaptor.getValue();
|
|
||||||
|
|
||||||
OAuth2ClientAuthenticationToken clientAuthentication = (OAuth2ClientAuthenticationToken) accessTokenAuthentication.getPrincipal();
|
|
||||||
assertThat(clientAuthentication.getPrincipal()).isEqualTo(this.registeredClient.getClientId());
|
|
||||||
assertThat(updatedAuthorization.getAccessToken()).isNotNull();
|
|
||||||
assertThat(accessTokenAuthentication.getAccessToken()).isEqualTo(updatedAuthorization.getAccessToken());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void authenticateWhenPublicClientAndMissingMethodThenDefaultPlainMethodAndReturnAccessToken() {
|
|
||||||
OAuth2Authorization authorization = TestOAuth2Authorizations
|
|
||||||
.authorization(this.registeredClient, Collections.singletonMap(PkceParameterNames.CODE_CHALLENGE, PLAIN_CODE_CHALLENGE))
|
|
||||||
.build();
|
|
||||||
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
|
|
||||||
.thenReturn(authorization);
|
|
||||||
when(this.jwtEncoder.encode(any(), any())).thenReturn(createJwt());
|
|
||||||
|
|
||||||
OAuth2AuthorizationCodeAuthenticationToken authentication = createAuthorizationCodeAuthentication(
|
|
||||||
this.registeredClient, PLAIN_CODE_VERIFIER);
|
|
||||||
|
|
||||||
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
|
|
||||||
(OAuth2AccessTokenAuthenticationToken) this.authenticationProvider.authenticate(authentication);
|
|
||||||
|
|
||||||
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
|
|
||||||
verify(this.authorizationService).save(authorizationCaptor.capture());
|
|
||||||
OAuth2Authorization updatedAuthorization = authorizationCaptor.getValue();
|
|
||||||
|
|
||||||
OAuth2ClientAuthenticationToken clientAuthentication = (OAuth2ClientAuthenticationToken) accessTokenAuthentication.getPrincipal();
|
|
||||||
assertThat(clientAuthentication.getPrincipal()).isEqualTo(this.registeredClient.getClientId());
|
|
||||||
assertThat(updatedAuthorization.getAccessToken()).isNotNull();
|
|
||||||
assertThat(accessTokenAuthentication.getAccessToken()).isEqualTo(updatedAuthorization.getAccessToken());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void authenticateWhenPublicClientAndS256MethodAndValidCodeVerifierThenReturnAccessToken() {
|
|
||||||
OAuth2Authorization authorization = TestOAuth2Authorizations
|
|
||||||
.authorization(this.registeredClient, createPkceParametersS256())
|
|
||||||
.build();
|
|
||||||
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
|
|
||||||
.thenReturn(authorization);
|
|
||||||
when(this.jwtEncoder.encode(any(), any())).thenReturn(createJwt());
|
|
||||||
|
|
||||||
OAuth2AuthorizationCodeAuthenticationToken authentication = createAuthorizationCodeAuthentication(
|
|
||||||
this.registeredClient, S256_CODE_VERIFIER);
|
|
||||||
|
|
||||||
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
|
|
||||||
(OAuth2AccessTokenAuthenticationToken) this.authenticationProvider.authenticate(authentication);
|
|
||||||
|
|
||||||
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
|
|
||||||
verify(this.authorizationService).save(authorizationCaptor.capture());
|
|
||||||
OAuth2Authorization updatedAuthorization = authorizationCaptor.getValue();
|
|
||||||
|
|
||||||
OAuth2ClientAuthenticationToken clientAuthentication = (OAuth2ClientAuthenticationToken) accessTokenAuthentication.getPrincipal();
|
|
||||||
assertThat(clientAuthentication.getPrincipal()).isEqualTo(this.registeredClient.getClientId());
|
|
||||||
assertThat(updatedAuthorization.getAccessToken()).isNotNull();
|
|
||||||
assertThat(accessTokenAuthentication.getAccessToken()).isEqualTo(updatedAuthorization.getAccessToken());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map<String, Object> createPkceParametersPlain() {
|
|
||||||
Map<String, Object> parameters = new HashMap<>();
|
|
||||||
parameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "plain");
|
|
||||||
parameters.put(PkceParameterNames.CODE_CHALLENGE, PLAIN_CODE_CHALLENGE);
|
|
||||||
return parameters;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map<String, Object> createPkceParametersS256() {
|
|
||||||
Map<String, Object> parameters = new HashMap<>();
|
|
||||||
parameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
|
|
||||||
parameters.put(PkceParameterNames.CODE_CHALLENGE, S256_CODE_CHALLENGE);
|
|
||||||
return parameters;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static OAuth2AuthorizationCodeAuthenticationToken createAuthorizationCodeAuthentication(
|
|
||||||
RegisteredClient registeredClient, String codeVerifier) {
|
|
||||||
return new OAuth2AuthorizationCodeAuthenticationToken(
|
|
||||||
AUTHORIZATION_CODE,
|
|
||||||
registeredClient.getClientId(),
|
|
||||||
registeredClient.getRedirectUris().iterator().next(),
|
|
||||||
Collections.singletonMap(PkceParameterNames.CODE_VERIFIER, codeVerifier)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Jwt createJwt() {
|
private static Jwt createJwt() {
|
||||||
Instant issuedAt = Instant.now();
|
Instant issuedAt = Instant.now();
|
||||||
Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS);
|
Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS);
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
|
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@ -35,7 +34,6 @@ public class OAuth2AuthorizationCodeAuthenticationTokenTests {
|
|||||||
private String code = "code";
|
private String code = "code";
|
||||||
private OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(
|
private OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(
|
||||||
TestRegisteredClients.registeredClient().build());
|
TestRegisteredClients.registeredClient().build());
|
||||||
private String clientId = "clientId";
|
|
||||||
private String redirectUri = "redirectUri";
|
private String redirectUri = "redirectUri";
|
||||||
private Map<String, Object> additionalParameters = Collections.singletonMap("param1", "value1");
|
private Map<String, Object> additionalParameters = Collections.singletonMap("param1", "value1");
|
||||||
|
|
||||||
@ -48,18 +46,11 @@ public class OAuth2AuthorizationCodeAuthenticationTokenTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void constructorWhenClientPrincipalNullThenThrowIllegalArgumentException() {
|
public void constructorWhenClientPrincipalNullThenThrowIllegalArgumentException() {
|
||||||
assertThatThrownBy(() -> new OAuth2AuthorizationCodeAuthenticationToken(this.code, (Authentication) null, this.redirectUri, null))
|
assertThatThrownBy(() -> new OAuth2AuthorizationCodeAuthenticationToken(this.code, null, this.redirectUri, null))
|
||||||
.isInstanceOf(IllegalArgumentException.class)
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessage("clientPrincipal cannot be null");
|
.hasMessage("clientPrincipal cannot be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void constructorWhenClientIdNullThenThrowIllegalArgumentException() {
|
|
||||||
assertThatThrownBy(() -> new OAuth2AuthorizationCodeAuthenticationToken(this.code, (String) null, this.redirectUri, null))
|
|
||||||
.isInstanceOf(IllegalArgumentException.class)
|
|
||||||
.hasMessage("clientId cannot be empty");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void constructorWhenClientPrincipalProvidedThenCreated() {
|
public void constructorWhenClientPrincipalProvidedThenCreated() {
|
||||||
OAuth2AuthorizationCodeAuthenticationToken authentication = new OAuth2AuthorizationCodeAuthenticationToken(
|
OAuth2AuthorizationCodeAuthenticationToken authentication = new OAuth2AuthorizationCodeAuthenticationToken(
|
||||||
@ -71,21 +62,10 @@ public class OAuth2AuthorizationCodeAuthenticationTokenTests {
|
|||||||
assertThat(authentication.getAdditionalParameters()).isEqualTo(this.additionalParameters);
|
assertThat(authentication.getAdditionalParameters()).isEqualTo(this.additionalParameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void constructorWhenClientIdProvidedThenCreated() {
|
|
||||||
OAuth2AuthorizationCodeAuthenticationToken authentication = new OAuth2AuthorizationCodeAuthenticationToken(
|
|
||||||
this.code, this.clientId, this.redirectUri, this.additionalParameters);
|
|
||||||
assertThat(authentication.getPrincipal()).isEqualTo(this.clientId);
|
|
||||||
assertThat(authentication.getCredentials().toString()).isEmpty();
|
|
||||||
assertThat(authentication.getCode()).isEqualTo(this.code);
|
|
||||||
assertThat(authentication.getRedirectUri()).isEqualTo(this.redirectUri);
|
|
||||||
assertThat(authentication.getAdditionalParameters()).isEqualTo(this.additionalParameters);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getAdditionalParametersWhenUpdateThenThrowUnsupportedOperationException() {
|
public void getAdditionalParametersWhenUpdateThenThrowUnsupportedOperationException() {
|
||||||
OAuth2AuthorizationCodeAuthenticationToken authentication = new OAuth2AuthorizationCodeAuthenticationToken(
|
OAuth2AuthorizationCodeAuthenticationToken authentication = new OAuth2AuthorizationCodeAuthenticationToken(
|
||||||
this.code, this.clientId, this.redirectUri, this.additionalParameters);
|
this.code, this.clientPrincipal, this.redirectUri, this.additionalParameters);
|
||||||
assertThatThrownBy(() -> authentication.getAdditionalParameters().put("another_key", 1))
|
assertThatThrownBy(() -> authentication.getAdditionalParameters().put("another_key", 1))
|
||||||
.isInstanceOf(UnsupportedOperationException.class);
|
.isInstanceOf(UnsupportedOperationException.class);
|
||||||
assertThatThrownBy(() -> authentication.getAdditionalParameters().remove("some_key"))
|
assertThatThrownBy(() -> authentication.getAdditionalParameters().remove("some_key"))
|
||||||
@ -93,12 +73,4 @@ public class OAuth2AuthorizationCodeAuthenticationTokenTests {
|
|||||||
assertThatThrownBy(() -> authentication.getAdditionalParameters().clear())
|
assertThatThrownBy(() -> authentication.getAdditionalParameters().clear())
|
||||||
.isInstanceOf(UnsupportedOperationException.class);
|
.isInstanceOf(UnsupportedOperationException.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void getClientIdWhenClientPrincipalProvidedThenNotNull() {
|
|
||||||
OAuth2AuthorizationCodeAuthenticationToken authentication = new OAuth2AuthorizationCodeAuthenticationToken(
|
|
||||||
this.code, this.clientPrincipal, this.redirectUri, this.additionalParameters);
|
|
||||||
assertThat(authentication.getClientId()).isNotNull();
|
|
||||||
assertThat(authentication.getClientId()).isEqualTo(this.clientPrincipal.getRegisteredClient().getClientId());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -17,41 +17,74 @@ package org.springframework.security.oauth2.server.authorization.authentication;
|
|||||||
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||||
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
|
||||||
|
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.RegisteredClient;
|
||||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
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.client.TestRegisteredClients;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.config.ClientSettings;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link OAuth2ClientAuthenticationProvider}.
|
* Tests for {@link OAuth2ClientAuthenticationProvider}.
|
||||||
*
|
*
|
||||||
* @author Patryk Kostrzewa
|
* @author Patryk Kostrzewa
|
||||||
* @author Joe Grandja
|
* @author Joe Grandja
|
||||||
|
* @author Daniel Garnier-Moiroux
|
||||||
*/
|
*/
|
||||||
public class OAuth2ClientAuthenticationProviderTests {
|
public class OAuth2ClientAuthenticationProviderTests {
|
||||||
private RegisteredClient registeredClient;
|
private static final String PLAIN_CODE_VERIFIER = "pkce-key";
|
||||||
|
private static final String PLAIN_CODE_CHALLENGE = PLAIN_CODE_VERIFIER;
|
||||||
|
|
||||||
|
// See RFC 7636: Appendix B. Example for the S256 code_challenge_method
|
||||||
|
// 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 AUTHORIZATION_CODE = "code";
|
||||||
|
|
||||||
private RegisteredClientRepository registeredClientRepository;
|
private RegisteredClientRepository registeredClientRepository;
|
||||||
|
private OAuth2AuthorizationService authorizationService;
|
||||||
private OAuth2ClientAuthenticationProvider authenticationProvider;
|
private OAuth2ClientAuthenticationProvider authenticationProvider;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() {
|
public void setUp() {
|
||||||
this.registeredClient = TestRegisteredClients.registeredClient().build();
|
this.registeredClientRepository = mock(RegisteredClientRepository.class);
|
||||||
this.registeredClientRepository = new InMemoryRegisteredClientRepository(this.registeredClient);
|
this.authorizationService = mock(OAuth2AuthorizationService.class);
|
||||||
this.authenticationProvider = new OAuth2ClientAuthenticationProvider(this.registeredClientRepository);
|
this.authenticationProvider = new OAuth2ClientAuthenticationProvider(
|
||||||
|
this.registeredClientRepository, this.authorizationService);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void constructorWhenRegisteredClientRepositoryNullThenThrowIllegalArgumentException() {
|
public void constructorWhenRegisteredClientRepositoryNullThenThrowIllegalArgumentException() {
|
||||||
assertThatThrownBy(() -> new OAuth2ClientAuthenticationProvider(null))
|
assertThatThrownBy(() -> new OAuth2ClientAuthenticationProvider(null, this.authorizationService))
|
||||||
.isInstanceOf(IllegalArgumentException.class)
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessage("registeredClientRepository cannot be null");
|
.hasMessage("registeredClientRepository cannot be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void constructorWhenAuthorizationServiceNullThenThrowIllegalArgumentException() {
|
||||||
|
assertThatThrownBy(() -> new OAuth2ClientAuthenticationProvider(this.registeredClientRepository, null))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("authorizationService cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void supportsWhenTypeOAuth2ClientAuthenticationTokenThenReturnTrue() {
|
public void supportsWhenTypeOAuth2ClientAuthenticationTokenThenReturnTrue() {
|
||||||
assertThat(this.authenticationProvider.supports(OAuth2ClientAuthenticationToken.class)).isTrue();
|
assertThat(this.authenticationProvider.supports(OAuth2ClientAuthenticationToken.class)).isTrue();
|
||||||
@ -59,8 +92,12 @@ public class OAuth2ClientAuthenticationProviderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authenticateWhenInvalidClientIdThenThrowOAuth2AuthenticationException() {
|
public void authenticateWhenInvalidClientIdThenThrowOAuth2AuthenticationException() {
|
||||||
|
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||||
|
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||||
|
.thenReturn(registeredClient);
|
||||||
|
|
||||||
OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
|
OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
|
||||||
this.registeredClient.getClientId() + "-invalid", this.registeredClient.getClientSecret());
|
registeredClient.getClientId() + "-invalid", registeredClient.getClientSecret(), null);
|
||||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||||
@ -70,8 +107,12 @@ public class OAuth2ClientAuthenticationProviderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authenticateWhenInvalidClientSecretThenThrowOAuth2AuthenticationException() {
|
public void authenticateWhenInvalidClientSecretThenThrowOAuth2AuthenticationException() {
|
||||||
|
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||||
|
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||||
|
.thenReturn(registeredClient);
|
||||||
|
|
||||||
OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
|
OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
|
||||||
this.registeredClient.getClientId(), this.registeredClient.getClientSecret() + "-invalid");
|
registeredClient.getClientId(), registeredClient.getClientSecret() + "-invalid", null);
|
||||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||||
@ -81,13 +122,283 @@ public class OAuth2ClientAuthenticationProviderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authenticateWhenValidCredentialsThenAuthenticated() {
|
public void authenticateWhenValidCredentialsThenAuthenticated() {
|
||||||
|
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||||
|
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||||
|
.thenReturn(registeredClient);
|
||||||
|
|
||||||
OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
|
OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
|
||||||
this.registeredClient.getClientId(), this.registeredClient.getClientSecret());
|
registeredClient.getClientId(), registeredClient.getClientSecret(), null);
|
||||||
OAuth2ClientAuthenticationToken authenticationResult =
|
OAuth2ClientAuthenticationToken authenticationResult =
|
||||||
(OAuth2ClientAuthenticationToken) this.authenticationProvider.authenticate(authentication);
|
(OAuth2ClientAuthenticationToken) this.authenticationProvider.authenticate(authentication);
|
||||||
assertThat(authenticationResult.isAuthenticated()).isTrue();
|
assertThat(authenticationResult.isAuthenticated()).isTrue();
|
||||||
assertThat(authenticationResult.getPrincipal().toString()).isEqualTo(this.registeredClient.getClientId());
|
assertThat(authenticationResult.getPrincipal().toString()).isEqualTo(registeredClient.getClientId());
|
||||||
assertThat(authenticationResult.getCredentials()).isNull();
|
assertThat(authenticationResult.getCredentials()).isNull();
|
||||||
assertThat(authenticationResult.getRegisteredClient()).isEqualTo(this.registeredClient);
|
assertThat(authenticationResult.getRegisteredClient()).isEqualTo(registeredClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authenticateWhenNotPkceThenContinueAuthenticated() {
|
||||||
|
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||||
|
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||||
|
.thenReturn(registeredClient);
|
||||||
|
|
||||||
|
OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
|
||||||
|
registeredClient.getClientId(), registeredClient.getClientSecret(), null);
|
||||||
|
OAuth2ClientAuthenticationToken authenticationResult =
|
||||||
|
(OAuth2ClientAuthenticationToken) this.authenticationProvider.authenticate(authentication);
|
||||||
|
assertThat(authenticationResult.isAuthenticated()).isTrue();
|
||||||
|
|
||||||
|
verifyNoInteractions(this.authorizationService);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authenticateWhenPkceAndInvalidCodeThenThrowOAuth2AuthenticationException() {
|
||||||
|
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||||
|
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||||
|
.thenReturn(registeredClient);
|
||||||
|
|
||||||
|
OAuth2Authorization authorization = TestOAuth2Authorizations
|
||||||
|
.authorization(registeredClient, createPkceAuthorizationParametersPlain())
|
||||||
|
.build();
|
||||||
|
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
|
||||||
|
.thenReturn(authorization);
|
||||||
|
|
||||||
|
Map<String, Object> parameters = createPkceTokenParameters(PLAIN_CODE_VERIFIER);
|
||||||
|
parameters.put(OAuth2ParameterNames.CODE, "invalid-code");
|
||||||
|
|
||||||
|
OAuth2ClientAuthenticationToken authentication =
|
||||||
|
new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), parameters);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||||
|
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||||
|
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||||
|
.extracting("errorCode")
|
||||||
|
.isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authenticateWhenPkceAndRequireProofKeyAndMissingCodeChallengeThenThrowOAuth2AuthenticationException() {
|
||||||
|
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||||
|
.clientSettings(
|
||||||
|
new ClientSettings().requireProofKey(true))
|
||||||
|
.build();
|
||||||
|
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||||
|
.thenReturn(registeredClient);
|
||||||
|
|
||||||
|
OAuth2Authorization authorization = TestOAuth2Authorizations
|
||||||
|
.authorization(registeredClient)
|
||||||
|
.build();
|
||||||
|
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
|
||||||
|
.thenReturn(authorization);
|
||||||
|
|
||||||
|
Map<String, Object> parameters = createPkceTokenParameters(PLAIN_CODE_VERIFIER);
|
||||||
|
|
||||||
|
OAuth2ClientAuthenticationToken authentication =
|
||||||
|
new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), parameters);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||||
|
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||||
|
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||||
|
.extracting("errorCode")
|
||||||
|
.isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authenticateWhenPkceAndMissingCodeVerifierThenThrowOAuth2AuthenticationException() {
|
||||||
|
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||||
|
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||||
|
.thenReturn(registeredClient);
|
||||||
|
|
||||||
|
OAuth2Authorization authorization = TestOAuth2Authorizations
|
||||||
|
.authorization(registeredClient, createPkceAuthorizationParametersPlain())
|
||||||
|
.build();
|
||||||
|
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
|
||||||
|
.thenReturn(authorization);
|
||||||
|
|
||||||
|
Map<String, Object> parameters = createPkceTokenParameters(PLAIN_CODE_VERIFIER);
|
||||||
|
parameters.remove(PkceParameterNames.CODE_VERIFIER);
|
||||||
|
|
||||||
|
OAuth2ClientAuthenticationToken authentication =
|
||||||
|
new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), parameters);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||||
|
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||||
|
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||||
|
.extracting("errorCode")
|
||||||
|
.isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authenticateWhenPkceAndPlainMethodAndInvalidCodeVerifierThenThrowOAuth2AuthenticationException() {
|
||||||
|
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||||
|
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||||
|
.thenReturn(registeredClient);
|
||||||
|
|
||||||
|
OAuth2Authorization authorization = TestOAuth2Authorizations
|
||||||
|
.authorization(registeredClient, createPkceAuthorizationParametersPlain())
|
||||||
|
.build();
|
||||||
|
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
|
||||||
|
.thenReturn(authorization);
|
||||||
|
|
||||||
|
Map<String, Object> parameters = createPkceTokenParameters("invalid-code-verifier");
|
||||||
|
|
||||||
|
OAuth2ClientAuthenticationToken authentication =
|
||||||
|
new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), parameters);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||||
|
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||||
|
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||||
|
.extracting("errorCode")
|
||||||
|
.isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authenticateWhenPkceAndS256MethodAndInvalidCodeVerifierThenThrowOAuth2AuthenticationException() {
|
||||||
|
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||||
|
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||||
|
.thenReturn(registeredClient);
|
||||||
|
|
||||||
|
OAuth2Authorization authorization = TestOAuth2Authorizations
|
||||||
|
.authorization(registeredClient, createPkceAuthorizationParametersS256())
|
||||||
|
.build();
|
||||||
|
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
|
||||||
|
.thenReturn(authorization);
|
||||||
|
|
||||||
|
Map<String, Object> parameters = createPkceTokenParameters("invalid-code-verifier");
|
||||||
|
|
||||||
|
OAuth2ClientAuthenticationToken authentication =
|
||||||
|
new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), parameters);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||||
|
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||||
|
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||||
|
.extracting("errorCode")
|
||||||
|
.isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authenticateWhenPkceAndPlainMethodAndValidCodeVerifierThenAuthenticated() {
|
||||||
|
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||||
|
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||||
|
.thenReturn(registeredClient);
|
||||||
|
|
||||||
|
OAuth2Authorization authorization = TestOAuth2Authorizations
|
||||||
|
.authorization(registeredClient, createPkceAuthorizationParametersPlain())
|
||||||
|
.build();
|
||||||
|
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
|
||||||
|
.thenReturn(authorization);
|
||||||
|
|
||||||
|
Map<String, Object> parameters = createPkceTokenParameters(PLAIN_CODE_VERIFIER);
|
||||||
|
|
||||||
|
OAuth2ClientAuthenticationToken authentication =
|
||||||
|
new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), parameters);
|
||||||
|
|
||||||
|
OAuth2ClientAuthenticationToken authenticationResult =
|
||||||
|
(OAuth2ClientAuthenticationToken) this.authenticationProvider.authenticate(authentication);
|
||||||
|
assertThat(authenticationResult.isAuthenticated()).isTrue();
|
||||||
|
assertThat(authenticationResult.getPrincipal().toString()).isEqualTo(registeredClient.getClientId());
|
||||||
|
assertThat(authenticationResult.getCredentials()).isNull();
|
||||||
|
assertThat(authenticationResult.getRegisteredClient()).isEqualTo(registeredClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authenticateWhenPkceAndMissingMethodThenDefaultPlainMethodAndAuthenticated() {
|
||||||
|
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||||
|
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||||
|
.thenReturn(registeredClient);
|
||||||
|
|
||||||
|
Map<String, Object> authorizationRequestAdditionalParameters = createPkceAuthorizationParametersPlain();
|
||||||
|
authorizationRequestAdditionalParameters.remove(PkceParameterNames.CODE_CHALLENGE_METHOD);
|
||||||
|
OAuth2Authorization authorization = TestOAuth2Authorizations
|
||||||
|
.authorization(registeredClient, authorizationRequestAdditionalParameters)
|
||||||
|
.build();
|
||||||
|
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
|
||||||
|
.thenReturn(authorization);
|
||||||
|
|
||||||
|
Map<String, Object> parameters = createPkceTokenParameters(PLAIN_CODE_VERIFIER);
|
||||||
|
|
||||||
|
OAuth2ClientAuthenticationToken authentication =
|
||||||
|
new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), parameters);
|
||||||
|
|
||||||
|
OAuth2ClientAuthenticationToken authenticationResult =
|
||||||
|
(OAuth2ClientAuthenticationToken) this.authenticationProvider.authenticate(authentication);
|
||||||
|
assertThat(authenticationResult.isAuthenticated()).isTrue();
|
||||||
|
assertThat(authenticationResult.getPrincipal().toString()).isEqualTo(registeredClient.getClientId());
|
||||||
|
assertThat(authenticationResult.getCredentials()).isNull();
|
||||||
|
assertThat(authenticationResult.getRegisteredClient()).isEqualTo(registeredClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authenticateWhenPkceAndS256MethodAndValidCodeVerifierThenAuthenticated() {
|
||||||
|
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||||
|
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||||
|
.thenReturn(registeredClient);
|
||||||
|
|
||||||
|
OAuth2Authorization authorization = TestOAuth2Authorizations
|
||||||
|
.authorization(registeredClient, createPkceAuthorizationParametersS256())
|
||||||
|
.build();
|
||||||
|
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
|
||||||
|
.thenReturn(authorization);
|
||||||
|
|
||||||
|
Map<String, Object> parameters = createPkceTokenParameters(S256_CODE_VERIFIER);
|
||||||
|
|
||||||
|
OAuth2ClientAuthenticationToken authentication =
|
||||||
|
new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), parameters);
|
||||||
|
|
||||||
|
OAuth2ClientAuthenticationToken authenticationResult =
|
||||||
|
(OAuth2ClientAuthenticationToken) this.authenticationProvider.authenticate(authentication);
|
||||||
|
assertThat(authenticationResult.isAuthenticated()).isTrue();
|
||||||
|
assertThat(authenticationResult.getPrincipal().toString()).isEqualTo(registeredClient.getClientId());
|
||||||
|
assertThat(authenticationResult.getCredentials()).isNull();
|
||||||
|
assertThat(authenticationResult.getRegisteredClient()).isEqualTo(registeredClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authenticateWhenPkceAndUnsupportedCodeChallengeMethodThenThrowOAuth2AuthenticationException() {
|
||||||
|
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||||
|
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||||
|
.thenReturn(registeredClient);
|
||||||
|
|
||||||
|
Map<String, Object> authorizationRequestAdditionalParameters = createPkceAuthorizationParametersPlain();
|
||||||
|
// This should never happen: the Authorization endpoint should not allow it
|
||||||
|
authorizationRequestAdditionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "unsupported-challenge-method");
|
||||||
|
OAuth2Authorization authorization = TestOAuth2Authorizations
|
||||||
|
.authorization(registeredClient, authorizationRequestAdditionalParameters)
|
||||||
|
.build();
|
||||||
|
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
|
||||||
|
.thenReturn(authorization);
|
||||||
|
|
||||||
|
Map<String, Object> parameters = createPkceTokenParameters(PLAIN_CODE_VERIFIER);
|
||||||
|
|
||||||
|
OAuth2ClientAuthenticationToken authentication =
|
||||||
|
new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), parameters);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||||
|
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||||
|
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||||
|
.extracting("errorCode")
|
||||||
|
.isEqualTo(OAuth2ErrorCodes.SERVER_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Object> createPkceTokenParameters(String codeVerifier) {
|
||||||
|
Map<String, Object> parameters = new HashMap<>();
|
||||||
|
parameters.put(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
|
||||||
|
parameters.put(OAuth2ParameterNames.CODE, AUTHORIZATION_CODE);
|
||||||
|
parameters.put(PkceParameterNames.CODE_VERIFIER, codeVerifier);
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Object> createPkceAuthorizationParametersPlain() {
|
||||||
|
Map<String, Object> parameters = new HashMap<>();
|
||||||
|
parameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "plain");
|
||||||
|
parameters.put(PkceParameterNames.CODE_CHALLENGE, PLAIN_CODE_CHALLENGE);
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Object> createPkceAuthorizationParametersS256() {
|
||||||
|
Map<String, Object> parameters = new HashMap<>();
|
||||||
|
parameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
|
||||||
|
parameters.put(PkceParameterNames.CODE_CHALLENGE, S256_CODE_CHALLENGE);
|
||||||
|
return parameters;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,9 @@ import org.junit.Test;
|
|||||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||||
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
|
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
@ -31,14 +34,14 @@ public class OAuth2ClientAuthenticationTokenTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void constructorWhenClientIdNullThenThrowIllegalArgumentException() {
|
public void constructorWhenClientIdNullThenThrowIllegalArgumentException() {
|
||||||
assertThatThrownBy(() -> new OAuth2ClientAuthenticationToken(null, "secret"))
|
assertThatThrownBy(() -> new OAuth2ClientAuthenticationToken(null, "secret", null))
|
||||||
.isInstanceOf(IllegalArgumentException.class)
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessage("clientId cannot be empty");
|
.hasMessage("clientId cannot be empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void constructorWhenClientSecretNullThenThrowIllegalArgumentException() {
|
public void constructorWhenClientSecretNullThenThrowIllegalArgumentException() {
|
||||||
assertThatThrownBy(() -> new OAuth2ClientAuthenticationToken("clientId", null))
|
assertThatThrownBy(() -> new OAuth2ClientAuthenticationToken("clientId", null, null))
|
||||||
.isInstanceOf(IllegalArgumentException.class)
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessage("clientSecret cannot be empty");
|
.hasMessage("clientSecret cannot be empty");
|
||||||
}
|
}
|
||||||
@ -52,13 +55,25 @@ public class OAuth2ClientAuthenticationTokenTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void constructorWhenClientCredentialsProvidedThenCreated() {
|
public void constructorWhenClientCredentialsProvidedThenCreated() {
|
||||||
OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken("clientId", "secret");
|
OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken("clientId", "secret", null);
|
||||||
assertThat(authentication.isAuthenticated()).isFalse();
|
assertThat(authentication.isAuthenticated()).isFalse();
|
||||||
assertThat(authentication.getPrincipal().toString()).isEqualTo("clientId");
|
assertThat(authentication.getPrincipal().toString()).isEqualTo("clientId");
|
||||||
assertThat(authentication.getCredentials()).isEqualTo("secret");
|
assertThat(authentication.getCredentials()).isEqualTo("secret");
|
||||||
assertThat(authentication.getRegisteredClient()).isNull();
|
assertThat(authentication.getRegisteredClient()).isNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void constructorWhenClientIdProvidedThenCreated() {
|
||||||
|
Map<String, Object> additionalParameters = new HashMap<>();
|
||||||
|
additionalParameters.put("param1", "value1");
|
||||||
|
OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken("clientId", additionalParameters);
|
||||||
|
assertThat(authentication.isAuthenticated()).isFalse();
|
||||||
|
assertThat(authentication.getPrincipal().toString()).isEqualTo("clientId");
|
||||||
|
assertThat(authentication.getCredentials()).isNull();
|
||||||
|
assertThat(authentication.getAdditionalParameters()).isEqualTo(additionalParameters);
|
||||||
|
assertThat(authentication.getRegisteredClient()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void constructorWhenRegisteredClientProvidedThenCreated() {
|
public void constructorWhenRegisteredClientProvidedThenCreated() {
|
||||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||||
|
@ -103,7 +103,7 @@ public class OAuth2ClientCredentialsAuthenticationProviderTests {
|
|||||||
@Test
|
@Test
|
||||||
public void authenticateWhenClientPrincipalNotAuthenticatedThenThrowOAuth2AuthenticationException() {
|
public void authenticateWhenClientPrincipalNotAuthenticatedThenThrowOAuth2AuthenticationException() {
|
||||||
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(
|
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(
|
||||||
this.registeredClient.getClientId(), this.registeredClient.getClientSecret());
|
this.registeredClient.getClientId(), this.registeredClient.getClientSecret(), null);
|
||||||
OAuth2ClientCredentialsAuthenticationToken authentication = new OAuth2ClientCredentialsAuthenticationToken(clientPrincipal);
|
OAuth2ClientCredentialsAuthenticationToken authentication = new OAuth2ClientCredentialsAuthenticationToken(clientPrincipal);
|
||||||
|
|
||||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||||
|
@ -19,8 +19,11 @@ import org.junit.Test;
|
|||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.mock.web.MockHttpServletRequest;
|
import org.springframework.mock.web.MockHttpServletRequest;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
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 org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
|
||||||
|
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
@ -29,6 +32,7 @@ import java.util.Base64;
|
|||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.assertj.core.api.Assertions.entry;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link ClientSecretBasicAuthenticationConverter}.
|
* Tests for {@link ClientSecretBasicAuthenticationConverter}.
|
||||||
@ -96,6 +100,20 @@ public class ClientSecretBasicAuthenticationConverterTests {
|
|||||||
assertThat(authentication.getCredentials()).isEqualTo("secret");
|
assertThat(authentication.getCredentials()).isEqualTo("secret");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void convertWhenConfidentialClientWithPkceParametersThenAdditionalParametersIncluded() throws Exception {
|
||||||
|
MockHttpServletRequest request = createPkceTokenRequest();
|
||||||
|
request.addHeader(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth("clientId", "secret"));
|
||||||
|
OAuth2ClientAuthenticationToken authentication = (OAuth2ClientAuthenticationToken) this.converter.convert(request);
|
||||||
|
assertThat(authentication.getPrincipal()).isEqualTo("clientId");
|
||||||
|
assertThat(authentication.getCredentials()).isEqualTo("secret");
|
||||||
|
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 String encodeBasicAuth(String clientId, String secret) throws Exception {
|
private static String encodeBasicAuth(String clientId, String secret) throws Exception {
|
||||||
clientId = URLEncoder.encode(clientId, StandardCharsets.UTF_8.name());
|
clientId = URLEncoder.encode(clientId, StandardCharsets.UTF_8.name());
|
||||||
secret = URLEncoder.encode(secret, StandardCharsets.UTF_8.name());
|
secret = URLEncoder.encode(secret, StandardCharsets.UTF_8.name());
|
||||||
@ -103,4 +121,12 @@ public class ClientSecretBasicAuthenticationConverterTests {
|
|||||||
byte[] encodedBytes = Base64.getEncoder().encode(credentialsString.getBytes(StandardCharsets.UTF_8));
|
byte[] encodedBytes = Base64.getEncoder().encode(credentialsString.getBytes(StandardCharsets.UTF_8));
|
||||||
return new String(encodedBytes, StandardCharsets.UTF_8);
|
return new String(encodedBytes, StandardCharsets.UTF_8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -162,7 +162,7 @@ public class OAuth2ClientAuthenticationFilterTests {
|
|||||||
@Test
|
@Test
|
||||||
public void doFilterWhenRequestMatchesAndBadCredentialsThenInvalidClientError() throws Exception {
|
public void doFilterWhenRequestMatchesAndBadCredentialsThenInvalidClientError() throws Exception {
|
||||||
when(this.authenticationConverter.convert(any(HttpServletRequest.class))).thenReturn(
|
when(this.authenticationConverter.convert(any(HttpServletRequest.class))).thenReturn(
|
||||||
new OAuth2ClientAuthenticationToken("clientId", "invalid-secret"));
|
new OAuth2ClientAuthenticationToken("clientId", "invalid-secret", null));
|
||||||
when(this.authenticationManager.authenticate(any(Authentication.class))).thenThrow(
|
when(this.authenticationManager.authenticate(any(Authentication.class))).thenThrow(
|
||||||
new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT)));
|
new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT)));
|
||||||
|
|
||||||
@ -185,7 +185,7 @@ public class OAuth2ClientAuthenticationFilterTests {
|
|||||||
public void doFilterWhenRequestMatchesAndValidCredentialsThenProcessed() throws Exception {
|
public void doFilterWhenRequestMatchesAndValidCredentialsThenProcessed() throws Exception {
|
||||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||||
when(this.authenticationConverter.convert(any(HttpServletRequest.class))).thenReturn(
|
when(this.authenticationConverter.convert(any(HttpServletRequest.class))).thenReturn(
|
||||||
new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), registeredClient.getClientSecret()));
|
new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), registeredClient.getClientSecret(), null));
|
||||||
when(this.authenticationManager.authenticate(any(Authentication.class))).thenReturn(
|
when(this.authenticationManager.authenticate(any(Authentication.class))).thenReturn(
|
||||||
new OAuth2ClientAuthenticationToken(registeredClient));
|
new OAuth2ClientAuthenticationToken(registeredClient));
|
||||||
|
|
||||||
|
@ -34,7 +34,6 @@ import org.springframework.security.oauth2.core.OAuth2Error;
|
|||||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
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.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
|
||||||
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
|
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
|
||||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||||
@ -167,17 +166,6 @@ public class OAuth2TokenEndpointFilterTests {
|
|||||||
OAuth2ParameterNames.GRANT_TYPE, OAuth2ErrorCodes.UNSUPPORTED_GRANT_TYPE, request);
|
OAuth2ParameterNames.GRANT_TYPE, OAuth2ErrorCodes.UNSUPPORTED_GRANT_TYPE, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void doFilterWhenTokenRequestMultipleClientIdThenInvalidRequestError() throws Exception {
|
|
||||||
MockHttpServletRequest request = createAuthorizationCodeTokenRequest(
|
|
||||||
TestRegisteredClients.registeredClient().build());
|
|
||||||
request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-1");
|
|
||||||
request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-2");
|
|
||||||
|
|
||||||
doFilterWhenTokenRequestInvalidParameterThenError(
|
|
||||||
OAuth2ParameterNames.CLIENT_ID, OAuth2ErrorCodes.INVALID_REQUEST, request);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void doFilterWhenTokenRequestMissingCodeThenInvalidRequestError() throws Exception {
|
public void doFilterWhenTokenRequestMissingCodeThenInvalidRequestError() throws Exception {
|
||||||
MockHttpServletRequest request = createAuthorizationCodeTokenRequest(
|
MockHttpServletRequest request = createAuthorizationCodeTokenRequest(
|
||||||
@ -208,26 +196,6 @@ public class OAuth2TokenEndpointFilterTests {
|
|||||||
OAuth2ParameterNames.REDIRECT_URI, OAuth2ErrorCodes.INVALID_REQUEST, request);
|
OAuth2ParameterNames.REDIRECT_URI, OAuth2ErrorCodes.INVALID_REQUEST, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void doFilterWhenTokenRequestNotAuthenticatedAndMissingCodeVerifierThenInvalidRequestError() throws Exception {
|
|
||||||
MockHttpServletRequest request = createAuthorizationCodeTokenRequest(
|
|
||||||
TestRegisteredClients.registeredClient().build());
|
|
||||||
|
|
||||||
doFilterWhenTokenRequestInvalidParameterThenError(
|
|
||||||
PkceParameterNames.CODE_VERIFIER, OAuth2ErrorCodes.INVALID_REQUEST, request);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void doFilterWhenTokenRequestNotAuthenticatedAndMultipleCodeVerifierThenInvalidRequestError() throws Exception {
|
|
||||||
MockHttpServletRequest request = createAuthorizationCodeTokenRequest(
|
|
||||||
TestRegisteredClients.registeredClient().build());
|
|
||||||
request.addParameter(PkceParameterNames.CODE_VERIFIER, "one-verifier");
|
|
||||||
request.addParameter(PkceParameterNames.CODE_VERIFIER, "two-verifier2");
|
|
||||||
|
|
||||||
doFilterWhenTokenRequestInvalidParameterThenError(
|
|
||||||
PkceParameterNames.CODE_VERIFIER, OAuth2ErrorCodes.INVALID_REQUEST, request);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void doFilterWhenAuthorizationCodeTokenRequestValidThenAccessTokenResponse() throws Exception {
|
public void doFilterWhenAuthorizationCodeTokenRequestValidThenAccessTokenResponse() throws Exception {
|
||||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||||
|
@ -0,0 +1,97 @@
|
|||||||
|
/*
|
||||||
|
* 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.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 PublicClientAuthenticationConverter}.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
*/
|
||||||
|
public class PublicClientAuthenticationConverterTests {
|
||||||
|
private PublicClientAuthenticationConverter converter = new PublicClientAuthenticationConverter();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void convertWhenNotPublicClientThenReturnNull() {
|
||||||
|
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||||
|
Authentication authentication = this.converter.convert(request);
|
||||||
|
assertThat(authentication).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void convertWhenMissingClientIdThenReturnNull() {
|
||||||
|
MockHttpServletRequest request = createPkceTokenRequest();
|
||||||
|
request.removeParameter(OAuth2ParameterNames.CLIENT_ID);
|
||||||
|
Authentication authentication = this.converter.convert(request);
|
||||||
|
assertThat(authentication).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void convertWhenMultipleClientIdThenInvalidRequestError() {
|
||||||
|
MockHttpServletRequest request = createPkceTokenRequest();
|
||||||
|
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 convertWhenMultipleCodeVerifierThenInvalidRequestError() {
|
||||||
|
MockHttpServletRequest request = createPkceTokenRequest();
|
||||||
|
request.addParameter(PkceParameterNames.CODE_VERIFIER, "code-verifier-2");
|
||||||
|
assertThatThrownBy(() -> this.converter.convert(request))
|
||||||
|
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||||
|
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||||
|
.extracting("errorCode")
|
||||||
|
.isEqualTo(OAuth2ErrorCodes.INVALID_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void convertWhenPublicClientThenReturnClientAuthenticationToken() {
|
||||||
|
MockHttpServletRequest request = createPkceTokenRequest();
|
||||||
|
OAuth2ClientAuthenticationToken authentication = (OAuth2ClientAuthenticationToken) this.converter.convert(request);
|
||||||
|
assertThat(authentication.getPrincipal()).isEqualTo("client-1");
|
||||||
|
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(OAuth2ParameterNames.CLIENT_ID, "client-1");
|
||||||
|
request.addParameter(PkceParameterNames.CODE_VERIFIER, "code-verifier-1");
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user