Extract AuthenticationProvider from OAuth2AuthorizationEndpointFilter

Closes gh-340
This commit is contained in:
Joe Grandja
2021-06-23 09:56:36 -04:00
parent 023e22c9d3
commit 08ba07d676
10 changed files with 2429 additions and 1767 deletions

View File

@@ -32,6 +32,7 @@ import org.springframework.security.config.annotation.web.configurers.ExceptionH
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationProvider;
@@ -221,6 +222,13 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
}
builder.authenticationProvider(postProcess(clientAuthenticationProvider));
OAuth2AuthorizationCodeRequestAuthenticationProvider authorizationCodeRequestAuthenticationProvider =
new OAuth2AuthorizationCodeRequestAuthenticationProvider(
OAuth2ConfigurerUtils.getRegisteredClientRepository(builder),
OAuth2ConfigurerUtils.getAuthorizationService(builder),
OAuth2ConfigurerUtils.getAuthorizationConsentService(builder));
builder.authenticationProvider(postProcess(authorizationCodeRequestAuthenticationProvider));
OAuth2TokenIntrospectionAuthenticationProvider tokenIntrospectionAuthenticationProvider =
new OAuth2TokenIntrospectionAuthenticationProvider(
OAuth2ConfigurerUtils.getRegisteredClientRepository(builder),
@@ -285,9 +293,7 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
OAuth2AuthorizationEndpointFilter authorizationEndpointFilter =
new OAuth2AuthorizationEndpointFilter(
OAuth2ConfigurerUtils.getRegisteredClientRepository(builder),
OAuth2ConfigurerUtils.getAuthorizationService(builder),
OAuth2ConfigurerUtils.getAuthorizationConsentService(builder),
authenticationManager,
providerSettings.authorizationEndpoint());
if (StringUtils.hasText(this.consentPage)) {
authorizationEndpointFilter.setUserConsentUri(this.consentPage);

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2020-2021 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;
import org.springframework.lang.Nullable;
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.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
/**
* This exception is thrown by {@link OAuth2AuthorizationCodeRequestAuthenticationProvider}
* when an attempt to authenticate the OAuth 2.0 Authorization Request (or Consent) fails.
*
* @author Joe Grandja
* @since 0.1.2
* @see OAuth2AuthorizationCodeRequestAuthenticationToken
* @see OAuth2AuthorizationCodeRequestAuthenticationProvider
*/
public class OAuth2AuthorizationCodeRequestAuthenticationException extends OAuth2AuthenticationException {
private final OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication;
/**
* Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationException} using the provided parameters.
*
* @param error the {@link OAuth2Error OAuth 2.0 Error}
* @param authorizationCodeRequestAuthentication the {@link Authentication} instance of the OAuth 2.0 Authorization Request (or Consent)
*/
public OAuth2AuthorizationCodeRequestAuthenticationException(OAuth2Error error,
@Nullable OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication) {
super(error);
this.authorizationCodeRequestAuthentication = authorizationCodeRequestAuthentication;
}
/**
* Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationException} using the provided parameters.
*
* @param error the {@link OAuth2Error OAuth 2.0 Error}
* @param cause the root cause
* @param authorizationCodeRequestAuthentication the {@link Authentication} instance of the OAuth 2.0 Authorization Request (or Consent)
*/
public OAuth2AuthorizationCodeRequestAuthenticationException(OAuth2Error error, Throwable cause,
@Nullable OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication) {
super(error, cause);
this.authorizationCodeRequestAuthentication = authorizationCodeRequestAuthentication;
}
/**
* Returns the {@link Authentication} instance of the OAuth 2.0 Authorization Request (or Consent), or {@code null} if not available.
*
* @return the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
*/
@Nullable
public OAuth2AuthorizationCodeRequestAuthenticationToken getAuthorizationCodeRequestAuthentication() {
return this.authorizationCodeRequestAuthentication;
}
}

View File

@@ -61,6 +61,7 @@ import static org.springframework.security.oauth2.server.authorization.authentic
* @since 0.0.1
* @see OAuth2AuthorizationCodeAuthenticationToken
* @see OAuth2AccessTokenAuthenticationToken
* @see OAuth2AuthorizationCodeRequestAuthenticationProvider
* @see OAuth2AuthorizationService
* @see JwtEncoder
* @see OAuth2TokenCustomizer

View File

@@ -0,0 +1,478 @@
/*
* Copyright 2020-2021 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.authentication;
import java.security.Principal;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Base64;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
import org.springframework.security.crypto.keygen.StringKeyGenerator;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2TokenType;
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.core.oidc.OidcScopes;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCodeRequestAuthenticationException;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
/**
* An {@link AuthenticationProvider} implementation for the OAuth 2.0 Authorization Request (and Consent)
* used in the Authorization Code Grant.
*
* @author Joe Grandja
* @since 0.1.2
* @see OAuth2AuthorizationCodeRequestAuthenticationToken
* @see OAuth2AuthorizationCodeAuthenticationProvider
* @see RegisteredClientRepository
* @see OAuth2AuthorizationService
* @see OAuth2AuthorizationConsentService
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1">Section 4.1.1 Authorization Request</a>
*/
public class OAuth2AuthorizationCodeRequestAuthenticationProvider implements AuthenticationProvider {
private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
private static final String PKCE_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1";
private static final Pattern LOOPBACK_ADDRESS_PATTERN =
Pattern.compile("^127(?:\\.[0-9]+){0,2}\\.[0-9]+$|^\\[(?:0*:)*?:?0*1]$");
private final RegisteredClientRepository registeredClientRepository;
private final OAuth2AuthorizationService authorizationService;
private final OAuth2AuthorizationConsentService authorizationConsentService;
private final StringKeyGenerator codeGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder());
/**
* Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationProvider} using the provided parameters.
*
* @param registeredClientRepository the repository of registered clients
* @param authorizationService the authorization service
* @param authorizationConsentService the authorization consent service
*/
public OAuth2AuthorizationCodeRequestAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
OAuth2AuthorizationService authorizationService, OAuth2AuthorizationConsentService authorizationConsentService) {
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
Assert.notNull(authorizationService, "authorizationService cannot be null");
Assert.notNull(authorizationConsentService, "authorizationConsentService cannot be null");
this.registeredClientRepository = registeredClientRepository;
this.authorizationService = authorizationService;
this.authorizationConsentService = authorizationConsentService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
return authorizationCodeRequestAuthentication.isConsent() ?
authenticateAuthorizationConsent(authentication) :
authenticateAuthorizationRequest(authentication);
}
@Override
public boolean supports(Class<?> authentication) {
return OAuth2AuthorizationCodeRequestAuthenticationToken.class.isAssignableFrom(authentication);
}
private Authentication authenticateAuthorizationRequest(Authentication authentication) throws AuthenticationException {
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(
authorizationCodeRequestAuthentication.getClientId());
if (registeredClient == null) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID,
authorizationCodeRequestAuthentication, null);
}
if (StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) {
if (!isValidRedirectUri(authorizationCodeRequestAuthentication.getRedirectUri(), registeredClient)) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
authorizationCodeRequestAuthentication, registeredClient);
}
} else if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID) ||
registeredClient.getRedirectUris().size() != 1) {
// redirect_uri is REQUIRED for OpenID Connect
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
authorizationCodeRequestAuthentication, registeredClient);
}
if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE)) {
throwError(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT, OAuth2ParameterNames.CLIENT_ID,
authorizationCodeRequestAuthentication, registeredClient);
}
Set<String> requestedScopes = authorizationCodeRequestAuthentication.getScopes();
Set<String> allowedScopes = registeredClient.getScopes();
if (!requestedScopes.isEmpty() && !allowedScopes.containsAll(requestedScopes)) {
throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE,
authorizationCodeRequestAuthentication, registeredClient);
}
// code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE)
String codeChallenge = (String) authorizationCodeRequestAuthentication.getAdditionalParameters().get(PkceParameterNames.CODE_CHALLENGE);
if (StringUtils.hasText(codeChallenge)) {
String codeChallengeMethod = (String) authorizationCodeRequestAuthentication.getAdditionalParameters().get(PkceParameterNames.CODE_CHALLENGE_METHOD);
if (StringUtils.hasText(codeChallengeMethod)) {
if (!"S256".equals(codeChallengeMethod) && !"plain".equals(codeChallengeMethod)) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, PKCE_ERROR_URI,
authorizationCodeRequestAuthentication, registeredClient, null);
}
}
} else if (registeredClient.getClientSettings().requireProofKey()) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE, PKCE_ERROR_URI,
authorizationCodeRequestAuthentication, registeredClient, null);
}
// ---------------
// The request is valid - ensure the resource owner is authenticated
// ---------------
Authentication principal = (Authentication) authorizationCodeRequestAuthentication.getPrincipal();
if (!isPrincipalAuthenticated(principal)) {
// Return the authorization request as-is where isAuthenticated() is false
return authorizationCodeRequestAuthentication;
}
OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
.authorizationUri(authorizationCodeRequestAuthentication.getAuthorizationUri())
.clientId(registeredClient.getClientId())
.redirectUri(authorizationCodeRequestAuthentication.getRedirectUri())
.scopes(requestedScopes)
.state(authorizationCodeRequestAuthentication.getState())
.additionalParameters(authorizationCodeRequestAuthentication.getAdditionalParameters())
.build();
OAuth2AuthorizationConsent currentAuthorizationConsent = this.authorizationConsentService.findById(
registeredClient.getId(), principal.getName());
if (requireAuthorizationConsent(registeredClient, authorizationRequest, currentAuthorizationConsent)) {
String state = this.stateGenerator.generateKey();
OAuth2Authorization authorization = authorizationBuilder(registeredClient, principal, authorizationRequest)
.attribute(OAuth2ParameterNames.STATE, state)
.build();
this.authorizationService.save(authorization);
// TODO Need to remove 'in-flight' authorization if consent step is not completed (e.g. approved or cancelled)
Set<String> currentAuthorizedScopes = currentAuthorizationConsent != null ?
currentAuthorizationConsent.getScopes() : null;
return OAuth2AuthorizationCodeRequestAuthenticationToken.with(registeredClient.getClientId(), principal)
.authorizationUri(authorizationRequest.getAuthorizationUri())
.scopes(currentAuthorizedScopes)
.state(state)
.consentRequired(true)
.build();
}
OAuth2AuthorizationCode authorizationCode = createAuthorizationCode();
OAuth2Authorization authorization = authorizationBuilder(registeredClient, principal, authorizationRequest)
.token(authorizationCode)
.attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizationRequest.getScopes())
.build();
this.authorizationService.save(authorization);
// TODO security checks for code parameter
// The authorization code MUST expire shortly after it is issued to mitigate the risk of leaks.
// A maximum authorization code lifetime of 10 minutes is RECOMMENDED.
// The client MUST NOT use the authorization code more than once.
// If an authorization code is used more than once, the authorization server MUST deny the request
// and SHOULD revoke (when possible) all tokens previously issued based on that authorization code.
// The authorization code is bound to the client identifier and redirection URI.
String redirectUri = authorizationRequest.getRedirectUri();
if (!StringUtils.hasText(redirectUri)) {
redirectUri = registeredClient.getRedirectUris().iterator().next();
}
return OAuth2AuthorizationCodeRequestAuthenticationToken.with(registeredClient.getClientId(), principal)
.authorizationUri(authorizationRequest.getAuthorizationUri())
.redirectUri(redirectUri)
.scopes(authorizationRequest.getScopes())
.state(authorizationRequest.getState())
.authorizationCode(authorizationCode)
.build();
}
private OAuth2AuthorizationCode createAuthorizationCode() {
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(5, ChronoUnit.MINUTES); // TODO Allow configuration for authorization code time-to-live
return new OAuth2AuthorizationCode(this.codeGenerator.generateKey(), issuedAt, expiresAt);
}
private Authentication authenticateAuthorizationConsent(Authentication authentication) throws AuthenticationException {
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
OAuth2Authorization authorization = this.authorizationService.findByToken(
authorizationCodeRequestAuthentication.getState(), STATE_TOKEN_TYPE);
if (authorization == null) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE,
authorizationCodeRequestAuthentication, null, null);
}
// The 'in-flight' authorization must be associated to the current principal
Authentication principal = (Authentication) authorizationCodeRequestAuthentication.getPrincipal();
if (!isPrincipalAuthenticated(principal) || !principal.getName().equals(authorization.getPrincipalName())) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE,
authorizationCodeRequestAuthentication, null, null);
}
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(
authorizationCodeRequestAuthentication.getClientId());
if (registeredClient == null || !registeredClient.getId().equals(authorization.getRegisteredClientId())) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID,
authorizationCodeRequestAuthentication, registeredClient);
}
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
Set<String> requestedScopes = authorizationRequest.getScopes();
Set<String> authorizedScopes = new HashSet<>(authorizationCodeRequestAuthentication.getScopes());
if (!requestedScopes.containsAll(authorizedScopes)) {
throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE,
authorizationCodeRequestAuthentication, registeredClient, authorizationRequest);
}
OAuth2AuthorizationConsent currentAuthorizationConsent = this.authorizationConsentService.findById(
authorization.getRegisteredClientId(), authorization.getPrincipalName());
Set<String> currentAuthorizedScopes = currentAuthorizationConsent != null ?
currentAuthorizationConsent.getScopes() : Collections.emptySet();
if (authorizedScopes.isEmpty() && currentAuthorizedScopes.isEmpty()) {
// Authorization consent denied
this.authorizationService.remove(authorization);
throwError(OAuth2ErrorCodes.ACCESS_DENIED, OAuth2ParameterNames.CLIENT_ID,
authorizationCodeRequestAuthentication, registeredClient, authorizationRequest);
}
if (requestedScopes.contains(OidcScopes.OPENID)) {
// 'openid' scope is auto-approved as it does not require consent
authorizedScopes.add(OidcScopes.OPENID);
}
if (!currentAuthorizedScopes.isEmpty()) {
for (String requestedScope : requestedScopes) {
if (currentAuthorizedScopes.contains(requestedScope)) {
authorizedScopes.add(requestedScope);
}
}
}
if (!authorizedScopes.isEmpty() && !authorizedScopes.equals(currentAuthorizedScopes)) {
OAuth2AuthorizationConsent.Builder authorizationConsentBuilder;
if (currentAuthorizationConsent != null) {
authorizationConsentBuilder = OAuth2AuthorizationConsent.from(currentAuthorizationConsent);
} else {
authorizationConsentBuilder = OAuth2AuthorizationConsent.withId(
authorization.getRegisteredClientId(), authorization.getPrincipalName());
}
authorizedScopes.forEach(authorizationConsentBuilder::scope);
OAuth2AuthorizationConsent authorizationConsent = authorizationConsentBuilder.build();
this.authorizationConsentService.save(authorizationConsent);
}
OAuth2AuthorizationCode authorizationCode = createAuthorizationCode();
OAuth2Authorization updatedAuthorization = OAuth2Authorization.from(authorization)
.token(authorizationCode)
.attributes(attrs -> {
attrs.remove(OAuth2ParameterNames.STATE);
attrs.put(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizedScopes);
})
.build();
this.authorizationService.save(updatedAuthorization);
String redirectUri = authorizationRequest.getRedirectUri();
if (!StringUtils.hasText(redirectUri)) {
redirectUri = registeredClient.getRedirectUris().iterator().next();
}
return OAuth2AuthorizationCodeRequestAuthenticationToken.with(registeredClient.getClientId(), principal)
.authorizationUri(authorizationRequest.getAuthorizationUri())
.redirectUri(redirectUri)
.scopes(authorizedScopes)
.state(authorizationRequest.getState())
.authorizationCode(authorizationCode)
.build();
}
private static OAuth2Authorization.Builder authorizationBuilder(RegisteredClient registeredClient, Authentication principal,
OAuth2AuthorizationRequest authorizationRequest) {
return OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(principal.getName())
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.attribute(Principal.class.getName(), principal)
.attribute(OAuth2AuthorizationRequest.class.getName(), authorizationRequest);
}
private static boolean requireAuthorizationConsent(RegisteredClient registeredClient,
OAuth2AuthorizationRequest authorizationRequest, OAuth2AuthorizationConsent authorizationConsent) {
if (!registeredClient.getClientSettings().requireUserConsent()) {
return false;
}
// 'openid' scope does not require consent
if (authorizationRequest.getScopes().contains(OidcScopes.OPENID) &&
authorizationRequest.getScopes().size() == 1) {
return false;
}
if (authorizationConsent != null &&
authorizationConsent.getScopes().containsAll(authorizationRequest.getScopes())) {
return false;
}
return true;
}
private static boolean isValidRedirectUri(String requestedRedirectUri, RegisteredClient registeredClient) {
UriComponents requestedRedirect;
try {
requestedRedirect = UriComponentsBuilder.fromUriString(requestedRedirectUri).build();
if (requestedRedirect.getFragment() != null) {
return false;
}
} catch (Exception ex) {
return false;
}
String requestedRedirectHost = requestedRedirect.getHost();
if (requestedRedirectHost == null || requestedRedirectHost.equals("localhost")) {
// As per https://tools.ietf.org/html/draft-ietf-oauth-v2-1-01#section-9.7.1
// While redirect URIs using localhost (i.e.,
// "http://localhost:{port}/{path}") function similarly to loopback IP
// redirects described in Section 10.3.3, the use of "localhost" is NOT RECOMMENDED.
return false;
}
if (!LOOPBACK_ADDRESS_PATTERN.matcher(requestedRedirectHost).matches()) {
// As per https://tools.ietf.org/html/draft-ietf-oauth-v2-1-01#section-9.7
// When comparing client redirect URIs against pre-registered URIs,
// authorization servers MUST utilize exact string matching.
return registeredClient.getRedirectUris().contains(requestedRedirectUri);
}
// As per https://tools.ietf.org/html/draft-ietf-oauth-v2-1-01#section-10.3.3
// The authorization server MUST allow any port to be specified at the
// time of the request for loopback IP redirect URIs, to accommodate
// clients that obtain an available ephemeral port from the operating
// system at the time of the request.
for (String registeredRedirectUri : registeredClient.getRedirectUris()) {
UriComponentsBuilder registeredRedirect = UriComponentsBuilder.fromUriString(registeredRedirectUri);
registeredRedirect.port(requestedRedirect.getPort());
if (registeredRedirect.build().toString().equals(requestedRedirect.toString())) {
return true;
}
}
return false;
}
private static boolean isPrincipalAuthenticated(Authentication principal) {
return principal != null &&
!AnonymousAuthenticationToken.class.isAssignableFrom(principal.getClass()) &&
principal.isAuthenticated();
}
private static void throwError(String errorCode, String parameterName,
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
RegisteredClient registeredClient) {
throwError(errorCode, parameterName, authorizationCodeRequestAuthentication, registeredClient, null);
}
private static void throwError(String errorCode, String parameterName,
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {
throwError(errorCode, parameterName, "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1",
authorizationCodeRequestAuthentication, registeredClient, authorizationRequest);
}
private static void throwError(String errorCode, String parameterName, String errorUri,
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {
boolean redirectOnError = true;
if (errorCode.equals(OAuth2ErrorCodes.INVALID_REQUEST) &&
(parameterName.equals(OAuth2ParameterNames.CLIENT_ID) ||
parameterName.equals(OAuth2ParameterNames.REDIRECT_URI) ||
parameterName.equals(OAuth2ParameterNames.STATE))) {
redirectOnError = false;
}
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult = authorizationCodeRequestAuthentication;
if (redirectOnError && !StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) {
String redirectUri = resolveRedirectUri(authorizationRequest, registeredClient);
String state = authorizationCodeRequestAuthentication.isConsent() && authorizationRequest != null ?
authorizationRequest.getState() : authorizationCodeRequestAuthentication.getState();
authorizationCodeRequestAuthenticationResult = from(authorizationCodeRequestAuthentication)
.redirectUri(redirectUri)
.state(state)
.build();
authorizationCodeRequestAuthenticationResult.setAuthenticated(authorizationCodeRequestAuthentication.isAuthenticated());
} else if (!redirectOnError && StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) {
authorizationCodeRequestAuthenticationResult = from(authorizationCodeRequestAuthentication)
.redirectUri(null) // Prevent redirects
.build();
authorizationCodeRequestAuthenticationResult.setAuthenticated(authorizationCodeRequestAuthentication.isAuthenticated());
}
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri);
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, authorizationCodeRequestAuthenticationResult);
}
private static String resolveRedirectUri(OAuth2AuthorizationRequest authorizationRequest, RegisteredClient registeredClient) {
if (authorizationRequest != null && StringUtils.hasText(authorizationRequest.getRedirectUri())) {
return authorizationRequest.getRedirectUri();
}
if (registeredClient != null) {
return registeredClient.getRedirectUris().iterator().next();
}
return null;
}
private static OAuth2AuthorizationCodeRequestAuthenticationToken.Builder from(OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication) {
return OAuth2AuthorizationCodeRequestAuthenticationToken.with(authorizationCodeRequestAuthentication.getClientId(), (Authentication) authorizationCodeRequestAuthentication.getPrincipal())
.authorizationUri(authorizationCodeRequestAuthentication.getAuthorizationUri())
.redirectUri(authorizationCodeRequestAuthentication.getRedirectUri())
.scopes(authorizationCodeRequestAuthentication.getScopes())
.state(authorizationCodeRequestAuthentication.getState())
.additionalParameters(authorizationCodeRequestAuthentication.getAdditionalParameters())
.consentRequired(authorizationCodeRequestAuthentication.isConsentRequired())
.consent(authorizationCodeRequestAuthentication.isConsent())
.authorizationCode(authorizationCodeRequestAuthentication.getAuthorizationCode());
}
}

View File

@@ -0,0 +1,320 @@
/*
* Copyright 2020-2021 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.authentication;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.Version;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* An {@link Authentication} implementation for the OAuth 2.0 Authorization Request (and Consent)
* used in the Authorization Code Grant.
*
* @author Joe Grandja
* @since 0.1.2
* @see OAuth2AuthorizationCodeRequestAuthenticationProvider
*/
public final class OAuth2AuthorizationCodeRequestAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
private String authorizationUri;
private String clientId;
private Authentication principal;
private String redirectUri;
private Set<String> scopes;
private String state;
private Map<String, Object> additionalParameters;
private boolean consentRequired;
private boolean consent;
private OAuth2AuthorizationCode authorizationCode;
private OAuth2AuthorizationCodeRequestAuthenticationToken() {
super(Collections.emptyList());
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public Object getCredentials() {
return "";
}
/**
* Returns the authorization URI.
*
* @return the authorization URI
*/
public String getAuthorizationUri() {
return this.authorizationUri;
}
/**
* Returns the client identifier.
*
* @return the client identifier
*/
public String getClientId() {
return this.clientId;
}
/**
* Returns the redirect uri.
*
* @return the redirect uri
*/
@Nullable
public String getRedirectUri() {
return this.redirectUri;
}
/**
* Returns the requested (or authorized) scope(s).
*
* @return the requested (or authorized) scope(s), or an empty {@code Set} if not available
*/
public Set<String> getScopes() {
return this.scopes;
}
/**
* Returns the state.
*
* @return the state
*/
@Nullable
public String getState() {
return this.state;
}
/**
* Returns the additional parameters.
*
* @return the additional parameters
*/
public Map<String, Object> getAdditionalParameters() {
return this.additionalParameters;
}
/**
* Returns {@code true} if authorization consent is required, {@code false} otherwise.
*
* @return {@code true} if authorization consent is required, {@code false} otherwise
*/
public boolean isConsentRequired() {
return this.consentRequired;
}
/**
* Returns {@code true} if this {@code Authentication} represents an authorization consent request,
* {@code false} otherwise.
*
* @return {@code true} if this {@code Authentication} represents an authorization consent request, {@code false} otherwise
*/
public boolean isConsent() {
return this.consent;
}
/**
* Returns the {@link OAuth2AuthorizationCode}.
*
* @return the {@link OAuth2AuthorizationCode}
*/
@Nullable
public OAuth2AuthorizationCode getAuthorizationCode() {
return this.authorizationCode;
}
/**
* Returns a new {@link Builder}, initialized with the given client identifier
* and {@code Principal} (Resource Owner).
*
* @param clientId the client identifier
* @param principal the {@code Principal} (Resource Owner)
* @return the {@link Builder}
*/
public static Builder with(@NonNull String clientId, @NonNull Authentication principal) {
Assert.hasText(clientId, "clientId cannot be empty");
Assert.notNull(principal, "principal cannot be null");
return new Builder(clientId, principal);
}
/**
* A builder for {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.
*/
public static final class Builder implements Serializable {
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
private String authorizationUri;
private String clientId;
private Authentication principal;
private String redirectUri;
private Set<String> scopes;
private String state;
private Map<String, Object> additionalParameters;
private boolean consentRequired;
private boolean consent;
private OAuth2AuthorizationCode authorizationCode;
private Builder(String clientId, Authentication principal) {
this.clientId = clientId;
this.principal = principal;
}
/**
* Sets the authorization URI.
*
* @param authorizationUri the authorization URI
* @return the {@link Builder}
*/
public Builder authorizationUri(String authorizationUri) {
this.authorizationUri = authorizationUri;
return this;
}
/**
* Sets the redirect uri.
*
* @param redirectUri the redirect uri
* @return the {@link Builder}
*/
public Builder redirectUri(String redirectUri) {
this.redirectUri = redirectUri;
return this;
}
/**
* Sets the requested (or authorized) scope(s).
*
* @param scopes the requested (or authorized) scope(s)
* @return the {@link Builder}
*/
public Builder scopes(Set<String> scopes) {
if (scopes != null) {
this.scopes = new HashSet<>(scopes);
}
return this;
}
/**
* Sets the state.
*
* @param state the state
* @return the {@link Builder}
*/
public Builder state(String state) {
this.state = state;
return this;
}
/**
* Sets the additional parameters.
*
* @param additionalParameters the additional parameters
* @return the {@link Builder}
*/
public Builder additionalParameters(Map<String, Object> additionalParameters) {
if (additionalParameters != null) {
this.additionalParameters = new HashMap<>(additionalParameters);
}
return this;
}
/**
* Set to {@code true} if authorization consent is required, {@code false} otherwise.
*
* @param consentRequired {@code true} if authorization consent is required, {@code false} otherwise
* @return the {@link Builder}
*/
public Builder consentRequired(boolean consentRequired) {
this.consentRequired = consentRequired;
return this;
}
/**
* Set to {@code true} if this {@code Authentication} represents an authorization consent request, {@code false} otherwise.
*
* @param consent {@code true} if this {@code Authentication} represents an authorization consent request, {@code false} otherwise
* @return the {@link Builder}
*/
public Builder consent(boolean consent) {
this.consent = consent;
return this;
}
/**
* Sets the {@link OAuth2AuthorizationCode}.
*
* @param authorizationCode the {@link OAuth2AuthorizationCode}
* @return the {@link Builder}
*/
public Builder authorizationCode(OAuth2AuthorizationCode authorizationCode) {
this.authorizationCode = authorizationCode;
return this;
}
/**
* Builds a new {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.
*
* @return the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
*/
public OAuth2AuthorizationCodeRequestAuthenticationToken build() {
Assert.hasText(this.authorizationUri, "authorizationUri cannot be empty");
if (this.consent) {
Assert.hasText(this.state, "state cannot be empty");
}
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
new OAuth2AuthorizationCodeRequestAuthenticationToken();
authentication.authorizationUri = this.authorizationUri;
authentication.clientId = this.clientId;
authentication.principal = this.principal;
authentication.redirectUri = this.redirectUri;
authentication.scopes = Collections.unmodifiableSet(
!CollectionUtils.isEmpty(this.scopes) ?
this.scopes :
Collections.emptySet());
authentication.state = this.state;
authentication.additionalParameters = Collections.unmodifiableMap(
!CollectionUtils.isEmpty(this.additionalParameters) ?
this.additionalParameters :
Collections.emptyMap());
authentication.consentRequired = this.consentRequired;
authentication.consent = this.consent;
authentication.authorizationCode = this.authorizationCode;
if (this.authorizationCode != null || this.consentRequired) {
authentication.setAuthenticated(true);
}
return authentication;
}
}
}

View File

@@ -410,7 +410,6 @@ public class OAuth2AuthorizationCodeGrantTests {
.param(OAuth2ParameterNames.SCOPE, "message.read")
.param(OAuth2ParameterNames.SCOPE, "message.write")
.param(OAuth2ParameterNames.STATE, "state")
.param("consent_action", "approve")
.with(user("user")))
.andExpect(status().is3xxRedirection())
.andReturn();
@@ -455,7 +454,7 @@ public class OAuth2AuthorizationCodeGrantTests {
.andExpect(status().is3xxRedirection())
.andReturn();
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
assertThat(redirectedUrl).matches("/oauth2/consent\\?scope=.+&client_id=.+&state=.+");
assertThat(redirectedUrl).matches("http://localhost/oauth2/consent\\?scope=.+&client_id=.+&state=.+");
String locationHeader = URLDecoder.decode(redirectedUrl, StandardCharsets.UTF_8.name());
UriComponents uriComponents = UriComponentsBuilder.fromUriString(locationHeader).build();

View File

@@ -0,0 +1,861 @@
/*
* Copyright 2020-2021 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.authentication;
import java.security.Principal;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2TokenType;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCodeRequestAuthenticationException;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Tests for {@link OAuth2AuthorizationCodeRequestAuthenticationProvider}.
*
* @author Joe Grandja
*/
public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
private RegisteredClientRepository registeredClientRepository;
private OAuth2AuthorizationService authorizationService;
private OAuth2AuthorizationConsentService authorizationConsentService;
private OAuth2AuthorizationCodeRequestAuthenticationProvider authenticationProvider;
private TestingAuthenticationToken principal;
@Before
public void setUp() {
this.registeredClientRepository = mock(RegisteredClientRepository.class);
this.authorizationService = mock(OAuth2AuthorizationService.class);
this.authorizationConsentService = mock(OAuth2AuthorizationConsentService.class);
this.authenticationProvider = new OAuth2AuthorizationCodeRequestAuthenticationProvider(
this.registeredClientRepository, this.authorizationService, this.authorizationConsentService);
this.principal = new TestingAuthenticationToken("principalName", "password");
this.principal.setAuthenticated(true);
}
@Test
public void constructorWhenRegisteredClientRepositoryNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new OAuth2AuthorizationCodeRequestAuthenticationProvider(
null, this.authorizationService, this.authorizationConsentService))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("registeredClientRepository cannot be null");
}
@Test
public void constructorWhenAuthorizationServiceNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new OAuth2AuthorizationCodeRequestAuthenticationProvider(
this.registeredClientRepository, null, this.authorizationConsentService))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("authorizationService cannot be null");
}
@Test
public void constructorWhenAuthorizationConsentServiceNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new OAuth2AuthorizationCodeRequestAuthenticationProvider(
this.registeredClientRepository, this.authorizationService, null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("authorizationConsentService cannot be null");
}
@Test
public void supportsWhenTypeOAuth2AuthorizationCodeRequestAuthenticationTokenThenReturnTrue() {
assertThat(this.authenticationProvider.supports(OAuth2AuthorizationCodeRequestAuthenticationToken.class)).isTrue();
}
@Test
public void authenticateWhenInvalidClientIdThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
authorizationCodeRequestAuthentication(registeredClient, this.principal)
.build();
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
.satisfies(ex ->
assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID, null)
);
}
// gh-243
@Test
public void authenticateWhenInvalidRedirectUriHostThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
.thenReturn(registeredClient);
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
authorizationCodeRequestAuthentication(registeredClient, this.principal)
.redirectUri("https:///invalid")
.build();
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
.satisfies(ex ->
assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, null)
);
}
// gh-243
@Test
public void authenticateWhenInvalidRedirectUriFragmentThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
.thenReturn(registeredClient);
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
authorizationCodeRequestAuthentication(registeredClient, this.principal)
.redirectUri("https://example.com#fragment")
.build();
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
.satisfies(ex ->
assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, null)
);
}
// gh-243
@Test
public void authenticateWhenRedirectUriLocalhostThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
.thenReturn(registeredClient);
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
authorizationCodeRequestAuthentication(registeredClient, this.principal)
.redirectUri("https://localhost:5000")
.build();
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
.satisfies(ex ->
assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, null)
);
}
@Test
public void authenticateWhenUnregisteredRedirectUriThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
.thenReturn(registeredClient);
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
authorizationCodeRequestAuthentication(registeredClient, this.principal)
.redirectUri("https://invalid-example.com")
.build();
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
.satisfies(ex ->
assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, null)
);
}
// gh-243
@Test
public void authenticateWhenRedirectUriIPv4LoopbackAndDifferentPortThenReturnAuthorizationCode() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.redirectUri("https://127.0.0.1:8080")
.build();
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
.thenReturn(registeredClient);
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
authorizationCodeRequestAuthentication(registeredClient, this.principal)
.redirectUri("https://127.0.0.1:5000")
.build();
OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult =
(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication);
assertAuthorizationCodeRequestWithAuthorizationCodeResult(registeredClient, authentication, authenticationResult);
}
// gh-243
@Test
public void authenticateWhenRedirectUriIPv6LoopbackAndDifferentPortThenReturnAuthorizationCode() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.redirectUri("https://[::1]:8080")
.build();
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
.thenReturn(registeredClient);
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
authorizationCodeRequestAuthentication(registeredClient, this.principal)
.redirectUri("https://[::1]:5000")
.build();
OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult =
(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication);
assertAuthorizationCodeRequestWithAuthorizationCodeResult(registeredClient, authentication, authenticationResult);
}
@Test
public void authenticateWhenMissingRedirectUriAndMultipleRegisteredThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().redirectUri("https://example2.com").build();
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
.thenReturn(registeredClient);
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
authorizationCodeRequestAuthentication(registeredClient, this.principal)
.redirectUri(null)
.build();
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
.satisfies(ex ->
assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, null)
);
}
@Test
public void authenticateWhenAuthenticationRequestMissingRedirectUriThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
// redirect_uri is REQUIRED for OpenID Connect requests
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.scope(OidcScopes.OPENID)
.build();
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
.thenReturn(registeredClient);
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
authorizationCodeRequestAuthentication(registeredClient, this.principal)
.redirectUri(null)
.build();
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
.satisfies(ex ->
assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, null)
);
}
@Test
public void authenticateWhenClientNotAuthorizedToRequestCodeThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.authorizationGrantTypes(Set::clear)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.build();
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
.thenReturn(registeredClient);
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
authorizationCodeRequestAuthentication(registeredClient, this.principal)
.build();
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
.satisfies(ex ->
assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
OAuth2ErrorCodes.UNAUTHORIZED_CLIENT, OAuth2ParameterNames.CLIENT_ID, authentication.getRedirectUri())
);
}
@Test
public void authenticateWhenInvalidScopeThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.build();
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
.thenReturn(registeredClient);
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
authorizationCodeRequestAuthentication(registeredClient, this.principal)
.scopes(Collections.singleton("invalid-scope"))
.build();
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
.satisfies(ex ->
assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE, authentication.getRedirectUri())
);
}
@Test
public void authenticateWhenPkceRequiredAndMissingCodeChallengeThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.clientSettings(clientSettings -> clientSettings.requireProofKey(true))
.build();
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
.thenReturn(registeredClient);
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
authorizationCodeRequestAuthentication(registeredClient, this.principal)
.build();
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
.satisfies(ex ->
assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE, authentication.getRedirectUri())
);
}
@Test
public void authenticateWhenPkceUnsupportedCodeChallengeMethodThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
.thenReturn(registeredClient);
Map<String, Object> additionalParameters = new HashMap<>();
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, "code-challenge");
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "unsupported");
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
authorizationCodeRequestAuthentication(registeredClient, this.principal)
.additionalParameters(additionalParameters)
.build();
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
.satisfies(ex ->
assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, authentication.getRedirectUri())
);
}
@Test
public void authenticateWhenPrincipalNotAuthenticatedThenReturnAuthorizationCodeRequest() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
.thenReturn(registeredClient);
this.principal.setAuthenticated(false);
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
authorizationCodeRequestAuthentication(registeredClient, this.principal)
.build();
OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult =
(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication);
assertThat(authenticationResult).isSameAs(authentication);
assertThat(authenticationResult.isAuthenticated()).isFalse();
}
@Test
public void authenticateWhenRequireAuthorizationConsentThenReturnAuthorizationConsent() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.clientSettings(clientSettings -> clientSettings.requireUserConsent(true))
.build();
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
.thenReturn(registeredClient);
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
authorizationCodeRequestAuthentication(registeredClient, this.principal)
.build();
OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult =
(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication);
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
verify(this.authorizationService).save(authorizationCaptor.capture());
OAuth2Authorization authorization = authorizationCaptor.getValue();
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
assertThat(authorizationRequest.getGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
assertThat(authorizationRequest.getResponseType()).isEqualTo(OAuth2AuthorizationResponseType.CODE);
assertThat(authorizationRequest.getAuthorizationUri()).isEqualTo(authentication.getAuthorizationUri());
assertThat(authorizationRequest.getClientId()).isEqualTo(registeredClient.getClientId());
assertThat(authorizationRequest.getRedirectUri()).isEqualTo(authentication.getRedirectUri());
assertThat(authorizationRequest.getScopes()).isEqualTo(authentication.getScopes());
assertThat(authorizationRequest.getState()).isEqualTo(authentication.getState());
assertThat(authorizationRequest.getAdditionalParameters()).isEqualTo(authentication.getAdditionalParameters());
assertThat(authorization.getRegisteredClientId()).isEqualTo(registeredClient.getId());
assertThat(authorization.getPrincipalName()).isEqualTo(this.principal.getName());
assertThat(authorization.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
assertThat(authorization.<Authentication>getAttribute(Principal.class.getName())).isEqualTo(this.principal);
String state = authorization.getAttribute(OAuth2ParameterNames.STATE);
assertThat(state).isNotNull();
assertThat(state).isNotEqualTo(authentication.getState());
assertThat(authenticationResult.getClientId()).isEqualTo(registeredClient.getClientId());
assertThat(authenticationResult.getPrincipal()).isEqualTo(this.principal);
assertThat(authenticationResult.getAuthorizationUri()).isEqualTo(authorizationRequest.getAuthorizationUri());
assertThat(authenticationResult.getScopes()).isEmpty();
assertThat(authenticationResult.getState()).isEqualTo(state);
assertThat(authenticationResult.isConsentRequired()).isTrue();
assertThat(authenticationResult.getAuthorizationCode()).isNull();
assertThat(authenticationResult.isAuthenticated()).isTrue();
}
@Test
public void authenticateWhenRequireAuthorizationConsentAndOnlyOpenidScopeRequestedThenAuthorizationConsentNotRequired() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.clientSettings(clientSettings -> clientSettings.requireUserConsent(true))
.scopes(scopes -> {
scopes.clear();
scopes.add(OidcScopes.OPENID);
})
.build();
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
.thenReturn(registeredClient);
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
authorizationCodeRequestAuthentication(registeredClient, this.principal)
.build();
OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult =
(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication);
assertAuthorizationCodeRequestWithAuthorizationCodeResult(registeredClient, authentication, authenticationResult);
}
@Test
public void authenticateWhenRequireAuthorizationConsentAndAllPreviouslyApprovedThenAuthorizationConsentNotRequired() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.clientSettings(clientSettings -> clientSettings.requireUserConsent(true))
.build();
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
.thenReturn(registeredClient);
OAuth2AuthorizationConsent.Builder builder =
OAuth2AuthorizationConsent.withId(registeredClient.getId(), this.principal.getName());
registeredClient.getScopes().forEach(builder::scope);
OAuth2AuthorizationConsent previousAuthorizationConsent = builder.build();
when(this.authorizationConsentService.findById(eq(registeredClient.getId()), eq(this.principal.getName())))
.thenReturn(previousAuthorizationConsent);
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
authorizationCodeRequestAuthentication(registeredClient, this.principal)
.build();
OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult =
(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication);
assertAuthorizationCodeRequestWithAuthorizationCodeResult(registeredClient, authentication, authenticationResult);
}
@Test
public void authenticateWhenAuthorizationCodeRequestValidThenReturnAuthorizationCode() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
.thenReturn(registeredClient);
Map<String, Object> additionalParameters = new HashMap<>();
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, "code-challenge");
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
authorizationCodeRequestAuthentication(registeredClient, this.principal)
.additionalParameters(additionalParameters)
.build();
OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult =
(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication);
assertAuthorizationCodeRequestWithAuthorizationCodeResult(registeredClient, authentication, authenticationResult);
}
private void assertAuthorizationCodeRequestWithAuthorizationCodeResult(
RegisteredClient registeredClient,
OAuth2AuthorizationCodeRequestAuthenticationToken authentication,
OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult) {
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
verify(this.authorizationService).save(authorizationCaptor.capture());
OAuth2Authorization authorization = authorizationCaptor.getValue();
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
assertThat(authorizationRequest.getGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
assertThat(authorizationRequest.getResponseType()).isEqualTo(OAuth2AuthorizationResponseType.CODE);
assertThat(authorizationRequest.getAuthorizationUri()).isEqualTo(authentication.getAuthorizationUri());
assertThat(authorizationRequest.getClientId()).isEqualTo(registeredClient.getClientId());
assertThat(authorizationRequest.getRedirectUri()).isEqualTo(authentication.getRedirectUri());
assertThat(authorizationRequest.getScopes()).isEqualTo(authentication.getScopes());
assertThat(authorizationRequest.getState()).isEqualTo(authentication.getState());
assertThat(authorizationRequest.getAdditionalParameters()).isEqualTo(authentication.getAdditionalParameters());
assertThat(authorization.getRegisteredClientId()).isEqualTo(registeredClient.getId());
assertThat(authorization.getPrincipalName()).isEqualTo(this.principal.getName());
assertThat(authorization.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
assertThat(authorization.<Authentication>getAttribute(Principal.class.getName())).isEqualTo(this.principal);
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization.getToken(OAuth2AuthorizationCode.class);
Set<String> authorizedScopes = authorization.getAttribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME);
assertThat(authenticationResult.getClientId()).isEqualTo(registeredClient.getClientId());
assertThat(authenticationResult.getPrincipal()).isEqualTo(this.principal);
assertThat(authenticationResult.getAuthorizationUri()).isEqualTo(authorizationRequest.getAuthorizationUri());
assertThat(authenticationResult.getRedirectUri()).isEqualTo(authorizationRequest.getRedirectUri());
assertThat(authenticationResult.getScopes()).isEqualTo(authorizedScopes);
assertThat(authenticationResult.getState()).isEqualTo(authorizationRequest.getState());
assertThat(authenticationResult.getAuthorizationCode()).isEqualTo(authorizationCode.getToken());
assertThat(authenticationResult.isAuthenticated()).isTrue();
}
@Test
public void authenticateWhenConsentRequestInvalidStateThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.build();
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
authorizationConsentRequestAuthentication(registeredClient, this.principal)
.build();
when(this.authorizationService.findByToken(eq(authentication.getState()), eq(STATE_TOKEN_TYPE)))
.thenReturn(null);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
.satisfies(ex ->
assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE, null)
);
}
@Test
public void authenticateWhenConsentRequestPrincipalNotAuthenticatedThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.build();
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
.principalName(this.principal.getName())
.build();
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
authorizationConsentRequestAuthentication(registeredClient, this.principal)
.build();
when(this.authorizationService.findByToken(eq(authentication.getState()), eq(STATE_TOKEN_TYPE)))
.thenReturn(authorization);
this.principal.setAuthenticated(false);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
.satisfies(ex ->
assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE, null)
);
}
@Test
public void authenticateWhenConsentRequestInvalidPrincipalThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.build();
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
.principalName(this.principal.getName().concat("-other"))
.build();
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
authorizationConsentRequestAuthentication(registeredClient, this.principal)
.build();
when(this.authorizationService.findByToken(eq(authentication.getState()), eq(STATE_TOKEN_TYPE)))
.thenReturn(authorization);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
.satisfies(ex ->
assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE, null)
);
}
@Test
public void authenticateWhenConsentRequestInvalidClientIdThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.build();
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
.principalName(this.principal.getName())
.build();
when(this.authorizationService.findByToken(eq("state"), eq(STATE_TOKEN_TYPE)))
.thenReturn(authorization);
RegisteredClient otherRegisteredClient = TestRegisteredClients.registeredClient2()
.build();
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
authorizationConsentRequestAuthentication(otherRegisteredClient, this.principal)
.state("state")
.build();
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
.satisfies(ex ->
assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID, null)
);
}
@Test
public void authenticateWhenConsentRequestDoesNotMatchClientThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.build();
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
.thenReturn(registeredClient);
RegisteredClient otherRegisteredClient = TestRegisteredClients.registeredClient2()
.build();
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(otherRegisteredClient)
.principalName(this.principal.getName())
.build();
when(this.authorizationService.findByToken(eq("state"), eq(STATE_TOKEN_TYPE)))
.thenReturn(authorization);
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
authorizationConsentRequestAuthentication(registeredClient, this.principal)
.state("state")
.build();
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
.satisfies(ex ->
assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID, null)
);
}
@Test
public void authenticateWhenConsentRequestScopeNotRequestedThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.build();
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
.thenReturn(registeredClient);
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
.principalName(this.principal.getName())
.build();
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
Set<String> authorizedScopes = new HashSet<>(authorizationRequest.getScopes());
authorizedScopes.add("scope-not-requested");
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
authorizationConsentRequestAuthentication(registeredClient, this.principal)
.scopes(authorizedScopes)
.build();
when(this.authorizationService.findByToken(eq(authentication.getState()), eq(STATE_TOKEN_TYPE)))
.thenReturn(authorization);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
.satisfies(ex ->
assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE, authorizationRequest.getRedirectUri())
);
}
@Test
public void authenticateWhenConsentRequestNotApprovedThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.build();
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
.thenReturn(registeredClient);
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
.principalName(this.principal.getName())
.build();
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
authorizationConsentRequestAuthentication(registeredClient, this.principal)
.scopes(new HashSet<>()) // No scopes approved
.build();
when(this.authorizationService.findByToken(eq(authentication.getState()), eq(STATE_TOKEN_TYPE)))
.thenReturn(authorization);
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
.satisfies(ex ->
assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
OAuth2ErrorCodes.ACCESS_DENIED, OAuth2ParameterNames.CLIENT_ID, authorizationRequest.getRedirectUri())
);
verify(this.authorizationService).remove(eq(authorization));
}
@Test
public void authenticateWhenConsentRequestApproveAllThenReturnAuthorizationCode() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.build();
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
.thenReturn(registeredClient);
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
.principalName(this.principal.getName())
.build();
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
Set<String> authorizedScopes = authorizationRequest.getScopes();
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
authorizationConsentRequestAuthentication(registeredClient, this.principal)
.scopes(authorizedScopes) // Approve all scopes
.build();
when(this.authorizationService.findByToken(eq(authentication.getState()), eq(STATE_TOKEN_TYPE)))
.thenReturn(authorization);
OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult =
(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication);
ArgumentCaptor<OAuth2AuthorizationConsent> authorizationConsentCaptor = ArgumentCaptor.forClass(OAuth2AuthorizationConsent.class);
verify(this.authorizationConsentService).save(authorizationConsentCaptor.capture());
OAuth2AuthorizationConsent authorizationConsent = authorizationConsentCaptor.getValue();
assertThat(authorizationConsent.getRegisteredClientId()).isEqualTo(authorization.getRegisteredClientId());
assertThat(authorizationConsent.getPrincipalName()).isEqualTo(authorization.getPrincipalName());
assertThat(authorizationConsent.getAuthorities()).hasSize(authorizedScopes.size());
assertThat(authorizationConsent.getScopes()).containsExactlyInAnyOrderElementsOf(authorizedScopes);
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
verify(this.authorizationService).save(authorizationCaptor.capture());
OAuth2Authorization updatedAuthorization = authorizationCaptor.getValue();
assertThat(updatedAuthorization.getRegisteredClientId()).isEqualTo(authorization.getRegisteredClientId());
assertThat(updatedAuthorization.getPrincipalName()).isEqualTo(authorization.getPrincipalName());
assertThat(updatedAuthorization.getAuthorizationGrantType()).isEqualTo(authorization.getAuthorizationGrantType());
assertThat(updatedAuthorization.<Authentication>getAttribute(Principal.class.getName()))
.isEqualTo(authorization.<Authentication>getAttribute(Principal.class.getName()));
assertThat(updatedAuthorization.<OAuth2AuthorizationRequest>getAttribute(OAuth2AuthorizationRequest.class.getName()))
.isEqualTo(authorizationRequest);
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = updatedAuthorization.getToken(OAuth2AuthorizationCode.class);
assertThat(authorizationCode).isNotNull();
assertThat(updatedAuthorization.<String>getAttribute(OAuth2ParameterNames.STATE)).isNull();
assertThat(updatedAuthorization.<Set<String>>getAttribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME))
.isEqualTo(authorizedScopes);
assertThat(authenticationResult.getClientId()).isEqualTo(registeredClient.getClientId());
assertThat(authenticationResult.getPrincipal()).isEqualTo(this.principal);
assertThat(authenticationResult.getAuthorizationUri()).isEqualTo(authorizationRequest.getAuthorizationUri());
assertThat(authenticationResult.getRedirectUri()).isEqualTo(authorizationRequest.getRedirectUri());
assertThat(authenticationResult.getScopes()).isEqualTo(authorizedScopes);
assertThat(authenticationResult.getState()).isEqualTo(authorizationRequest.getState());
assertThat(authenticationResult.getAuthorizationCode()).isEqualTo(authorizationCode.getToken());
assertThat(authenticationResult.isAuthenticated()).isTrue();
}
@Test
public void authenticateWhenConsentRequestWithPreviouslyApprovedThenAuthorizationConsentUpdated() {
String previouslyApprovedScope = "message.read";
String requestedScope = "message.write";
String otherPreviouslyApprovedScope = "other.scope";
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.scopes(scopes -> {
scopes.clear();
scopes.add(previouslyApprovedScope);
scopes.add(requestedScope);
})
.build();
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
.thenReturn(registeredClient);
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
.principalName(this.principal.getName())
.build();
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
Set<String> requestedScopes = authorizationRequest.getScopes();
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
authorizationConsentRequestAuthentication(registeredClient, this.principal)
.scopes(requestedScopes)
.build();
when(this.authorizationService.findByToken(eq(authentication.getState()), eq(STATE_TOKEN_TYPE)))
.thenReturn(authorization);
OAuth2AuthorizationConsent previousAuthorizationConsent =
OAuth2AuthorizationConsent.withId(authorization.getRegisteredClientId(), authorization.getPrincipalName())
.scope(previouslyApprovedScope)
.scope(otherPreviouslyApprovedScope)
.build();
when(this.authorizationConsentService.findById(eq(authorization.getRegisteredClientId()), eq(authorization.getPrincipalName())))
.thenReturn(previousAuthorizationConsent);
OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult =
(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication);
ArgumentCaptor<OAuth2AuthorizationConsent> authorizationConsentCaptor = ArgumentCaptor.forClass(OAuth2AuthorizationConsent.class);
verify(this.authorizationConsentService).save(authorizationConsentCaptor.capture());
OAuth2AuthorizationConsent updatedAuthorizationConsent = authorizationConsentCaptor.getValue();
assertThat(updatedAuthorizationConsent.getRegisteredClientId()).isEqualTo(previousAuthorizationConsent.getRegisteredClientId());
assertThat(updatedAuthorizationConsent.getPrincipalName()).isEqualTo(previousAuthorizationConsent.getPrincipalName());
assertThat(updatedAuthorizationConsent.getScopes()).containsExactlyInAnyOrder(
previouslyApprovedScope, otherPreviouslyApprovedScope, requestedScope);
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
verify(this.authorizationService).save(authorizationCaptor.capture());
OAuth2Authorization updatedAuthorization = authorizationCaptor.getValue();
assertThat(updatedAuthorization.<Set<String>>getAttribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME))
.isEqualTo(requestedScopes);
assertThat(authenticationResult.getScopes()).isEqualTo(requestedScopes);
}
@Test
public void authenticateWhenConsentRequestApproveNoneButPreviouslyApprovedThenAuthorizationConsentNotUpdated() {
String previouslyApprovedScope = "message.read";
String requestedScope = "message.write";
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.scopes(scopes -> {
scopes.clear();
scopes.add(previouslyApprovedScope);
scopes.add(requestedScope);
})
.build();
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
.thenReturn(registeredClient);
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
.principalName(this.principal.getName())
.build();
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
authorizationConsentRequestAuthentication(registeredClient, this.principal)
.scopes(new HashSet<>()) // No scopes approved
.build();
when(this.authorizationService.findByToken(eq(authentication.getState()), eq(STATE_TOKEN_TYPE)))
.thenReturn(authorization);
OAuth2AuthorizationConsent previousAuthorizationConsent =
OAuth2AuthorizationConsent.withId(authorization.getRegisteredClientId(), authorization.getPrincipalName())
.scope(previouslyApprovedScope)
.build();
when(this.authorizationConsentService.findById(eq(authorization.getRegisteredClientId()), eq(authorization.getPrincipalName())))
.thenReturn(previousAuthorizationConsent);
OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult =
(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication);
verify(this.authorizationConsentService, never()).save(any());
assertThat(authenticationResult.getScopes()).isEqualTo(Collections.singleton(previouslyApprovedScope));
}
private static void assertAuthenticationException(OAuth2AuthorizationCodeRequestAuthenticationException authenticationException,
String errorCode, String parameterName, String redirectUri) {
OAuth2Error error = authenticationException.getError();
assertThat(error.getErrorCode()).isEqualTo(errorCode);
assertThat(error.getDescription()).contains(parameterName);
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
authenticationException.getAuthorizationCodeRequestAuthentication();
assertThat(authorizationCodeRequestAuthentication.getRedirectUri()).isEqualTo(redirectUri);
}
private static OAuth2AuthorizationCodeRequestAuthenticationToken.Builder authorizationCodeRequestAuthentication(
RegisteredClient registeredClient, Authentication principal) {
return OAuth2AuthorizationCodeRequestAuthenticationToken.with(registeredClient.getClientId(), principal)
.authorizationUri("https://provider.com/oauth2/authorize")
.redirectUri(registeredClient.getRedirectUris().iterator().next())
.scopes(registeredClient.getScopes())
.state("state");
}
private static OAuth2AuthorizationCodeRequestAuthenticationToken.Builder authorizationConsentRequestAuthentication(
RegisteredClient registeredClient, Authentication principal) {
return OAuth2AuthorizationCodeRequestAuthenticationToken.with(registeredClient.getClientId(), principal)
.authorizationUri("https://provider.com/oauth2/authorize")
.scopes(registeredClient.getScopes())
.state("state")
.consent(true);
}
}

View File

@@ -0,0 +1,200 @@
/*
* Copyright 2020-2021 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.authentication;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import org.junit.Test;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Tests for {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.
*
* @author Joe Grandja
*/
public class OAuth2AuthorizationCodeRequestAuthenticationTokenTests {
private static final String AUTHORIZATION_URI = "https://provider.com/oauth2/authorize";
private static final String STATE = "state";
private static final RegisteredClient REGISTERED_CLIENT = TestRegisteredClients.registeredClient().build();
private static final TestingAuthenticationToken PRINCIPAL = new TestingAuthenticationToken("principalName", "password");
private static final OAuth2AuthorizationCode AUTHORIZATION_CODE =
new OAuth2AuthorizationCode("code", Instant.now(), Instant.now().plus(5, ChronoUnit.MINUTES));
@Test
public void withWhenClientIdNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> OAuth2AuthorizationCodeRequestAuthenticationToken.with(null, PRINCIPAL))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("clientId cannot be empty");
}
@Test
public void withWhenPrincipalNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> OAuth2AuthorizationCodeRequestAuthenticationToken.with(REGISTERED_CLIENT.getClientId(), null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("principal cannot be null");
}
@Test
public void buildWhenAuthorizationUriNotProvidedThenThrowIllegalArgumentException() {
assertThatThrownBy(() ->
OAuth2AuthorizationCodeRequestAuthenticationToken.with(REGISTERED_CLIENT.getClientId(), PRINCIPAL)
.build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("authorizationUri cannot be empty");
}
@Test
public void buildWhenStateNotProvidedThenThrowIllegalArgumentException() {
assertThatThrownBy(() ->
OAuth2AuthorizationCodeRequestAuthenticationToken.with(REGISTERED_CLIENT.getClientId(), PRINCIPAL)
.authorizationUri(AUTHORIZATION_URI)
.consent(true)
.build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("state cannot be empty");
}
@Test
public void buildWhenAuthorizationCodeRequestThenValuesAreSet() {
String clientId = REGISTERED_CLIENT.getClientId();
String redirectUri = REGISTERED_CLIENT.getRedirectUris().iterator().next();
Set<String> requestedScopes = REGISTERED_CLIENT.getScopes();
Map<String, Object> additionalParameters = Collections.singletonMap("param1", "value1");
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
OAuth2AuthorizationCodeRequestAuthenticationToken.with(clientId, PRINCIPAL)
.authorizationUri(AUTHORIZATION_URI)
.redirectUri(redirectUri)
.scopes(requestedScopes)
.state(STATE)
.additionalParameters(additionalParameters)
.build();
assertThat(authentication.getPrincipal()).isEqualTo(PRINCIPAL);
assertThat(authentication.getCredentials()).isEqualTo("");
assertThat(authentication.getAuthorities()).isEmpty();
assertThat(authentication.getAuthorizationUri()).isEqualTo(AUTHORIZATION_URI);
assertThat(authentication.getClientId()).isEqualTo(clientId);
assertThat(authentication.getRedirectUri()).isEqualTo(redirectUri);
assertThat(authentication.getScopes()).containsExactlyInAnyOrderElementsOf(requestedScopes);
assertThat(authentication.getState()).isEqualTo(STATE);
assertThat(authentication.getAdditionalParameters()).containsExactlyInAnyOrderEntriesOf(additionalParameters);
assertThat(authentication.isConsentRequired()).isFalse();
assertThat(authentication.isConsent()).isFalse();
assertThat(authentication.getAuthorizationCode()).isNull();
assertThat(authentication.isAuthenticated()).isFalse();
}
@Test
public void buildWhenAuthorizationConsentRequiredThenValuesAreSet() {
String clientId = REGISTERED_CLIENT.getClientId();
Set<String> authorizedScopes = REGISTERED_CLIENT.getScopes();
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
OAuth2AuthorizationCodeRequestAuthenticationToken.with(clientId, PRINCIPAL)
.authorizationUri(AUTHORIZATION_URI)
.scopes(authorizedScopes)
.state(STATE)
.consentRequired(true)
.build();
assertThat(authentication.getPrincipal()).isEqualTo(PRINCIPAL);
assertThat(authentication.getCredentials()).isEqualTo("");
assertThat(authentication.getAuthorities()).isEmpty();
assertThat(authentication.getAuthorizationUri()).isEqualTo(AUTHORIZATION_URI);
assertThat(authentication.getClientId()).isEqualTo(clientId);
assertThat(authentication.getRedirectUri()).isNull();
assertThat(authentication.getScopes()).containsExactlyInAnyOrderElementsOf(authorizedScopes);
assertThat(authentication.getState()).isEqualTo(STATE);
assertThat(authentication.getAdditionalParameters()).isEmpty();
assertThat(authentication.isConsentRequired()).isTrue();
assertThat(authentication.isConsent()).isFalse();
assertThat(authentication.getAuthorizationCode()).isNull();
assertThat(authentication.isAuthenticated()).isTrue();
}
@Test
public void buildWhenAuthorizationConsentRequestThenValuesAreSet() {
String clientId = REGISTERED_CLIENT.getClientId();
Set<String> authorizedScopes = REGISTERED_CLIENT.getScopes();
Map<String, Object> additionalParameters = Collections.singletonMap("param1", "value1");
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
OAuth2AuthorizationCodeRequestAuthenticationToken.with(clientId, PRINCIPAL)
.authorizationUri(AUTHORIZATION_URI)
.scopes(authorizedScopes)
.state(STATE)
.additionalParameters(additionalParameters)
.consent(true)
.build();
assertThat(authentication.getPrincipal()).isEqualTo(PRINCIPAL);
assertThat(authentication.getCredentials()).isEqualTo("");
assertThat(authentication.getAuthorities()).isEmpty();
assertThat(authentication.getAuthorizationUri()).isEqualTo(AUTHORIZATION_URI);
assertThat(authentication.getClientId()).isEqualTo(clientId);
assertThat(authentication.getRedirectUri()).isNull();
assertThat(authentication.getScopes()).containsExactlyInAnyOrderElementsOf(authorizedScopes);
assertThat(authentication.getState()).isEqualTo(STATE);
assertThat(authentication.getAdditionalParameters()).containsExactlyInAnyOrderEntriesOf(additionalParameters);
assertThat(authentication.isConsentRequired()).isFalse();
assertThat(authentication.isConsent()).isTrue();
assertThat(authentication.getAuthorizationCode()).isNull();
assertThat(authentication.isAuthenticated()).isFalse();
}
@Test
public void buildWhenAuthorizationResponseThenValuesAreSet() {
String clientId = REGISTERED_CLIENT.getClientId();
String redirectUri = REGISTERED_CLIENT.getRedirectUris().iterator().next();
Set<String> authorizedScopes = REGISTERED_CLIENT.getScopes();
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
OAuth2AuthorizationCodeRequestAuthenticationToken.with(clientId, PRINCIPAL)
.authorizationUri(AUTHORIZATION_URI)
.redirectUri(redirectUri)
.scopes(authorizedScopes)
.state(STATE)
.authorizationCode(AUTHORIZATION_CODE)
.build();
assertThat(authentication.getPrincipal()).isEqualTo(PRINCIPAL);
assertThat(authentication.getCredentials()).isEqualTo("");
assertThat(authentication.getAuthorities()).isEmpty();
assertThat(authentication.getAuthorizationUri()).isEqualTo(AUTHORIZATION_URI);
assertThat(authentication.getClientId()).isEqualTo(clientId);
assertThat(authentication.getRedirectUri()).isEqualTo(redirectUri);
assertThat(authentication.getScopes()).containsExactlyInAnyOrderElementsOf(authorizedScopes);
assertThat(authentication.getState()).isEqualTo(STATE);
assertThat(authentication.getAdditionalParameters()).isEmpty();
assertThat(authentication.isConsentRequired()).isFalse();
assertThat(authentication.isConsent()).isFalse();
assertThat(authentication.getAuthorizationCode()).isEqualTo(AUTHORIZATION_CODE);
assertThat(authentication.isAuthenticated()).isTrue();
}
}