diff --git a/docs/src/docs/asciidoc/protocol-endpoints.adoc b/docs/src/docs/asciidoc/protocol-endpoints.adoc index 1a01c230..0be400bf 100644 --- a/docs/src/docs/asciidoc/protocol-endpoints.adoc +++ b/docs/src/docs/asciidoc/protocol-endpoints.adoc @@ -32,9 +32,9 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h return http.build(); } ---- -<1> `authorizationRequestConverter()`: Adds an `AuthenticationConverter` (_pre-processor_) used when attempting to extract an https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1[OAuth2 authorization request] (or consent) from `HttpServletRequest` to an instance of `OAuth2AuthorizationCodeRequestAuthenticationToken`. +<1> `authorizationRequestConverter()`: Adds an `AuthenticationConverter` (_pre-processor_) used when attempting to extract an https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1[OAuth2 authorization request] (or consent) from `HttpServletRequest` to an instance of `OAuth2AuthorizationCodeRequestAuthenticationToken` or `OAuth2AuthorizationConsentAuthenticationToken`. <2> `authorizationRequestConverters()`: Sets the `Consumer` providing access to the `List` of default and (optionally) added ``AuthenticationConverter``'s allowing the ability to add, remove, or customize a specific `AuthenticationConverter`. -<3> `authenticationProvider()`: Adds an `AuthenticationProvider` (_main processor_) used for authenticating the `OAuth2AuthorizationCodeRequestAuthenticationToken`. +<3> `authenticationProvider()`: Adds an `AuthenticationProvider` (_main processor_) used for authenticating the `OAuth2AuthorizationCodeRequestAuthenticationToken` or `OAuth2AuthorizationConsentAuthenticationToken`. <4> `authenticationProviders()`: Sets the `Consumer` providing access to the `List` of default and (optionally) added ``AuthenticationProvider``'s allowing the ability to add, remove, or customize a specific `AuthenticationProvider`. <5> `authorizationResponseHandler()`: The `AuthenticationSuccessHandler` (_post-processor_) used for handling an "`authenticated`" `OAuth2AuthorizationCodeRequestAuthenticationToken` and returning the https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2[OAuth2AuthorizationResponse]. <6> `errorResponseHandler()`: The `AuthenticationFailureHandler` (_post-processor_) used for handling an `OAuth2AuthorizationCodeRequestAuthenticationException` and returning the https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1[OAuth2Error response]. @@ -45,8 +45,8 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h `OAuth2AuthorizationEndpointFilter` is configured with the following defaults: -* `*AuthenticationConverter*` -- An `OAuth2AuthorizationCodeRequestAuthenticationConverter`. -* `*AuthenticationManager*` -- An `AuthenticationManager` composed of `OAuth2AuthorizationCodeRequestAuthenticationProvider`. +* `*AuthenticationConverter*` -- A `DelegatingAuthenticationConverter` composed of `OAuth2AuthorizationCodeRequestAuthenticationConverter` and `OAuth2AuthorizationConsentAuthenticationConverter`. +* `*AuthenticationManager*` -- An `AuthenticationManager` composed of `OAuth2AuthorizationCodeRequestAuthenticationProvider` and `OAuth2AuthorizationConsentAuthenticationProvider`. * `*AuthenticationSuccessHandler*` -- An internal implementation that handles an "`authenticated`" `OAuth2AuthorizationCodeRequestAuthenticationToken` and returns the `OAuth2AuthorizationResponse`. * `*AuthenticationFailureHandler*` -- An internal implementation that uses the `OAuth2Error` associated with the `OAuth2AuthorizationCodeRequestAuthenticationException` and returns the `OAuth2Error` response. diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeGenerator.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeGenerator.java new file mode 100644 index 00000000..9e5b6e28 --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeGenerator.java @@ -0,0 +1,55 @@ +/* + * Copyright 2020-2022 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.util.Base64; + +import org.springframework.lang.Nullable; +import org.springframework.security.crypto.keygen.Base64StringKeyGenerator; +import org.springframework.security.crypto.keygen.StringKeyGenerator; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; + +/** + * An {@link OAuth2TokenGenerator} that generates an {@link OAuth2AuthorizationCode}. + * + * @author Joe Grandja + * @since 0.4.0 + * @see OAuth2TokenGenerator + * @see OAuth2AuthorizationCode + * @see OAuth2AuthorizationCodeRequestAuthenticationProvider + * @see OAuth2AuthorizationConsentAuthenticationProvider + */ +final class OAuth2AuthorizationCodeGenerator implements OAuth2TokenGenerator { + private final StringKeyGenerator authorizationCodeGenerator = + new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96); + + @Nullable + @Override + public OAuth2AuthorizationCode generate(OAuth2TokenContext context) { + if (context.getTokenType() == null || + !OAuth2ParameterNames.CODE.equals(context.getTokenType().getValue())) { + return null; + } + Instant issuedAt = Instant.now(); + Instant expiresAt = issuedAt.plus(context.getRegisteredClient().getTokenSettings().getAuthorizationCodeTimeToLive()); + return new OAuth2AuthorizationCode(this.authorizationCodeGenerator.generateKey(), issuedAt, expiresAt); + } + +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java index 80c50097..3fe93cb4 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java @@ -16,19 +16,14 @@ package org.springframework.security.oauth2.server.authorization.authentication; import java.security.Principal; -import java.time.Instant; import java.util.Base64; -import java.util.Collections; -import java.util.HashSet; import java.util.Set; import java.util.function.Consumer; -import org.springframework.lang.Nullable; 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.core.GrantedAuthority; import org.springframework.security.crypto.keygen.Base64StringKeyGenerator; import org.springframework.security.crypto.keygen.StringKeyGenerator; import org.springframework.security.oauth2.core.AuthorizationGrantType; @@ -54,7 +49,7 @@ import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** - * An {@link AuthenticationProvider} implementation for the OAuth 2.0 Authorization Request (and Consent) + * An {@link AuthenticationProvider} implementation for the OAuth 2.0 Authorization Request * used in the Authorization Code Grant. * * @author Joe Grandja @@ -63,6 +58,7 @@ import org.springframework.util.StringUtils; * @see OAuth2AuthorizationCodeRequestAuthenticationToken * @see OAuth2AuthorizationCodeRequestAuthenticationValidator * @see OAuth2AuthorizationCodeAuthenticationProvider + * @see OAuth2AuthorizationConsentAuthenticationProvider * @see RegisteredClientRepository * @see OAuth2AuthorizationService * @see OAuth2AuthorizationConsentService @@ -71,7 +67,6 @@ import org.springframework.util.StringUtils; public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implements AuthenticationProvider { private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1"; private static final String PKCE_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1"; - private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE); private static final StringKeyGenerator DEFAULT_STATE_GENERATOR = new Base64StringKeyGenerator(Base64.getUrlEncoder()); private final RegisteredClientRepository registeredClientRepository; @@ -80,7 +75,6 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen private OAuth2TokenGenerator authorizationCodeGenerator = new OAuth2AuthorizationCodeGenerator(); private Consumer authenticationValidator = new OAuth2AuthorizationCodeRequestAuthenticationValidator(); - private Consumer authorizationConsentCustomizer; /** * Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationProvider} using the provided parameters. @@ -104,73 +98,6 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = (OAuth2AuthorizationCodeRequestAuthenticationToken) authentication; - return authorizationCodeRequestAuthentication.isConsent() ? - authenticateAuthorizationConsent(authentication) : - authenticateAuthorizationRequest(authentication); - } - - @Override - public boolean supports(Class authentication) { - return OAuth2AuthorizationCodeRequestAuthenticationToken.class.isAssignableFrom(authentication); - } - - /** - * Sets the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode}. - * - * @param authorizationCodeGenerator the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode} - * @since 0.2.3 - */ - public void setAuthorizationCodeGenerator(OAuth2TokenGenerator authorizationCodeGenerator) { - Assert.notNull(authorizationCodeGenerator, "authorizationCodeGenerator cannot be null"); - this.authorizationCodeGenerator = authorizationCodeGenerator; - } - - /** - * Sets the {@code Consumer} providing access to the {@link OAuth2AuthorizationCodeRequestAuthenticationContext} - * and is responsible for validating specific OAuth 2.0 Authorization Request parameters - * associated in the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}. - * The default authentication validator is {@link OAuth2AuthorizationCodeRequestAuthenticationValidator}. - * - *

- * NOTE: The authentication validator MUST throw {@link OAuth2AuthorizationCodeRequestAuthenticationException} if validation fails. - * - * @param authenticationValidator the {@code Consumer} providing access to the {@link OAuth2AuthorizationCodeRequestAuthenticationContext} and is responsible for validating specific OAuth 2.0 Authorization Request parameters - * @since 0.4.0 - */ - public void setAuthenticationValidator(Consumer authenticationValidator) { - Assert.notNull(authenticationValidator, "authenticationValidator cannot be null"); - this.authenticationValidator = authenticationValidator; - } - - /** - * Sets the {@code Consumer} providing access to the {@link OAuth2AuthorizationConsentAuthenticationContext} - * containing an {@link OAuth2AuthorizationConsent.Builder} and additional context information. - * - *

- * The following context attributes are available: - *

    - *
  • The {@link OAuth2AuthorizationConsent.Builder} used to build the authorization consent - * prior to {@link OAuth2AuthorizationConsentService#save(OAuth2AuthorizationConsent)}.
  • - *
  • The {@link Authentication} of type - * {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.
  • - *
  • The {@link RegisteredClient} associated with the authorization request.
  • - *
  • The {@link OAuth2Authorization} associated with the state token presented in the - * authorization consent request.
  • - *
  • The {@link OAuth2AuthorizationRequest} associated with the authorization consent request.
  • - *
- * - * @param authorizationConsentCustomizer the {@code Consumer} providing access to the - * {@link OAuth2AuthorizationConsentAuthenticationContext} containing an {@link OAuth2AuthorizationConsent.Builder} - */ - public void setAuthorizationConsentCustomizer(Consumer authorizationConsentCustomizer) { - Assert.notNull(authorizationConsentCustomizer, "authorizationConsentCustomizer cannot be null"); - this.authorizationConsentCustomizer = authorizationConsentCustomizer; - } - - private Authentication authenticateAuthorizationRequest(Authentication authentication) throws AuthenticationException { - OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = - (OAuth2AuthorizationCodeRequestAuthenticationToken) authentication; - RegisteredClient registeredClient = this.registeredClientRepository.findByClientId( authorizationCodeRequestAuthentication.getClientId()); if (registeredClient == null) { @@ -234,12 +161,8 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen Set currentAuthorizedScopes = currentAuthorizationConsent != null ? currentAuthorizationConsent.getScopes() : null; - return OAuth2AuthorizationCodeRequestAuthenticationToken.with(registeredClient.getClientId(), principal) - .authorizationUri(authorizationRequest.getAuthorizationUri()) - .scopes(currentAuthorizedScopes) - .state(state) - .consentRequired(true) - .build(); + return new OAuth2AuthorizationConsentAuthenticationToken(authorizationRequest.getAuthorizationUri(), + registeredClient.getClientId(), principal, state, currentAuthorizedScopes, null); } OAuth2TokenContext tokenContext = createAuthorizationCodeTokenContext( @@ -262,136 +185,42 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen 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(); + return new OAuth2AuthorizationCodeRequestAuthenticationToken(authorizationRequest.getAuthorizationUri(), + registeredClient.getClientId(), principal, authorizationCode, redirectUri, + authorizationRequest.getState(), authorizationRequest.getScopes()); } - 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 requestedScopes = authorizationRequest.getScopes(); - Set 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 currentAuthorizedScopes = currentAuthorizationConsent != null ? - currentAuthorizationConsent.getScopes() : Collections.emptySet(); - - if (!currentAuthorizedScopes.isEmpty()) { - for (String requestedScope : requestedScopes) { - if (currentAuthorizedScopes.contains(requestedScope)) { - authorizedScopes.add(requestedScope); - } - } - } - - if (!authorizedScopes.isEmpty() && requestedScopes.contains(OidcScopes.OPENID)) { - // 'openid' scope is auto-approved as it does not require consent - authorizedScopes.add(OidcScopes.OPENID); - } - - OAuth2AuthorizationConsent.Builder authorizationConsentBuilder; - if (currentAuthorizationConsent != null) { - authorizationConsentBuilder = OAuth2AuthorizationConsent.from(currentAuthorizationConsent); - } else { - authorizationConsentBuilder = OAuth2AuthorizationConsent.withId( - authorization.getRegisteredClientId(), authorization.getPrincipalName()); - } - authorizedScopes.forEach(authorizationConsentBuilder::scope); - - if (this.authorizationConsentCustomizer != null) { - // @formatter:off - OAuth2AuthorizationConsentAuthenticationContext authorizationConsentAuthenticationContext = - OAuth2AuthorizationConsentAuthenticationContext.with(authorizationCodeRequestAuthentication) - .authorizationConsent(authorizationConsentBuilder) - .registeredClient(registeredClient) - .authorization(authorization) - .authorizationRequest(authorizationRequest) - .build(); - // @formatter:on - this.authorizationConsentCustomizer.accept(authorizationConsentAuthenticationContext); - } - - Set authorities = new HashSet<>(); - authorizationConsentBuilder.authorities(authorities::addAll); - - if (authorities.isEmpty()) { - // Authorization consent denied (or revoked) - if (currentAuthorizationConsent != null) { - this.authorizationConsentService.remove(currentAuthorizationConsent); - } - this.authorizationService.remove(authorization); - throwError(OAuth2ErrorCodes.ACCESS_DENIED, OAuth2ParameterNames.CLIENT_ID, - authorizationCodeRequestAuthentication, registeredClient, authorizationRequest); - } - - OAuth2AuthorizationConsent authorizationConsent = authorizationConsentBuilder.build(); - if (!authorizationConsent.equals(currentAuthorizationConsent)) { - this.authorizationConsentService.save(authorizationConsent); - } - - OAuth2TokenContext tokenContext = createAuthorizationCodeTokenContext( - authorizationCodeRequestAuthentication, registeredClient, authorization, authorizedScopes); - OAuth2AuthorizationCode authorizationCode = this.authorizationCodeGenerator.generate(tokenContext); - if (authorizationCode == null) { - OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, - "The token generator failed to generate the authorization code.", ERROR_URI); - throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null); - } - - OAuth2Authorization updatedAuthorization = OAuth2Authorization.from(authorization) - .authorizedScopes(authorizedScopes) - .token(authorizationCode) - .attributes(attrs -> { - attrs.remove(OAuth2ParameterNames.STATE); - }) - .build(); - this.authorizationService.save(updatedAuthorization); + @Override + public boolean supports(Class authentication) { + return OAuth2AuthorizationCodeRequestAuthenticationToken.class.isAssignableFrom(authentication); + } - String redirectUri = authorizationRequest.getRedirectUri(); - if (!StringUtils.hasText(redirectUri)) { - redirectUri = registeredClient.getRedirectUris().iterator().next(); - } + /** + * Sets the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode}. + * + * @param authorizationCodeGenerator the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode} + * @since 0.2.3 + */ + public void setAuthorizationCodeGenerator(OAuth2TokenGenerator authorizationCodeGenerator) { + Assert.notNull(authorizationCodeGenerator, "authorizationCodeGenerator cannot be null"); + this.authorizationCodeGenerator = authorizationCodeGenerator; + } - return OAuth2AuthorizationCodeRequestAuthenticationToken.with(registeredClient.getClientId(), principal) - .authorizationUri(authorizationRequest.getAuthorizationUri()) - .redirectUri(redirectUri) - .scopes(authorizedScopes) - .state(authorizationRequest.getState()) - .authorizationCode(authorizationCode) - .build(); + /** + * Sets the {@code Consumer} providing access to the {@link OAuth2AuthorizationCodeRequestAuthenticationContext} + * and is responsible for validating specific OAuth 2.0 Authorization Request parameters + * associated in the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}. + * The default authentication validator is {@link OAuth2AuthorizationCodeRequestAuthenticationValidator}. + * + *

+ * NOTE: The authentication validator MUST throw {@link OAuth2AuthorizationCodeRequestAuthenticationException} if validation fails. + * + * @param authenticationValidator the {@code Consumer} providing access to the {@link OAuth2AuthorizationCodeRequestAuthenticationContext} and is responsible for validating specific OAuth 2.0 Authorization Request parameters + * @since 0.4.0 + */ + public void setAuthenticationValidator(Consumer authenticationValidator) { + Assert.notNull(authenticationValidator, "authenticationValidator cannot be null"); + this.authenticationValidator = authenticationValidator; } private static OAuth2Authorization.Builder authorizationBuilder(RegisteredClient registeredClient, Authentication principal, @@ -454,14 +283,7 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen 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, ERROR_URI, - authorizationCodeRequestAuthentication, registeredClient, authorizationRequest); + throwError(errorCode, parameterName, ERROR_URI, authorizationCodeRequestAuthentication, registeredClient, null); } private static void throwError(String errorCode, String parameterName, String errorUri, @@ -475,30 +297,19 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication, RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) { - boolean redirectOnError = true; + String redirectUri = resolveRedirectUri(authorizationRequest, registeredClient); if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_REQUEST) && (parameterName.equals(OAuth2ParameterNames.CLIENT_ID) || parameterName.equals(OAuth2ParameterNames.STATE))) { - redirectOnError = false; + redirectUri = null; // Prevent redirects } - 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(); - } else if (!redirectOnError && StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) { - authorizationCodeRequestAuthenticationResult = from(authorizationCodeRequestAuthentication) - .redirectUri(null) // Prevent redirects - .build(); - } - - authorizationCodeRequestAuthenticationResult.setAuthenticated(authorizationCodeRequestAuthentication.isAuthenticated()); + OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult = + new OAuth2AuthorizationCodeRequestAuthenticationToken( + authorizationCodeRequestAuthentication.getAuthorizationUri(), authorizationCodeRequestAuthentication.getClientId(), + (Authentication) authorizationCodeRequestAuthentication.getPrincipal(), redirectUri, + authorizationCodeRequestAuthentication.getState(), authorizationCodeRequestAuthentication.getScopes(), + authorizationCodeRequestAuthentication.getAdditionalParameters()); throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, authorizationCodeRequestAuthenticationResult); } @@ -513,32 +324,4 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen 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()) - .authorizationCode(authorizationCodeRequestAuthentication.getAuthorizationCode()); - } - - private static class OAuth2AuthorizationCodeGenerator implements OAuth2TokenGenerator { - private final StringKeyGenerator authorizationCodeGenerator = - new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96); - - @Nullable - @Override - public OAuth2AuthorizationCode generate(OAuth2TokenContext context) { - if (context.getTokenType() == null || - !OAuth2ParameterNames.CODE.equals(context.getTokenType().getValue())) { - return null; - } - Instant issuedAt = Instant.now(); - Instant expiresAt = issuedAt.plus(context.getRegisteredClient().getTokenSettings().getAuthorizationCodeTimeToLive()); - return new OAuth2AuthorizationCode(this.authorizationCodeGenerator.generateKey(), issuedAt, expiresAt); - } - - } - } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationToken.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationToken.java index 71f3a630..a7fab020 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationToken.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationToken.java @@ -15,45 +15,104 @@ */ 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.server.authorization.OAuth2AuthorizationCode; import org.springframework.security.oauth2.server.authorization.util.SpringAuthorizationServerVersion; import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; /** - * An {@link Authentication} implementation for the OAuth 2.0 Authorization Request (and Consent) + * An {@link Authentication} implementation for the OAuth 2.0 Authorization Request * used in the Authorization Code Grant. * * @author Joe Grandja * @since 0.1.2 * @see OAuth2AuthorizationCodeRequestAuthenticationProvider + * @see OAuth2AuthorizationConsentAuthenticationProvider */ -public final class OAuth2AuthorizationCodeRequestAuthenticationToken extends AbstractAuthenticationToken { +public class OAuth2AuthorizationCodeRequestAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID; - private String authorizationUri; - private String clientId; - private Authentication principal; - private String redirectUri; - private Set scopes; - private String state; - private Map additionalParameters; - private boolean consentRequired; - private boolean consent; - private OAuth2AuthorizationCode authorizationCode; + private final String authorizationUri; + private final String clientId; + private final Authentication principal; + private final String redirectUri; + private final String state; + private final Set scopes; + private final Map additionalParameters; + private final OAuth2AuthorizationCode authorizationCode; - private OAuth2AuthorizationCodeRequestAuthenticationToken() { + /** + * Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationToken} using the provided parameters. + * + * @param authorizationUri the authorization URI + * @param clientId the client identifier + * @param principal the {@code Principal} (Resource Owner) + * @param redirectUri the redirect uri + * @param state the state + * @param scopes the requested scope(s) + * @param additionalParameters the additional parameters + * @since 0.4.0 + */ + public OAuth2AuthorizationCodeRequestAuthenticationToken(String authorizationUri, String clientId, Authentication principal, + @Nullable String redirectUri, @Nullable String state, @Nullable Set scopes, @Nullable Map additionalParameters) { super(Collections.emptyList()); + Assert.hasText(authorizationUri, "authorizationUri cannot be empty"); + Assert.hasText(clientId, "clientId cannot be empty"); + Assert.notNull(principal, "principal cannot be null"); + this.authorizationUri = authorizationUri; + this.clientId = clientId; + this.principal = principal; + this.redirectUri = redirectUri; + this.state = state; + this.scopes = Collections.unmodifiableSet( + scopes != null ? + new HashSet<>(scopes) : + Collections.emptySet()); + this.additionalParameters = Collections.unmodifiableMap( + additionalParameters != null ? + new HashMap<>(additionalParameters) : + Collections.emptyMap()); + this.authorizationCode = null; + } + + /** + * Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationToken} using the provided parameters. + * + * @param authorizationUri the authorization URI + * @param clientId the client identifier + * @param principal the {@code Principal} (Resource Owner) + * @param authorizationCode the {@link OAuth2AuthorizationCode} + * @param redirectUri the redirect uri + * @param state the state + * @param scopes the authorized scope(s) + * @since 0.4.0 + */ + public OAuth2AuthorizationCodeRequestAuthenticationToken(String authorizationUri, String clientId, Authentication principal, + OAuth2AuthorizationCode authorizationCode, @Nullable String redirectUri, @Nullable String state, @Nullable Set scopes) { + super(Collections.emptyList()); + Assert.hasText(authorizationUri, "authorizationUri cannot be empty"); + Assert.hasText(clientId, "clientId cannot be empty"); + Assert.notNull(principal, "principal cannot be null"); + Assert.notNull(authorizationCode, "authorizationCode cannot be null"); + this.authorizationUri = authorizationUri; + this.clientId = clientId; + this.principal = principal; + this.authorizationCode = authorizationCode; + this.redirectUri = redirectUri; + this.state = state; + this.scopes = Collections.unmodifiableSet( + scopes != null ? + new HashSet<>(scopes) : + Collections.emptySet()); + this.additionalParameters = Collections.emptyMap(); + setAuthenticated(true); } @Override @@ -94,15 +153,6 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationToken extends Abs 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 getScopes() { - return this.scopes; - } - /** * Returns the state. * @@ -114,31 +164,21 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationToken extends Abs } /** - * Returns the additional parameters. - * - * @return the additional parameters - */ - public Map getAdditionalParameters() { - return this.additionalParameters; - } - - /** - * Returns {@code true} if authorization consent is required, {@code false} otherwise. + * Returns the requested (or authorized) scope(s). * - * @return {@code true} if authorization consent is required, {@code false} otherwise + * @return the requested (or authorized) scope(s), or an empty {@code Set} if not available */ - public boolean isConsentRequired() { - return this.consentRequired; + public Set getScopes() { + return this.scopes; } /** - * Returns {@code true} if this {@code Authentication} represents an authorization consent request, - * {@code false} otherwise. + * Returns the additional parameters. * - * @return {@code true} if this {@code Authentication} represents an authorization consent request, {@code false} otherwise + * @return the additional parameters, or an empty {@code Map} if not available */ - public boolean isConsent() { - return this.consent; + public Map getAdditionalParameters() { + return this.additionalParameters; } /** @@ -151,170 +191,4 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationToken extends Abs 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 = SpringAuthorizationServerVersion.SERIAL_VERSION_UID; - private String authorizationUri; - private String clientId; - private Authentication principal; - private String redirectUri; - private Set scopes; - private String state; - private Map 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 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 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; - } - - } - } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationValidator.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationValidator.java index c19f6a97..43e1c237 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationValidator.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationValidator.java @@ -189,38 +189,23 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationValidator impleme OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication, RegisteredClient registeredClient) { - boolean redirectOnError = true; + String redirectUri = StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri()) ? + authorizationCodeRequestAuthentication.getRedirectUri() : + registeredClient.getRedirectUris().iterator().next(); if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_REQUEST) && parameterName.equals(OAuth2ParameterNames.REDIRECT_URI)) { - redirectOnError = false; + redirectUri = null; // Prevent redirects } - OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult = authorizationCodeRequestAuthentication; - - if (redirectOnError && !StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) { - String redirectUri = registeredClient.getRedirectUris().iterator().next(); - authorizationCodeRequestAuthenticationResult = from(authorizationCodeRequestAuthentication) - .redirectUri(redirectUri) - .build(); - } else if (!redirectOnError && StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) { - authorizationCodeRequestAuthenticationResult = from(authorizationCodeRequestAuthentication) - .redirectUri(null) // Prevent redirects - .build(); - } - - authorizationCodeRequestAuthenticationResult.setAuthenticated(authorizationCodeRequestAuthentication.isAuthenticated()); + OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult = + new OAuth2AuthorizationCodeRequestAuthenticationToken( + authorizationCodeRequestAuthentication.getAuthorizationUri(), authorizationCodeRequestAuthentication.getClientId(), + (Authentication) authorizationCodeRequestAuthentication.getPrincipal(), redirectUri, + authorizationCodeRequestAuthentication.getState(), authorizationCodeRequestAuthentication.getScopes(), + authorizationCodeRequestAuthentication.getAdditionalParameters()); + authorizationCodeRequestAuthenticationResult.setAuthenticated(true); throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, authorizationCodeRequestAuthenticationResult); } - 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()) - .authorizationCode(authorizationCodeRequestAuthentication.getAuthorizationCode()); - } - } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationContext.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationContext.java index e9986e60..c8e572c7 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationContext.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationContext.java @@ -36,7 +36,7 @@ import org.springframework.util.Assert; * @since 0.2.1 * @see OAuth2AuthenticationContext * @see OAuth2AuthorizationConsent - * @see OAuth2AuthorizationCodeRequestAuthenticationProvider#setAuthorizationConsentCustomizer(Consumer) + * @see OAuth2AuthorizationConsentAuthenticationProvider#setAuthorizationConsentCustomizer(Consumer) */ public final class OAuth2AuthorizationConsentAuthenticationContext implements OAuth2AuthenticationContext { private final Map context; @@ -95,12 +95,12 @@ public final class OAuth2AuthorizationConsentAuthenticationContext implements OA } /** - * Constructs a new {@link Builder} with the provided {@link OAuth2AuthorizationCodeRequestAuthenticationToken}. + * Constructs a new {@link Builder} with the provided {@link OAuth2AuthorizationConsentAuthenticationToken}. * - * @param authentication the {@link OAuth2AuthorizationCodeRequestAuthenticationToken} + * @param authentication the {@link OAuth2AuthorizationConsentAuthenticationToken} * @return the {@link Builder} */ - public static Builder with(OAuth2AuthorizationCodeRequestAuthenticationToken authentication) { + public static Builder with(OAuth2AuthorizationConsentAuthenticationToken authentication) { return new Builder(authentication); } @@ -109,7 +109,7 @@ public final class OAuth2AuthorizationConsentAuthenticationContext implements OA */ public static final class Builder extends AbstractBuilder { - private Builder(OAuth2AuthorizationCodeRequestAuthenticationToken authentication) { + private Builder(OAuth2AuthorizationConsentAuthenticationToken authentication) { super(authentication); } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationProvider.java new file mode 100644 index 00000000..46199dfa --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationProvider.java @@ -0,0 +1,316 @@ +/* + * Copyright 2020-2022 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.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Consumer; + +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.core.GrantedAuthority; +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.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.oidc.OidcScopes; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode; +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.OAuth2TokenType; +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.context.AuthorizationServerContextHolder; +import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * An {@link AuthenticationProvider} implementation for the OAuth 2.0 Authorization Consent + * used in the Authorization Code Grant. + * + * @author Joe Grandja + * @since 0.4.0 + * @see OAuth2AuthorizationConsentAuthenticationToken + * @see OAuth2AuthorizationConsent + * @see OAuth2AuthorizationCodeRequestAuthenticationProvider + * @see RegisteredClientRepository + * @see OAuth2AuthorizationService + * @see OAuth2AuthorizationConsentService + */ +public final class OAuth2AuthorizationConsentAuthenticationProvider implements AuthenticationProvider { + private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1"; + private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE); + private final RegisteredClientRepository registeredClientRepository; + private final OAuth2AuthorizationService authorizationService; + private final OAuth2AuthorizationConsentService authorizationConsentService; + private OAuth2TokenGenerator authorizationCodeGenerator = new OAuth2AuthorizationCodeGenerator(); + private Consumer authorizationConsentCustomizer; + + /** + * Constructs an {@code OAuth2AuthorizationConsentAuthenticationProvider} using the provided parameters. + * + * @param registeredClientRepository the repository of registered clients + * @param authorizationService the authorization service + * @param authorizationConsentService the authorization consent service + */ + public OAuth2AuthorizationConsentAuthenticationProvider(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 { + OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication = + (OAuth2AuthorizationConsentAuthenticationToken) authentication; + + OAuth2Authorization authorization = this.authorizationService.findByToken( + authorizationConsentAuthentication.getState(), STATE_TOKEN_TYPE); + if (authorization == null) { + throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE, + authorizationConsentAuthentication, null, null); + } + + // The 'in-flight' authorization must be associated to the current principal + Authentication principal = (Authentication) authorizationConsentAuthentication.getPrincipal(); + if (!isPrincipalAuthenticated(principal) || !principal.getName().equals(authorization.getPrincipalName())) { + throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE, + authorizationConsentAuthentication, null, null); + } + + RegisteredClient registeredClient = this.registeredClientRepository.findByClientId( + authorizationConsentAuthentication.getClientId()); + if (registeredClient == null || !registeredClient.getId().equals(authorization.getRegisteredClientId())) { + throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID, + authorizationConsentAuthentication, registeredClient, null); + } + + OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName()); + Set requestedScopes = authorizationRequest.getScopes(); + Set authorizedScopes = new HashSet<>(authorizationConsentAuthentication.getScopes()); + if (!requestedScopes.containsAll(authorizedScopes)) { + throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE, + authorizationConsentAuthentication, registeredClient, authorizationRequest); + } + + OAuth2AuthorizationConsent currentAuthorizationConsent = this.authorizationConsentService.findById( + authorization.getRegisteredClientId(), authorization.getPrincipalName()); + Set currentAuthorizedScopes = currentAuthorizationConsent != null ? + currentAuthorizationConsent.getScopes() : Collections.emptySet(); + + if (!currentAuthorizedScopes.isEmpty()) { + for (String requestedScope : requestedScopes) { + if (currentAuthorizedScopes.contains(requestedScope)) { + authorizedScopes.add(requestedScope); + } + } + } + + if (!authorizedScopes.isEmpty() && requestedScopes.contains(OidcScopes.OPENID)) { + // 'openid' scope is auto-approved as it does not require consent + authorizedScopes.add(OidcScopes.OPENID); + } + + OAuth2AuthorizationConsent.Builder authorizationConsentBuilder; + if (currentAuthorizationConsent != null) { + authorizationConsentBuilder = OAuth2AuthorizationConsent.from(currentAuthorizationConsent); + } else { + authorizationConsentBuilder = OAuth2AuthorizationConsent.withId( + authorization.getRegisteredClientId(), authorization.getPrincipalName()); + } + authorizedScopes.forEach(authorizationConsentBuilder::scope); + + if (this.authorizationConsentCustomizer != null) { + // @formatter:off + OAuth2AuthorizationConsentAuthenticationContext authorizationConsentAuthenticationContext = + OAuth2AuthorizationConsentAuthenticationContext.with(authorizationConsentAuthentication) + .authorizationConsent(authorizationConsentBuilder) + .registeredClient(registeredClient) + .authorization(authorization) + .authorizationRequest(authorizationRequest) + .build(); + // @formatter:on + this.authorizationConsentCustomizer.accept(authorizationConsentAuthenticationContext); + } + + Set authorities = new HashSet<>(); + authorizationConsentBuilder.authorities(authorities::addAll); + + if (authorities.isEmpty()) { + // Authorization consent denied (or revoked) + if (currentAuthorizationConsent != null) { + this.authorizationConsentService.remove(currentAuthorizationConsent); + } + this.authorizationService.remove(authorization); + throwError(OAuth2ErrorCodes.ACCESS_DENIED, OAuth2ParameterNames.CLIENT_ID, + authorizationConsentAuthentication, registeredClient, authorizationRequest); + } + + OAuth2AuthorizationConsent authorizationConsent = authorizationConsentBuilder.build(); + if (!authorizationConsent.equals(currentAuthorizationConsent)) { + this.authorizationConsentService.save(authorizationConsent); + } + + OAuth2TokenContext tokenContext = createAuthorizationCodeTokenContext( + authorizationConsentAuthentication, registeredClient, authorization, authorizedScopes); + OAuth2AuthorizationCode authorizationCode = this.authorizationCodeGenerator.generate(tokenContext); + if (authorizationCode == null) { + OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, + "The token generator failed to generate the authorization code.", ERROR_URI); + throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null); + } + + OAuth2Authorization updatedAuthorization = OAuth2Authorization.from(authorization) + .authorizedScopes(authorizedScopes) + .token(authorizationCode) + .attributes(attrs -> { + attrs.remove(OAuth2ParameterNames.STATE); + }) + .build(); + this.authorizationService.save(updatedAuthorization); + + String redirectUri = authorizationRequest.getRedirectUri(); + if (!StringUtils.hasText(redirectUri)) { + redirectUri = registeredClient.getRedirectUris().iterator().next(); + } + + return new OAuth2AuthorizationCodeRequestAuthenticationToken( + authorizationRequest.getAuthorizationUri(), registeredClient.getClientId(), principal, authorizationCode, + redirectUri, authorizationRequest.getState(), authorizedScopes); + } + + @Override + public boolean supports(Class authentication) { + return OAuth2AuthorizationConsentAuthenticationToken.class.isAssignableFrom(authentication); + } + + /** + * Sets the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode}. + * + * @param authorizationCodeGenerator the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode} + */ + public void setAuthorizationCodeGenerator(OAuth2TokenGenerator authorizationCodeGenerator) { + Assert.notNull(authorizationCodeGenerator, "authorizationCodeGenerator cannot be null"); + this.authorizationCodeGenerator = authorizationCodeGenerator; + } + + /** + * Sets the {@code Consumer} providing access to the {@link OAuth2AuthorizationConsentAuthenticationContext} + * containing an {@link OAuth2AuthorizationConsent.Builder} and additional context information. + * + *

+ * The following context attributes are available: + *

    + *
  • The {@link OAuth2AuthorizationConsent.Builder} used to build the authorization consent + * prior to {@link OAuth2AuthorizationConsentService#save(OAuth2AuthorizationConsent)}.
  • + *
  • The {@link Authentication} of type + * {@link OAuth2AuthorizationConsentAuthenticationToken}.
  • + *
  • The {@link RegisteredClient} associated with the authorization request.
  • + *
  • The {@link OAuth2Authorization} associated with the state token presented in the + * authorization consent request.
  • + *
  • The {@link OAuth2AuthorizationRequest} associated with the authorization consent request.
  • + *
+ * + * @param authorizationConsentCustomizer the {@code Consumer} providing access to the + * {@link OAuth2AuthorizationConsentAuthenticationContext} containing an {@link OAuth2AuthorizationConsent.Builder} + */ + public void setAuthorizationConsentCustomizer(Consumer authorizationConsentCustomizer) { + Assert.notNull(authorizationConsentCustomizer, "authorizationConsentCustomizer cannot be null"); + this.authorizationConsentCustomizer = authorizationConsentCustomizer; + } + + private static OAuth2TokenContext createAuthorizationCodeTokenContext( + OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication, + RegisteredClient registeredClient, OAuth2Authorization authorization, Set authorizedScopes) { + + // @formatter:off + return DefaultOAuth2TokenContext.builder() + .registeredClient(registeredClient) + .principal((Authentication) authorizationConsentAuthentication.getPrincipal()) + .authorization(authorization) + .authorizationServerContext(AuthorizationServerContextHolder.getContext()) + .tokenType(new OAuth2TokenType(OAuth2ParameterNames.CODE)) + .authorizedScopes(authorizedScopes) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .authorizationGrant(authorizationConsentAuthentication) + .build(); + // @formatter:on + } + + private static boolean isPrincipalAuthenticated(Authentication principal) { + return principal != null && + !AnonymousAuthenticationToken.class.isAssignableFrom(principal.getClass()) && + principal.isAuthenticated(); + } + + private static void throwError(String errorCode, String parameterName, + OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication, + RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) { + OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, ERROR_URI); + throwError(error, parameterName, authorizationConsentAuthentication, registeredClient, authorizationRequest); + } + + private static void throwError(OAuth2Error error, String parameterName, + OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication, + RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) { + + String redirectUri = resolveRedirectUri(authorizationRequest, registeredClient); + if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_REQUEST) && + (parameterName.equals(OAuth2ParameterNames.CLIENT_ID) || + parameterName.equals(OAuth2ParameterNames.STATE))) { + redirectUri = null; // Prevent redirects + } + + String state = authorizationRequest != null ? + authorizationRequest.getState() : + authorizationConsentAuthentication.getState(); + Set requestedScopes = authorizationRequest != null ? + authorizationRequest.getScopes() : + authorizationConsentAuthentication.getScopes(); + + OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult = + new OAuth2AuthorizationCodeRequestAuthenticationToken( + authorizationConsentAuthentication.getAuthorizationUri(), authorizationConsentAuthentication.getClientId(), + (Authentication) authorizationConsentAuthentication.getPrincipal(), redirectUri, + state, requestedScopes, null); + + 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; + } + +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationToken.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationToken.java new file mode 100644 index 00000000..dd068d63 --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationToken.java @@ -0,0 +1,135 @@ +/* + * Copyright 2020-2022 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.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.lang.Nullable; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.server.authorization.util.SpringAuthorizationServerVersion; +import org.springframework.util.Assert; + +/** + * An {@link Authentication} implementation for the OAuth 2.0 Authorization Consent + * used in the Authorization Code Grant. + * + * @author Joe Grandja + * @since 0.4.0 + * @see OAuth2AuthorizationConsentAuthenticationProvider + * @see OAuth2AuthorizationCodeRequestAuthenticationProvider + */ +public class OAuth2AuthorizationConsentAuthenticationToken extends AbstractAuthenticationToken { + private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID; + private final String authorizationUri; + private final String clientId; + private final Authentication principal; + private final String state; + private final Set scopes; + private final Map additionalParameters; + + /** + * Constructs an {@code OAuth2AuthorizationConsentAuthenticationToken} using the provided parameters. + * + * @param authorizationUri the authorization URI + * @param clientId the client identifier + * @param principal the {@code Principal} (Resource Owner) + * @param state the state + * @param scopes the requested (or authorized) scope(s) + * @param additionalParameters the additional parameters + */ + public OAuth2AuthorizationConsentAuthenticationToken(String authorizationUri, String clientId, Authentication principal, + String state, @Nullable Set scopes, @Nullable Map additionalParameters) { + super(Collections.emptyList()); + Assert.hasText(authorizationUri, "authorizationUri cannot be empty"); + Assert.hasText(clientId, "clientId cannot be empty"); + Assert.notNull(principal, "principal cannot be null"); + Assert.hasText(state, "state cannot be empty"); + this.authorizationUri = authorizationUri; + this.clientId = clientId; + this.principal = principal; + this.state = state; + this.scopes = Collections.unmodifiableSet( + scopes != null ? + new HashSet<>(scopes) : + Collections.emptySet()); + this.additionalParameters = Collections.unmodifiableMap( + additionalParameters != null ? + new HashMap<>(additionalParameters) : + Collections.emptyMap()); + setAuthenticated(true); + } + + @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 state. + * + * @return the state + */ + public String getState() { + return this.state; + } + + /** + * Returns the requested (or authorized) scope(s). + * + * @return the requested (or authorized) scope(s), or an empty {@code Set} if not available + */ + public Set getScopes() { + return this.scopes; + } + + /** + * Returns the additional parameters. + * + * @return the additional parameters, or an empty {@code Map} if not available + */ + public Map getAdditionalParameters() { + return this.additionalParameters; + } + +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationEndpointConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationEndpointConfigurer.java index 99ec36f7..089183c1 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationEndpointConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationEndpointConfigurer.java @@ -31,10 +31,13 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResp import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationProvider; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationProvider; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationToken; import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter; import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter; import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeRequestAuthenticationConverter; +import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationConsentAuthenticationConverter; import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; @@ -72,7 +75,8 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C /** * Adds an {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or Consent) from {@link HttpServletRequest} - * to an instance of {@link OAuth2AuthorizationCodeRequestAuthenticationToken} used for authenticating the request. + * to an instance of {@link OAuth2AuthorizationCodeRequestAuthenticationToken} or {@link OAuth2AuthorizationConsentAuthenticationToken} + * used for authenticating the request. * * @param authorizationRequestConverter an {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or Consent) from {@link HttpServletRequest} * @return the {@link OAuth2AuthorizationEndpointConfigurer} for further configuration @@ -170,7 +174,7 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C * *
    *
  • It must be an HTTP POST
  • - *
  • It must be submitted to {@link AuthorizationServerSettings#getAuthorizationEndpoint()} ()}
  • + *
  • It must be submitted to {@link AuthorizationServerSettings#getAuthorizationEndpoint()}
  • *
  • It must include the received {@code client_id} as an HTTP parameter
  • *
  • It must include the received {@code state} as an HTTP parameter
  • *
  • It must include the list of {@code scope}s the {@code Resource Owner} @@ -242,6 +246,7 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C List authenticationConverters = new ArrayList<>(); authenticationConverters.add(new OAuth2AuthorizationCodeRequestAuthenticationConverter()); + authenticationConverters.add(new OAuth2AuthorizationConsentAuthenticationConverter()); return authenticationConverters; } @@ -256,6 +261,13 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C OAuth2ConfigurerUtils.getAuthorizationConsentService(httpSecurity)); authenticationProviders.add(authorizationCodeRequestAuthenticationProvider); + OAuth2AuthorizationConsentAuthenticationProvider authorizationConsentAuthenticationProvider = + new OAuth2AuthorizationConsentAuthenticationProvider( + OAuth2ConfigurerUtils.getRegisteredClientRepository(httpSecurity), + OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity), + OAuth2ConfigurerUtils.getAuthorizationConsentService(httpSecurity)); + authenticationProviders.add(authorizationConsentAuthenticationProvider); + return authenticationProviders; } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilter.java index 91ec1317..6f1bc191 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilter.java @@ -17,6 +17,7 @@ package org.springframework.security.oauth2.server.authorization.web; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.HashSet; import java.util.Set; @@ -28,6 +29,7 @@ import javax.servlet.http.HttpServletResponse; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; @@ -40,7 +42,11 @@ import org.springframework.security.oauth2.core.oidc.OidcScopes; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationProvider; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationProvider; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter; import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeRequestAuthenticationConverter; +import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationConsentAuthenticationConverter; import org.springframework.security.web.DefaultRedirectStrategy; import org.springframework.security.web.RedirectStrategy; import org.springframework.security.web.authentication.AuthenticationConverter; @@ -61,7 +67,7 @@ import org.springframework.web.util.UriComponentsBuilder; /** * A {@code Filter} for the OAuth 2.0 Authorization Code Grant, - * which handles the processing of the OAuth 2.0 Authorization Request (and Consent). + * which handles the processing of the OAuth 2.0 Authorization Request and Consent. * * @author Joe Grandja * @author Paurav Munshi @@ -71,6 +77,7 @@ import org.springframework.web.util.UriComponentsBuilder; * @since 0.0.1 * @see AuthenticationManager * @see OAuth2AuthorizationCodeRequestAuthenticationProvider + * @see OAuth2AuthorizationConsentAuthenticationProvider * @see Section 4.1 Authorization Code Grant * @see Section 4.1.1 Authorization Request * @see Section 4.1.2 Authorization Response @@ -110,7 +117,10 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte Assert.hasText(authorizationEndpointUri, "authorizationEndpointUri cannot be empty"); this.authenticationManager = authenticationManager; this.authorizationEndpointMatcher = createDefaultRequestMatcher(authorizationEndpointUri); - this.authenticationConverter = new OAuth2AuthorizationCodeRequestAuthenticationConverter(); + this.authenticationConverter = new DelegatingAuthenticationConverter( + Arrays.asList( + new OAuth2AuthorizationCodeRequestAuthenticationConverter(), + new OAuth2AuthorizationConsentAuthenticationConverter())); } private static RequestMatcher createDefaultRequestMatcher(String authorizationEndpointUri) { @@ -145,14 +155,14 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte } try { - OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = - (OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationConverter.convert(request); - authorizationCodeRequestAuthentication.setDetails(this.authenticationDetailsSource.buildDetails(request)); - - OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult = - (OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationManager.authenticate(authorizationCodeRequestAuthentication); + Authentication authentication = this.authenticationConverter.convert(request); + if (authentication instanceof AbstractAuthenticationToken) { + ((AbstractAuthenticationToken) authentication) + .setDetails(this.authenticationDetailsSource.buildDetails(request)); + } + Authentication authenticationResult = this.authenticationManager.authenticate(authentication); - if (!authorizationCodeRequestAuthenticationResult.isAuthenticated()) { + if (!authenticationResult.isAuthenticated()) { // If the Principal (Resource Owner) is not authenticated then // pass through the chain with the expectation that the authentication process // will commence via AuthenticationEntryPoint @@ -160,13 +170,15 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte return; } - if (authorizationCodeRequestAuthenticationResult.isConsentRequired()) { - sendAuthorizationConsent(request, response, authorizationCodeRequestAuthentication, authorizationCodeRequestAuthenticationResult); + if (authenticationResult instanceof OAuth2AuthorizationConsentAuthenticationToken) { + sendAuthorizationConsent(request, response, + (OAuth2AuthorizationCodeRequestAuthenticationToken) authentication, + (OAuth2AuthorizationConsentAuthenticationToken) authenticationResult); return; } this.authenticationSuccessHandler.onAuthenticationSuccess( - request, response, authorizationCodeRequestAuthenticationResult); + request, response, authenticationResult); } catch (OAuth2AuthenticationException ex) { this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex); @@ -186,7 +198,8 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte /** * Sets the {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or Consent) from {@link HttpServletRequest} - * to an instance of {@link OAuth2AuthorizationCodeRequestAuthenticationToken} used for authenticating the request. + * to an instance of {@link OAuth2AuthorizationCodeRequestAuthenticationToken} or {@link OAuth2AuthorizationConsentAuthenticationToken} + * used for authenticating the request. * * @param authenticationConverter the {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or Consent) from {@link HttpServletRequest} */ @@ -229,13 +242,13 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte private void sendAuthorizationConsent(HttpServletRequest request, HttpServletResponse response, OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication, - OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult) throws IOException { + OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication) throws IOException { - String clientId = authorizationCodeRequestAuthenticationResult.getClientId(); - Authentication principal = (Authentication) authorizationCodeRequestAuthenticationResult.getPrincipal(); + String clientId = authorizationConsentAuthentication.getClientId(); + Authentication principal = (Authentication) authorizationConsentAuthentication.getPrincipal(); Set requestedScopes = authorizationCodeRequestAuthentication.getScopes(); - Set authorizedScopes = authorizationCodeRequestAuthenticationResult.getScopes(); - String state = authorizationCodeRequestAuthenticationResult.getState(); + Set authorizedScopes = authorizationConsentAuthentication.getScopes(); + String state = authorizationConsentAuthentication.getState(); if (hasConsentUri()) { String redirectUri = UriComponentsBuilder.fromUriString(resolveConsentUri(request)) diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2AuthorizationCodeRequestAuthenticationConverter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2AuthorizationCodeRequestAuthenticationConverter.java index 1288d200..d8b6a55b 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2AuthorizationCodeRequestAuthenticationConverter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2AuthorizationCodeRequestAuthenticationConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the original author or authors. + * Copyright 2020-2022 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. @@ -43,7 +43,7 @@ import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; /** - * Attempts to extract an Authorization Request (or Consent) from {@link HttpServletRequest} + * Attempts to extract an Authorization Request from {@link HttpServletRequest} * for the OAuth 2.0 Authorization Code Grant and then converts it to * an {@link OAuth2AuthorizationCodeRequestAuthenticationToken} used for authenticating the request. * @@ -62,20 +62,19 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationConverter impleme @Override public Authentication convert(HttpServletRequest request) { + if (!"GET".equals(request.getMethod()) && !OIDC_REQUEST_MATCHER.matches(request)) { + return null; + } + MultiValueMap parameters = OAuth2EndpointUtils.getParameters(request); - boolean authorizationRequest = false; - if ("GET".equals(request.getMethod()) || OIDC_REQUEST_MATCHER.matches(request)) { - authorizationRequest = true; - - // response_type (REQUIRED) - String responseType = request.getParameter(OAuth2ParameterNames.RESPONSE_TYPE); - if (!StringUtils.hasText(responseType) || - parameters.get(OAuth2ParameterNames.RESPONSE_TYPE).size() != 1) { - throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.RESPONSE_TYPE); - } else if (!responseType.equals(OAuth2AuthorizationResponseType.CODE.getValue())) { - throwError(OAuth2ErrorCodes.UNSUPPORTED_RESPONSE_TYPE, OAuth2ParameterNames.RESPONSE_TYPE); - } + // response_type (REQUIRED) + String responseType = request.getParameter(OAuth2ParameterNames.RESPONSE_TYPE); + if (!StringUtils.hasText(responseType) || + parameters.get(OAuth2ParameterNames.RESPONSE_TYPE).size() != 1) { + throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.RESPONSE_TYPE); + } else if (!responseType.equals(OAuth2AuthorizationResponseType.CODE.getValue())) { + throwError(OAuth2ErrorCodes.UNSUPPORTED_RESPONSE_TYPE, OAuth2ParameterNames.RESPONSE_TYPE); } String authorizationUri = request.getRequestURL().toString(); @@ -101,37 +100,21 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationConverter impleme // scope (OPTIONAL) Set scopes = null; - if (authorizationRequest) { - String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE); - if (StringUtils.hasText(scope) && - parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) { - throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE); - } - if (StringUtils.hasText(scope)) { - scopes = new HashSet<>( - Arrays.asList(StringUtils.delimitedListToStringArray(scope, " "))); - } - } else { - // Consent request - if (parameters.containsKey(OAuth2ParameterNames.SCOPE)) { - scopes = new HashSet<>(parameters.get(OAuth2ParameterNames.SCOPE)); - } + String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE); + if (StringUtils.hasText(scope) && + parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) { + throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE); + } + if (StringUtils.hasText(scope)) { + scopes = new HashSet<>( + Arrays.asList(StringUtils.delimitedListToStringArray(scope, " "))); } - // state - // RECOMMENDED for Authorization Request + // state (RECOMMENDED) String state = parameters.getFirst(OAuth2ParameterNames.STATE); - if (authorizationRequest) { - if (StringUtils.hasText(state) && - parameters.get(OAuth2ParameterNames.STATE).size() != 1) { - throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE); - } - } else { - // REQUIRED for Authorization Consent Request - if (!StringUtils.hasText(state) || - parameters.get(OAuth2ParameterNames.STATE).size() != 1) { - throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE); - } + if (StringUtils.hasText(state) && + parameters.get(OAuth2ParameterNames.STATE).size() != 1) { + throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE); } // code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE) @@ -159,14 +142,8 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationConverter impleme } }); - return OAuth2AuthorizationCodeRequestAuthenticationToken.with(clientId, principal) - .authorizationUri(authorizationUri) - .redirectUri(redirectUri) - .scopes(scopes) - .state(state) - .additionalParameters(additionalParameters) - .consent(!authorizationRequest) - .build(); + return new OAuth2AuthorizationCodeRequestAuthenticationToken(authorizationUri, clientId, principal, + redirectUri, state, scopes, additionalParameters); } private static RequestMatcher createOidcRequestMatcher() { diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2AuthorizationConsentAuthenticationConverter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2AuthorizationConsentAuthenticationConverter.java new file mode 100644 index 00000000..6c618507 --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2AuthorizationConsentAuthenticationConverter.java @@ -0,0 +1,109 @@ +/* + * Copyright 2020-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.server.authorization.web.authentication; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter; +import org.springframework.security.web.authentication.AuthenticationConverter; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; + +/** + * Attempts to extract an Authorization Consent from {@link HttpServletRequest} + * for the OAuth 2.0 Authorization Code Grant and then converts it to + * an {@link OAuth2AuthorizationConsentAuthenticationToken} used for authenticating the request. + * + * @author Joe Grandja + * @since 0.4.0 + * @see AuthenticationConverter + * @see OAuth2AuthorizationConsentAuthenticationToken + * @see OAuth2AuthorizationEndpointFilter + */ +public final class OAuth2AuthorizationConsentAuthenticationConverter implements AuthenticationConverter { + private static final String DEFAULT_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1"; + private static final Authentication ANONYMOUS_AUTHENTICATION = new AnonymousAuthenticationToken( + "anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); + + @Override + public Authentication convert(HttpServletRequest request) { + if (!"POST".equals(request.getMethod()) || + request.getParameter(OAuth2ParameterNames.RESPONSE_TYPE) != null) { + return null; + } + + MultiValueMap parameters = OAuth2EndpointUtils.getParameters(request); + + String authorizationUri = request.getRequestURL().toString(); + + // client_id (REQUIRED) + String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID); + if (!StringUtils.hasText(clientId) || + parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) { + throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID); + } + + Authentication principal = SecurityContextHolder.getContext().getAuthentication(); + if (principal == null) { + principal = ANONYMOUS_AUTHENTICATION; + } + + // state (REQUIRED) + String state = parameters.getFirst(OAuth2ParameterNames.STATE); + if (!StringUtils.hasText(state) || + parameters.get(OAuth2ParameterNames.STATE).size() != 1) { + throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE); + } + + // scope (OPTIONAL) + Set scopes = null; + if (parameters.containsKey(OAuth2ParameterNames.SCOPE)) { + scopes = new HashSet<>(parameters.get(OAuth2ParameterNames.SCOPE)); + } + + Map additionalParameters = new HashMap<>(); + parameters.forEach((key, value) -> { + if (!key.equals(OAuth2ParameterNames.CLIENT_ID) && + !key.equals(OAuth2ParameterNames.STATE) && + !key.equals(OAuth2ParameterNames.SCOPE)) { + additionalParameters.put(key, value.get(0)); + } + }); + + return new OAuth2AuthorizationConsentAuthenticationToken(authorizationUri, clientId, principal, + state, scopes, additionalParameters); + } + + private static void throwError(String errorCode, String parameterName) { + OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, DEFAULT_ERROR_URI); + throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null); + } + +} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProviderTests.java index acaa546d..db7e2459 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProviderTests.java @@ -18,7 +18,6 @@ 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 java.util.function.Consumer; @@ -42,8 +41,6 @@ import org.springframework.security.oauth2.server.authorization.OAuth2Authorizat 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.OAuth2TokenType; -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; @@ -58,7 +55,6 @@ 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; @@ -69,7 +65,8 @@ import static org.mockito.Mockito.when; * @author Steve Riesenberg */ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests { - private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE); + private static final String AUTHORIZATION_URI = "https://provider.com/oauth2/authorize"; + private static final String STATE = "state"; private RegisteredClientRepository registeredClientRepository; private OAuth2AuthorizationService authorizationService; private OAuth2AuthorizationConsentService authorizationConsentService; @@ -132,19 +129,13 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests { .hasMessage("authenticationValidator cannot be null"); } - @Test - public void setAuthorizationConsentCustomizerWhenNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> this.authenticationProvider.setAuthorizationConsentCustomizer(null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("authorizationConsentCustomizer cannot be null"); - } - @Test public void authenticateWhenInvalidClientIdThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); OAuth2AuthorizationCodeRequestAuthenticationToken authentication = - authorizationCodeRequestAuthentication(registeredClient, this.principal) - .build(); + new OAuth2AuthorizationCodeRequestAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), null); assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) .isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class) .satisfies(ex -> @@ -160,9 +151,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests { when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) .thenReturn(registeredClient); OAuth2AuthorizationCodeRequestAuthenticationToken authentication = - authorizationCodeRequestAuthentication(registeredClient, this.principal) - .redirectUri("https:///invalid") - .build(); + new OAuth2AuthorizationCodeRequestAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + "https:///invalid", STATE, registeredClient.getScopes(), null); assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) .isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class) .satisfies(ex -> @@ -178,9 +169,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests { when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) .thenReturn(registeredClient); OAuth2AuthorizationCodeRequestAuthenticationToken authentication = - authorizationCodeRequestAuthentication(registeredClient, this.principal) - .redirectUri("https://example.com#fragment") - .build(); + new OAuth2AuthorizationCodeRequestAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + "https://example.com#fragment", STATE, registeredClient.getScopes(), null); assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) .isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class) .satisfies(ex -> @@ -196,9 +187,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests { when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) .thenReturn(registeredClient); OAuth2AuthorizationCodeRequestAuthenticationToken authentication = - authorizationCodeRequestAuthentication(registeredClient, this.principal) - .redirectUri("https://localhost:5000") - .build(); + new OAuth2AuthorizationCodeRequestAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + "https://localhost:5000", STATE, registeredClient.getScopes(), null); assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) .isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class) .satisfies(ex -> @@ -216,9 +207,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests { when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) .thenReturn(registeredClient); OAuth2AuthorizationCodeRequestAuthenticationToken authentication = - authorizationCodeRequestAuthentication(registeredClient, this.principal) - .redirectUri("https://invalid-example.com") - .build(); + new OAuth2AuthorizationCodeRequestAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + "https://invalid-example.com", STATE, registeredClient.getScopes(), null); assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) .isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class) .satisfies(ex -> @@ -236,9 +227,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests { when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) .thenReturn(registeredClient); OAuth2AuthorizationCodeRequestAuthenticationToken authentication = - authorizationCodeRequestAuthentication(registeredClient, this.principal) - .redirectUri("https://127.0.0.1:5000") - .build(); + new OAuth2AuthorizationCodeRequestAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + "https://127.0.0.1:5000", STATE, registeredClient.getScopes(), null); OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult = (OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication); @@ -255,9 +246,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests { when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) .thenReturn(registeredClient); OAuth2AuthorizationCodeRequestAuthenticationToken authentication = - authorizationCodeRequestAuthentication(registeredClient, this.principal) - .redirectUri("https://[::1]:5000") - .build(); + new OAuth2AuthorizationCodeRequestAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + "https://[::1]:5000", STATE, registeredClient.getScopes(), null); OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult = (OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication); @@ -271,9 +262,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests { when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) .thenReturn(registeredClient); OAuth2AuthorizationCodeRequestAuthenticationToken authentication = - authorizationCodeRequestAuthentication(registeredClient, this.principal) - .redirectUri(null) - .build(); + new OAuth2AuthorizationCodeRequestAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + null, STATE, registeredClient.getScopes(), null); assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) .isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class) .satisfies(ex -> @@ -291,9 +282,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests { when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) .thenReturn(registeredClient); OAuth2AuthorizationCodeRequestAuthenticationToken authentication = - authorizationCodeRequestAuthentication(registeredClient, this.principal) - .redirectUri(null) - .build(); + new OAuth2AuthorizationCodeRequestAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + null, STATE, registeredClient.getScopes(), null); assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) .isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class) .satisfies(ex -> @@ -311,8 +302,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests { when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) .thenReturn(registeredClient); OAuth2AuthorizationCodeRequestAuthenticationToken authentication = - authorizationCodeRequestAuthentication(registeredClient, this.principal) - .build(); + new OAuth2AuthorizationCodeRequestAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), null); assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) .isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class) .satisfies(ex -> @@ -328,9 +320,10 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests { when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) .thenReturn(registeredClient); OAuth2AuthorizationCodeRequestAuthenticationToken authentication = - authorizationCodeRequestAuthentication(registeredClient, this.principal) - .scopes(Collections.singleton("invalid-scope")) - .build(); + new OAuth2AuthorizationCodeRequestAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + registeredClient.getRedirectUris().iterator().next(), STATE, + Collections.singleton("invalid-scope"), null); assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) .isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class) .satisfies(ex -> @@ -347,8 +340,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests { when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) .thenReturn(registeredClient); OAuth2AuthorizationCodeRequestAuthenticationToken authentication = - authorizationCodeRequestAuthentication(registeredClient, this.principal) - .build(); + new OAuth2AuthorizationCodeRequestAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), null); assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) .isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class) .satisfies(ex -> @@ -366,9 +360,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests { additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, "code-challenge"); additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "unsupported"); OAuth2AuthorizationCodeRequestAuthenticationToken authentication = - authorizationCodeRequestAuthentication(registeredClient, this.principal) - .additionalParameters(additionalParameters) - .build(); + new OAuth2AuthorizationCodeRequestAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), additionalParameters); assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) .isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class) .satisfies(ex -> @@ -386,9 +380,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests { Map additionalParameters = new HashMap<>(); additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, "code-challenge"); OAuth2AuthorizationCodeRequestAuthenticationToken authentication = - authorizationCodeRequestAuthentication(registeredClient, this.principal) - .additionalParameters(additionalParameters) - .build(); + new OAuth2AuthorizationCodeRequestAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), additionalParameters); assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) .isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class) .satisfies(ex -> @@ -405,8 +399,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests { this.principal.setAuthenticated(false); OAuth2AuthorizationCodeRequestAuthenticationToken authentication = - authorizationCodeRequestAuthentication(registeredClient, this.principal) - .build(); + new OAuth2AuthorizationCodeRequestAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), null); OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult = (OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication); @@ -424,11 +419,12 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests { .thenReturn(registeredClient); OAuth2AuthorizationCodeRequestAuthenticationToken authentication = - authorizationCodeRequestAuthentication(registeredClient, this.principal) - .build(); + new OAuth2AuthorizationCodeRequestAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), null); - OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult = - (OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication); + OAuth2AuthorizationConsentAuthenticationToken authenticationResult = + (OAuth2AuthorizationConsentAuthenticationToken) this.authenticationProvider.authenticate(authentication); ArgumentCaptor authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class); verify(this.authorizationService).save(authorizationCaptor.capture()); @@ -457,8 +453,6 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests { 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(); } @@ -475,8 +469,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests { .thenReturn(registeredClient); OAuth2AuthorizationCodeRequestAuthenticationToken authentication = - authorizationCodeRequestAuthentication(registeredClient, this.principal) - .build(); + new OAuth2AuthorizationCodeRequestAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), null); OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult = (OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication); @@ -500,8 +495,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests { .thenReturn(previousAuthorizationConsent); OAuth2AuthorizationCodeRequestAuthenticationToken authentication = - authorizationCodeRequestAuthentication(registeredClient, this.principal) - .build(); + new OAuth2AuthorizationCodeRequestAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), null); OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult = (OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication); @@ -519,9 +515,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests { additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, "code-challenge"); additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256"); OAuth2AuthorizationCodeRequestAuthenticationToken authentication = - authorizationCodeRequestAuthentication(registeredClient, this.principal) - .additionalParameters(additionalParameters) - .build(); + new OAuth2AuthorizationCodeRequestAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), additionalParameters); OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult = (OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication); @@ -540,8 +536,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests { this.authenticationProvider.setAuthorizationCodeGenerator(authorizationCodeGenerator); OAuth2AuthorizationCodeRequestAuthenticationToken authentication = - authorizationCodeRequestAuthentication(registeredClient, this.principal) - .build(); + new OAuth2AuthorizationCodeRequestAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), null); assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) .isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class) @@ -563,8 +560,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests { this.authenticationProvider.setAuthenticationValidator(authenticationValidator); OAuth2AuthorizationCodeRequestAuthenticationToken authentication = - authorizationCodeRequestAuthentication(registeredClient, this.principal) - .build(); + new OAuth2AuthorizationCodeRequestAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), null); OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult = (OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication); @@ -611,410 +609,6 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests { 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 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 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); - - assertAuthorizationConsentRequestWithAuthorizationCodeResult(registeredClient, authorization, authenticationResult); - } - - @Test - public void authenticateWhenCustomAuthorizationConsentCustomizerThenUsed() { - 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 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); - - @SuppressWarnings("unchecked") - Consumer authorizationConsentCustomizer = mock(Consumer.class); - this.authenticationProvider.setAuthorizationConsentCustomizer(authorizationConsentCustomizer); - - OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult = - (OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication); - - assertAuthorizationConsentRequestWithAuthorizationCodeResult(registeredClient, authorization, authenticationResult); - - ArgumentCaptor authenticationContextCaptor = - ArgumentCaptor.forClass(OAuth2AuthorizationConsentAuthenticationContext.class); - verify(authorizationConsentCustomizer).accept(authenticationContextCaptor.capture()); - - OAuth2AuthorizationConsentAuthenticationContext authenticationContext = authenticationContextCaptor.getValue(); - assertThat(authenticationContext.getAuthentication()).isEqualTo(authentication); - assertThat(authenticationContext.getAuthorizationConsent()).isNotNull(); - assertThat(authenticationContext.getRegisteredClient()).isEqualTo(registeredClient); - assertThat(authenticationContext.getAuthorization()).isEqualTo(authorization); - assertThat(authenticationContext.getAuthorizationRequest()).isEqualTo(authorizationRequest); - } - - private void assertAuthorizationConsentRequestWithAuthorizationCodeResult( - RegisteredClient registeredClient, - OAuth2Authorization authorization, - OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult) { - OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName()); - Set authorizedScopes = authorizationRequest.getScopes(); - - ArgumentCaptor 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 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.getAttribute(Principal.class.getName())) - .isEqualTo(authorization.getAttribute(Principal.class.getName())); - assertThat(updatedAuthorization.getAttribute(OAuth2AuthorizationRequest.class.getName())) - .isEqualTo(authorizationRequest); - OAuth2Authorization.Token authorizationCode = updatedAuthorization.getToken(OAuth2AuthorizationCode.class); - assertThat(authorizationCode).isNotNull(); - assertThat(updatedAuthorization.getAttribute(OAuth2ParameterNames.STATE)).isNull(); - assertThat(updatedAuthorization.getAuthorizedScopes()).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 authenticateWhenConsentRequestApproveNoneAndRevokePreviouslyApprovedThenAuthorizationConsentRemoved() { - 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(); - OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName()); - 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); - - // Revoke all (including previously approved) - this.authenticationProvider.setAuthorizationConsentCustomizer((authorizationConsentContext) -> - authorizationConsentContext.getAuthorizationConsent().authorities(Set::clear)); - - assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) - .isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class) - .satisfies(ex -> - assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex, - OAuth2ErrorCodes.ACCESS_DENIED, OAuth2ParameterNames.CLIENT_ID, authorizationRequest.getRedirectUri()) - ); - - verify(this.authorizationConsentService).remove(eq(previousAuthorizationConsent)); - verify(this.authorizationService).remove(eq(authorization)); - } - - @Test - public void authenticateWhenConsentRequestApproveSomeAndPreviouslyApprovedThenAuthorizationConsentUpdated() { - 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 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 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 authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class); - verify(this.authorizationService).save(authorizationCaptor.capture()); - OAuth2Authorization updatedAuthorization = authorizationCaptor.getValue(); - assertThat(updatedAuthorization.getAuthorizedScopes()).isEqualTo(requestedScopes); - assertThat(authenticationResult.getScopes()).isEqualTo(requestedScopes); - } - - @Test - public void authenticateWhenConsentRequestApproveNoneAndPreviouslyApprovedThenAuthorizationConsentNotUpdated() { - 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) { @@ -1025,30 +619,6 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests { OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = authenticationException.getAuthorizationCodeRequestAuthentication(); assertThat(authorizationCodeRequestAuthentication.getRedirectUri()).isEqualTo(redirectUri); - - // gh-595 - if (OAuth2ErrorCodes.ACCESS_DENIED.equals(errorCode)) { - assertThat(authorizationCodeRequestAuthentication.isConsent()).isFalse(); - assertThat(authorizationCodeRequestAuthentication.isConsentRequired()).isFalse(); - } - } - - 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); } } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationTokenTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationTokenTests.java index d66044fd..e38ed115 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationTokenTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationTokenTests.java @@ -38,61 +38,57 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; */ 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)) + public void constructorWhenAuthorizationUriNotProvidedThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> + new OAuth2AuthorizationCodeRequestAuthenticationToken(null, REGISTERED_CLIENT.getClientId(), PRINCIPAL, + null, null, (Set) null, null)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("clientId cannot be empty"); + .hasMessage("authorizationUri cannot be empty"); } @Test - public void withWhenPrincipalNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> OAuth2AuthorizationCodeRequestAuthenticationToken.with(REGISTERED_CLIENT.getClientId(), null)) + public void constructorWhenClientIdNotProvidedThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> + new OAuth2AuthorizationCodeRequestAuthenticationToken(AUTHORIZATION_URI, null, PRINCIPAL, + null, null, (Set) null, null)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("principal cannot be null"); + .hasMessage("clientId cannot be empty"); } @Test - public void buildWhenAuthorizationUriNotProvidedThenThrowIllegalArgumentException() { + public void constructorWhenPrincipalNotProvidedThenThrowIllegalArgumentException() { assertThatThrownBy(() -> - OAuth2AuthorizationCodeRequestAuthenticationToken.with(REGISTERED_CLIENT.getClientId(), PRINCIPAL) - .build()) + new OAuth2AuthorizationCodeRequestAuthenticationToken(AUTHORIZATION_URI, REGISTERED_CLIENT.getClientId(), null, + null, null, (Set) null, null)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("authorizationUri cannot be empty"); + .hasMessage("principal cannot be null"); } @Test - public void buildWhenStateNotProvidedThenThrowIllegalArgumentException() { + public void constructorWhenAuthorizationCodeNotProvidedThenThrowIllegalArgumentException() { assertThatThrownBy(() -> - OAuth2AuthorizationCodeRequestAuthenticationToken.with(REGISTERED_CLIENT.getClientId(), PRINCIPAL) - .authorizationUri(AUTHORIZATION_URI) - .consent(true) - .build()) + new OAuth2AuthorizationCodeRequestAuthenticationToken(AUTHORIZATION_URI, REGISTERED_CLIENT.getClientId(), PRINCIPAL, + null, null, null, (Set) null)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("state cannot be empty"); + .hasMessage("authorizationCode cannot be null"); } @Test - public void buildWhenAuthorizationCodeRequestThenValuesAreSet() { + public void constructorWhenAuthorizationRequestThenValuesAreSet() { String clientId = REGISTERED_CLIENT.getClientId(); String redirectUri = REGISTERED_CLIENT.getRedirectUris().iterator().next(); + String state = "state"; Set requestedScopes = REGISTERED_CLIENT.getScopes(); Map additionalParameters = Collections.singletonMap("param1", "value1"); - OAuth2AuthorizationCodeRequestAuthenticationToken authentication = - OAuth2AuthorizationCodeRequestAuthenticationToken.with(clientId, PRINCIPAL) - .authorizationUri(AUTHORIZATION_URI) - .redirectUri(redirectUri) - .scopes(requestedScopes) - .state(STATE) - .additionalParameters(additionalParameters) - .build(); + OAuth2AuthorizationCodeRequestAuthenticationToken authentication = new OAuth2AuthorizationCodeRequestAuthenticationToken( + AUTHORIZATION_URI, clientId, PRINCIPAL, redirectUri, state, requestedScopes, additionalParameters); assertThat(authentication.getPrincipal()).isEqualTo(PRINCIPAL); assertThat(authentication.getCredentials()).isEqualTo(""); @@ -100,87 +96,22 @@ public class OAuth2AuthorizationCodeRequestAuthenticationTokenTests { assertThat(authentication.getAuthorizationUri()).isEqualTo(AUTHORIZATION_URI); assertThat(authentication.getClientId()).isEqualTo(clientId); assertThat(authentication.getRedirectUri()).isEqualTo(redirectUri); + assertThat(authentication.getState()).isEqualTo(state); 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 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 authorizedScopes = REGISTERED_CLIENT.getScopes(); - Map 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() { + public void constructorWhenAuthorizationResponseThenValuesAreSet() { String clientId = REGISTERED_CLIENT.getClientId(); String redirectUri = REGISTERED_CLIENT.getRedirectUris().iterator().next(); + String state = "state"; Set authorizedScopes = REGISTERED_CLIENT.getScopes(); - OAuth2AuthorizationCodeRequestAuthenticationToken authentication = - OAuth2AuthorizationCodeRequestAuthenticationToken.with(clientId, PRINCIPAL) - .authorizationUri(AUTHORIZATION_URI) - .redirectUri(redirectUri) - .scopes(authorizedScopes) - .state(STATE) - .authorizationCode(AUTHORIZATION_CODE) - .build(); + OAuth2AuthorizationCodeRequestAuthenticationToken authentication = new OAuth2AuthorizationCodeRequestAuthenticationToken( + AUTHORIZATION_URI, clientId, PRINCIPAL, AUTHORIZATION_CODE, redirectUri, state, authorizedScopes); assertThat(authentication.getPrincipal()).isEqualTo(PRINCIPAL); assertThat(authentication.getCredentials()).isEqualTo(""); @@ -188,11 +119,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationTokenTests { assertThat(authentication.getAuthorizationUri()).isEqualTo(AUTHORIZATION_URI); assertThat(authentication.getClientId()).isEqualTo(clientId); assertThat(authentication.getRedirectUri()).isEqualTo(redirectUri); + assertThat(authentication.getState()).isEqualTo(state); 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(); } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationContextTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationContextTests.java index 84110b66..419c41dd 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationContextTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationContextTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the original author or authors. + * Copyright 2020-2022 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. @@ -42,10 +42,10 @@ public class OAuth2AuthorizationConsentAuthenticationContextTests { private final Authentication principal = this.authorization.getAttribute(Principal.class.getName()); private final OAuth2AuthorizationRequest authorizationRequest = this.authorization.getAttribute( OAuth2AuthorizationRequest.class.getName()); - private final OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = - OAuth2AuthorizationCodeRequestAuthenticationToken.with(this.registeredClient.getClientId(), this.principal) - .authorizationUri(this.authorizationRequest.getAuthorizationUri()) - .build(); + private final OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication = + new OAuth2AuthorizationConsentAuthenticationToken( + this.authorizationRequest.getAuthorizationUri(), this.registeredClient.getClientId(), + this.principal, "state", null, null); private final OAuth2AuthorizationConsent.Builder authorizationConsentBuilder = OAuth2AuthorizationConsent.withId(this.authorization.getRegisteredClientId(), this.authorization.getPrincipalName()); @@ -59,7 +59,7 @@ public class OAuth2AuthorizationConsentAuthenticationContextTests { @Test public void setWhenValueNullThenThrowIllegalArgumentException() { OAuth2AuthorizationConsentAuthenticationContext.Builder builder = - OAuth2AuthorizationConsentAuthenticationContext.with(this.authorizationCodeRequestAuthentication); + OAuth2AuthorizationConsentAuthenticationContext.with(this.authorizationConsentAuthentication); assertThatThrownBy(() -> builder.authorizationConsent(null)) .isInstanceOf(IllegalArgumentException.class); @@ -76,7 +76,7 @@ public class OAuth2AuthorizationConsentAuthenticationContextTests { @Test public void buildWhenRequiredValueNullThenThrowIllegalArgumentException() { OAuth2AuthorizationConsentAuthenticationContext.Builder builder = - OAuth2AuthorizationConsentAuthenticationContext.with(this.authorizationCodeRequestAuthentication); + OAuth2AuthorizationConsentAuthenticationContext.with(this.authorizationConsentAuthentication); assertThatThrownBy(builder::build) .isInstanceOf(IllegalArgumentException.class) @@ -104,7 +104,7 @@ public class OAuth2AuthorizationConsentAuthenticationContextTests { @Test public void buildWhenAllValuesProvidedThenAllValuesAreSet() { OAuth2AuthorizationConsentAuthenticationContext context = - OAuth2AuthorizationConsentAuthenticationContext.with(this.authorizationCodeRequestAuthentication) + OAuth2AuthorizationConsentAuthenticationContext.with(this.authorizationConsentAuthentication) .authorizationConsent(this.authorizationConsentBuilder) .registeredClient(this.registeredClient) .authorization(this.authorization) @@ -113,7 +113,7 @@ public class OAuth2AuthorizationConsentAuthenticationContextTests { .context(ctx -> ctx.put("custom-key-2", "custom-value-2")) .build(); - assertThat(context.getAuthentication()).isEqualTo(this.authorizationCodeRequestAuthentication); + assertThat(context.getAuthentication()).isEqualTo(this.authorizationConsentAuthentication); assertThat(context.getAuthorizationConsent()).isEqualTo(this.authorizationConsentBuilder); assertThat(context.getRegisteredClient()).isEqualTo(this.registeredClient); assertThat(context.getAuthorization()).isEqualTo(this.authorization); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationProviderTests.java new file mode 100644 index 00000000..a1d0ffd4 --- /dev/null +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationProviderTests.java @@ -0,0 +1,548 @@ +/* + * Copyright 2020-2022 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.HashSet; +import java.util.Set; +import java.util.function.Consumer; + +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.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode; +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.OAuth2TokenType; +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 org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder; +import org.springframework.security.oauth2.server.authorization.context.TestAuthorizationServerContext; +import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; + +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 OAuth2AuthorizationConsentAuthenticationProvider}. + * + * @author Joe Grandja + * @author Steve Riesenberg + */ +public class OAuth2AuthorizationConsentAuthenticationProviderTests { + private static final String AUTHORIZATION_URI = "https://provider.com/oauth2/authorize"; + private static final String STATE = "state"; + private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE); + private RegisteredClientRepository registeredClientRepository; + private OAuth2AuthorizationService authorizationService; + private OAuth2AuthorizationConsentService authorizationConsentService; + private OAuth2AuthorizationConsentAuthenticationProvider 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 OAuth2AuthorizationConsentAuthenticationProvider( + this.registeredClientRepository, this.authorizationService, this.authorizationConsentService); + this.principal = new TestingAuthenticationToken("principalName", "password"); + this.principal.setAuthenticated(true); + AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder().issuer("https://provider.com").build(); + AuthorizationServerContextHolder.setContext(new TestAuthorizationServerContext(authorizationServerSettings, null)); + } + + @Test + public void constructorWhenRegisteredClientRepositoryNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new OAuth2AuthorizationConsentAuthenticationProvider( + null, this.authorizationService, this.authorizationConsentService)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("registeredClientRepository cannot be null"); + } + + @Test + public void constructorWhenAuthorizationServiceNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new OAuth2AuthorizationConsentAuthenticationProvider( + this.registeredClientRepository, null, this.authorizationConsentService)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("authorizationService cannot be null"); + } + + @Test + public void constructorWhenAuthorizationConsentServiceNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new OAuth2AuthorizationConsentAuthenticationProvider( + this.registeredClientRepository, this.authorizationService, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("authorizationConsentService cannot be null"); + } + + @Test + public void supportsWhenTypeOAuth2AuthorizationConsentAuthenticationTokenThenReturnTrue() { + assertThat(this.authenticationProvider.supports(OAuth2AuthorizationConsentAuthenticationToken.class)).isTrue(); + } + + @Test + public void setAuthorizationCodeGeneratorWhenNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.authenticationProvider.setAuthorizationCodeGenerator(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("authorizationCodeGenerator cannot be null"); + } + + @Test + public void setAuthorizationConsentCustomizerWhenNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.authenticationProvider.setAuthorizationConsentCustomizer(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("authorizationConsentCustomizer cannot be null"); + } + + @Test + public void authenticateWhenInvalidStateThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient() + .build(); + OAuth2AuthorizationConsentAuthenticationToken authentication = + new OAuth2AuthorizationConsentAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + STATE, registeredClient.getScopes(), null); + 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 authenticateWhenPrincipalNotAuthenticatedThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient() + .build(); + OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient) + .principalName(this.principal.getName()) + .build(); + OAuth2AuthorizationConsentAuthenticationToken authentication = + new OAuth2AuthorizationConsentAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + STATE, registeredClient.getScopes(), null); + 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 authenticateWhenInvalidPrincipalThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient() + .build(); + OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient) + .principalName(this.principal.getName().concat("-other")) + .build(); + OAuth2AuthorizationConsentAuthenticationToken authentication = + new OAuth2AuthorizationConsentAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + STATE, registeredClient.getScopes(), null); + 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 authenticateWhenInvalidClientIdThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() { + 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(); + OAuth2AuthorizationConsentAuthenticationToken authentication = + new OAuth2AuthorizationConsentAuthenticationToken( + AUTHORIZATION_URI, otherRegisteredClient.getClientId(), principal, + STATE, registeredClient.getScopes(), null); + + assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) + .isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class) + .satisfies(ex -> + assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex, + OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID, null) + ); + } + + @Test + public void authenticateWhenDoesNotMatchClientThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() { + 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); + OAuth2AuthorizationConsentAuthenticationToken authentication = + new OAuth2AuthorizationConsentAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + STATE, registeredClient.getScopes(), null); + + assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) + .isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class) + .satisfies(ex -> + assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex, + OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID, null) + ); + } + + @Test + public void authenticateWhenScopeNotRequestedThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() { + 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 authorizedScopes = new HashSet<>(authorizationRequest.getScopes()); + authorizedScopes.add("scope-not-requested"); + OAuth2AuthorizationConsentAuthenticationToken authentication = + new OAuth2AuthorizationConsentAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + STATE, authorizedScopes, null); + 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 authenticateWhenNotApprovedThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient() + .build(); + when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) + .thenReturn(registeredClient); + OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient) + .principalName(this.principal.getName()) + .build(); + OAuth2AuthorizationConsentAuthenticationToken authentication = + new OAuth2AuthorizationConsentAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + STATE, new HashSet<>(), null); // No scopes approved + 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 authenticateWhenApproveAllThenReturnAuthorizationCode() { + 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 authorizedScopes = authorizationRequest.getScopes(); + OAuth2AuthorizationConsentAuthenticationToken authentication = + new OAuth2AuthorizationConsentAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + STATE, authorizedScopes, null); // Approve all scopes + when(this.authorizationService.findByToken(eq(authentication.getState()), eq(STATE_TOKEN_TYPE))) + .thenReturn(authorization); + + OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult = + (OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication); + + assertAuthorizationConsentRequestWithAuthorizationCodeResult(registeredClient, authorization, authenticationResult); + } + + @Test + public void authenticateWhenCustomAuthorizationConsentCustomizerThenUsed() { + 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 authorizedScopes = authorizationRequest.getScopes(); + OAuth2AuthorizationConsentAuthenticationToken authentication = + new OAuth2AuthorizationConsentAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + STATE, authorizedScopes, null); // Approve all scopes + when(this.authorizationService.findByToken(eq(authentication.getState()), eq(STATE_TOKEN_TYPE))) + .thenReturn(authorization); + + @SuppressWarnings("unchecked") + Consumer authorizationConsentCustomizer = mock(Consumer.class); + this.authenticationProvider.setAuthorizationConsentCustomizer(authorizationConsentCustomizer); + + OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult = + (OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication); + + assertAuthorizationConsentRequestWithAuthorizationCodeResult(registeredClient, authorization, authenticationResult); + + ArgumentCaptor authenticationContextCaptor = + ArgumentCaptor.forClass(OAuth2AuthorizationConsentAuthenticationContext.class); + verify(authorizationConsentCustomizer).accept(authenticationContextCaptor.capture()); + + OAuth2AuthorizationConsentAuthenticationContext authenticationContext = authenticationContextCaptor.getValue(); + assertThat(authenticationContext.getAuthentication()).isEqualTo(authentication); + assertThat(authenticationContext.getAuthorizationConsent()).isNotNull(); + assertThat(authenticationContext.getRegisteredClient()).isEqualTo(registeredClient); + assertThat(authenticationContext.getAuthorization()).isEqualTo(authorization); + assertThat(authenticationContext.getAuthorizationRequest()).isEqualTo(authorizationRequest); + } + + private void assertAuthorizationConsentRequestWithAuthorizationCodeResult( + RegisteredClient registeredClient, + OAuth2Authorization authorization, + OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult) { + OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName()); + Set authorizedScopes = authorizationRequest.getScopes(); + + ArgumentCaptor 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 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.getAttribute(Principal.class.getName())) + .isEqualTo(authorization.getAttribute(Principal.class.getName())); + assertThat(updatedAuthorization.getAttribute(OAuth2AuthorizationRequest.class.getName())) + .isEqualTo(authorizationRequest); + OAuth2Authorization.Token authorizationCode = updatedAuthorization.getToken(OAuth2AuthorizationCode.class); + assertThat(authorizationCode).isNotNull(); + assertThat(updatedAuthorization.getAttribute(OAuth2ParameterNames.STATE)).isNull(); + assertThat(updatedAuthorization.getAuthorizedScopes()).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 authenticateWhenApproveNoneAndRevokePreviouslyApprovedThenAuthorizationConsentRemoved() { + 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(); + OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName()); + OAuth2AuthorizationConsentAuthenticationToken authentication = + new OAuth2AuthorizationConsentAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + STATE, new HashSet<>(), null); // No scopes approved + 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); + + // Revoke all (including previously approved) + this.authenticationProvider.setAuthorizationConsentCustomizer((authorizationConsentContext) -> + authorizationConsentContext.getAuthorizationConsent().authorities(Set::clear)); + + assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) + .isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class) + .satisfies(ex -> + assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex, + OAuth2ErrorCodes.ACCESS_DENIED, OAuth2ParameterNames.CLIENT_ID, authorizationRequest.getRedirectUri()) + ); + + verify(this.authorizationConsentService).remove(eq(previousAuthorizationConsent)); + verify(this.authorizationService).remove(eq(authorization)); + } + + @Test + public void authenticateWhenApproveSomeAndPreviouslyApprovedThenAuthorizationConsentUpdated() { + 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 requestedScopes = authorizationRequest.getScopes(); + OAuth2AuthorizationConsentAuthenticationToken authentication = + new OAuth2AuthorizationConsentAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + STATE, requestedScopes, null); + 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 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 authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class); + verify(this.authorizationService).save(authorizationCaptor.capture()); + OAuth2Authorization updatedAuthorization = authorizationCaptor.getValue(); + assertThat(updatedAuthorization.getAuthorizedScopes()).isEqualTo(requestedScopes); + assertThat(authenticationResult.getScopes()).isEqualTo(requestedScopes); + } + + @Test + public void authenticateWhenApproveNoneAndPreviouslyApprovedThenAuthorizationConsentNotUpdated() { + 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(); + OAuth2AuthorizationConsentAuthenticationToken authentication = + new OAuth2AuthorizationConsentAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + STATE, new HashSet<>(), null); // No scopes approved + 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); + } + +} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationCodeGrantTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationCodeGrantTests.java index efacc025..fcd79619 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationCodeGrantTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationCodeGrantTests.java @@ -90,6 +90,8 @@ import org.springframework.security.oauth2.server.authorization.TestOAuth2Author import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationProvider; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationContext; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationProvider; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationToken; import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository.RegisteredClientParametersMapper; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; @@ -108,6 +110,7 @@ import org.springframework.security.oauth2.server.authorization.token.OAuth2Toke import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeRequestAuthenticationConverter; +import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationConsentAuthenticationConverter; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.security.web.authentication.AuthenticationFailureHandler; @@ -626,13 +629,9 @@ public class OAuth2AuthorizationCodeGrantTests { OAuth2AuthorizationCode authorizationCode = new OAuth2AuthorizationCode( "code", Instant.now(), Instant.now().plus(5, ChronoUnit.MINUTES)); OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult = - OAuth2AuthorizationCodeRequestAuthenticationToken.with(registeredClient.getClientId(), principal) - .authorizationUri("https://provider.com/oauth2/authorize") - .redirectUri(registeredClient.getRedirectUris().iterator().next()) - .scopes(registeredClient.getScopes()) - .state("state") - .authorizationCode(authorizationCode) - .build(); + new OAuth2AuthorizationCodeRequestAuthenticationToken( + "https://provider.com/oauth2/authorize", registeredClient.getClientId(), principal, authorizationCode, + registeredClient.getRedirectUris().iterator().next(), "state", registeredClient.getScopes()); when(authorizationRequestConverter.convert(any())).thenReturn(authorizationCodeRequestAuthenticationResult); when(authorizationRequestAuthenticationProvider.supports(eq(OAuth2AuthorizationCodeRequestAuthenticationToken.class))).thenReturn(true); when(authorizationRequestAuthenticationProvider.authenticate(any())).thenReturn(authorizationCodeRequestAuthenticationResult); @@ -650,7 +649,8 @@ public class OAuth2AuthorizationCodeGrantTests { List authenticationConverters = authenticationConvertersCaptor.getValue(); assertThat(authenticationConverters).allMatch((converter) -> converter == authorizationRequestConverter || - converter instanceof OAuth2AuthorizationCodeRequestAuthenticationConverter); + converter instanceof OAuth2AuthorizationCodeRequestAuthenticationConverter || + converter instanceof OAuth2AuthorizationConsentAuthenticationConverter); verify(authorizationRequestAuthenticationProvider).authenticate(eq(authorizationCodeRequestAuthenticationResult)); @@ -660,7 +660,8 @@ public class OAuth2AuthorizationCodeGrantTests { List authenticationProviders = authenticationProvidersCaptor.getValue(); assertThat(authenticationProviders).allMatch((provider) -> provider == authorizationRequestAuthenticationProvider || - provider instanceof OAuth2AuthorizationCodeRequestAuthenticationProvider); + provider instanceof OAuth2AuthorizationCodeRequestAuthenticationProvider || + provider instanceof OAuth2AuthorizationConsentAuthenticationProvider); verify(authorizationResponseHandler).onAuthenticationSuccess(any(), any(), eq(authorizationCodeRequestAuthenticationResult)); } @@ -934,7 +935,7 @@ public class OAuth2AuthorizationCodeGrantTests { new OAuth2AuthorizationServerConfigurer(); authorizationServerConfigurer .authorizationEndpoint(authorizationEndpoint -> - authorizationEndpoint.authenticationProvider(createProvider())); + authorizationEndpoint.authenticationProviders(configureAuthenticationProviders())); RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher(); http @@ -966,15 +967,14 @@ public class OAuth2AuthorizationCodeGrantTests { }; } - private AuthenticationProvider createProvider() { - OAuth2AuthorizationCodeRequestAuthenticationProvider authorizationCodeRequestAuthenticationProvider = - new OAuth2AuthorizationCodeRequestAuthenticationProvider( - this.registeredClientRepository, - this.authorizationService, - this.authorizationConsentService); - authorizationCodeRequestAuthenticationProvider.setAuthorizationConsentCustomizer(new AuthorizationConsentCustomizer()); - - return authorizationCodeRequestAuthenticationProvider; + private Consumer> configureAuthenticationProviders() { + return (authenticationProviders) -> + authenticationProviders.forEach((authenticationProvider) -> { + if (authenticationProvider instanceof OAuth2AuthorizationConsentAuthenticationProvider) { + ((OAuth2AuthorizationConsentAuthenticationProvider) authenticationProvider) + .setAuthorizationConsentCustomizer(new AuthorizationConsentCustomizer()); + } + }); } static class AuthorizationConsentCustomizer implements Consumer { @@ -983,10 +983,10 @@ public class OAuth2AuthorizationCodeGrantTests { public void accept(OAuth2AuthorizationConsentAuthenticationContext authorizationConsentAuthenticationContext) { OAuth2AuthorizationConsent.Builder authorizationConsentBuilder = authorizationConsentAuthenticationContext.getAuthorizationConsent(); - OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = + OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication = authorizationConsentAuthenticationContext.getAuthentication(); Map additionalParameters = - authorizationCodeRequestAuthentication.getAdditionalParameters(); + authorizationConsentAuthentication.getAdditionalParameters(); RegisteredClient registeredClient = authorizationConsentAuthenticationContext.getRegisteredClient(); ClientSettings clientSettings = registeredClient.getClientSettings(); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilterTests.java index 79d4909e..e4038667 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilterTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilterTests.java @@ -40,7 +40,6 @@ import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.core.OAuth2Error; @@ -52,6 +51,7 @@ import org.springframework.security.oauth2.core.oidc.OidcScopes; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationToken; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; import org.springframework.security.web.authentication.AuthenticationConverter; @@ -82,6 +82,8 @@ import static org.mockito.Mockito.when; */ public class OAuth2AuthorizationEndpointFilterTests { private static final String DEFAULT_AUTHORIZATION_ENDPOINT_URI = "/oauth2/authorize"; + private static final String AUTHORIZATION_URI = "https://provider.com/oauth2/authorize"; + private static final String STATE = "state"; private static final String REMOTE_ADDRESS = "remote-address"; private AuthenticationManager authenticationManager; private OAuth2AuthorizationEndpointFilter filter; @@ -280,8 +282,9 @@ public class OAuth2AuthorizationEndpointFilterTests { public void doFilterWhenAuthorizationRequestAuthenticationExceptionThenErrorResponse() throws Exception { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = - authorizationCodeRequestAuthentication(registeredClient, this.principal) - .build(); + new OAuth2AuthorizationCodeRequestAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), null); OAuth2Error error = new OAuth2Error("errorCode", "errorDescription", "errorUri"); when(this.authenticationManager.authenticate(any())) .thenThrow(new OAuth2AuthorizationCodeRequestAuthenticationException(error, authorizationCodeRequestAuthentication)); @@ -304,8 +307,9 @@ public class OAuth2AuthorizationEndpointFilterTests { public void doFilterWhenCustomAuthenticationConverterThenUsed() throws Exception { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = - authorizationCodeRequestAuthentication(registeredClient, this.principal) - .build(); + new OAuth2AuthorizationCodeRequestAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), null); AuthenticationConverter authenticationConverter = mock(AuthenticationConverter.class); when(authenticationConverter.convert(any())).thenReturn(authorizationCodeRequestAuthentication); @@ -329,9 +333,9 @@ public class OAuth2AuthorizationEndpointFilterTests { public void doFilterWhenCustomAuthenticationSuccessHandlerThenUsed() throws Exception { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult = - authorizationCodeRequestAuthentication(registeredClient, this.principal) - .authorizationCode(this.authorizationCode) - .build(); + new OAuth2AuthorizationCodeRequestAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, this.authorizationCode, + registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes()); authorizationCodeRequestAuthenticationResult.setAuthenticated(true); when(this.authenticationManager.authenticate(any())) .thenReturn(authorizationCodeRequestAuthenticationResult); @@ -354,8 +358,9 @@ public class OAuth2AuthorizationEndpointFilterTests { public void doFilterWhenCustomAuthenticationFailureHandlerThenUsed() throws Exception { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = - authorizationCodeRequestAuthentication(registeredClient, this.principal) - .build(); + new OAuth2AuthorizationCodeRequestAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), null); OAuth2Error error = new OAuth2Error("errorCode", "errorDescription", "errorUri"); OAuth2AuthorizationCodeRequestAuthenticationException authenticationException = new OAuth2AuthorizationCodeRequestAuthenticationException(error, authorizationCodeRequestAuthentication); @@ -380,7 +385,9 @@ public class OAuth2AuthorizationEndpointFilterTests { public void doFilterWhenCustomAuthenticationDetailsSourceThenUsed() throws Exception { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = - authorizationCodeRequestAuthentication(registeredClient, this.principal).build(); + new OAuth2AuthorizationCodeRequestAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), null); MockHttpServletRequest request = createAuthorizationRequest(registeredClient); AuthenticationDetailsSource authenticationDetailsSource = @@ -407,8 +414,9 @@ public class OAuth2AuthorizationEndpointFilterTests { this.principal.setAuthenticated(false); RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult = - authorizationCodeRequestAuthentication(registeredClient, this.principal) - .build(); + new OAuth2AuthorizationCodeRequestAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), null); authorizationCodeRequestAuthenticationResult.setAuthenticated(false); when(this.authenticationManager.authenticate(any())) .thenReturn(authorizationCodeRequestAuthenticationResult); @@ -432,14 +440,13 @@ public class OAuth2AuthorizationEndpointFilterTests { scopes.addAll(requestedScopes); }) .build(); - OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult = - authorizationCodeRequestAuthentication(registeredClient, this.principal) - .scopes(new HashSet<>()) // No scopes previously approved - .consentRequired(true) - .build(); - authorizationCodeRequestAuthenticationResult.setAuthenticated(true); + OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthenticationResult = + new OAuth2AuthorizationConsentAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + STATE, new HashSet<>(), null); // No scopes previously approved + authorizationConsentAuthenticationResult.setAuthenticated(true); when(this.authenticationManager.authenticate(any())) - .thenReturn(authorizationCodeRequestAuthenticationResult); + .thenReturn(authorizationConsentAuthenticationResult); MockHttpServletRequest request = createAuthorizationRequest(registeredClient); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -464,14 +471,13 @@ public class OAuth2AuthorizationEndpointFilterTests { scopes.addAll(requestedScopes); }) .build(); - OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult = - authorizationCodeRequestAuthentication(registeredClient, this.principal) - .scopes(new HashSet<>()) // No scopes previously approved - .consentRequired(true) - .build(); - authorizationCodeRequestAuthenticationResult.setAuthenticated(true); + OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthenticationResult = + new OAuth2AuthorizationConsentAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + STATE, new HashSet<>(), null); // No scopes previously approved + authorizationConsentAuthenticationResult.setAuthenticated(true); when(this.authenticationManager.authenticate(any())) - .thenReturn(authorizationCodeRequestAuthenticationResult); + .thenReturn(authorizationConsentAuthenticationResult); MockHttpServletRequest request = createAuthorizationRequest(registeredClient); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -500,14 +506,13 @@ public class OAuth2AuthorizationEndpointFilterTests { scopes.addAll(requestedScopes); }) .build(); - OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult = - authorizationCodeRequestAuthentication(registeredClient, this.principal) - .scopes(approvedScopes) - .consentRequired(true) - .build(); - authorizationCodeRequestAuthenticationResult.setAuthenticated(true); + OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthenticationResult = + new OAuth2AuthorizationConsentAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, + STATE, approvedScopes, null); + authorizationConsentAuthenticationResult.setAuthenticated(true); when(this.authenticationManager.authenticate(any())) - .thenReturn(authorizationCodeRequestAuthenticationResult); + .thenReturn(authorizationConsentAuthenticationResult); MockHttpServletRequest request = createAuthorizationRequest(registeredClient); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -532,9 +537,9 @@ public class OAuth2AuthorizationEndpointFilterTests { public void doFilterWhenAuthorizationRequestAuthenticatedThenAuthorizationResponse() throws Exception { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult = - authorizationCodeRequestAuthentication(registeredClient, this.principal) - .authorizationCode(this.authorizationCode) - .build(); + new OAuth2AuthorizationCodeRequestAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, this.authorizationCode, + registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes()); authorizationCodeRequestAuthenticationResult.setAuthenticated(true); when(this.authenticationManager.authenticate(any())) .thenReturn(authorizationCodeRequestAuthenticationResult); @@ -568,9 +573,9 @@ public class OAuth2AuthorizationEndpointFilterTests { }) .build(); OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult = - authorizationCodeRequestAuthentication(registeredClient, this.principal) - .authorizationCode(this.authorizationCode) - .build(); + new OAuth2AuthorizationCodeRequestAuthenticationToken( + AUTHORIZATION_URI, registeredClient.getClientId(), principal, this.authorizationCode, + registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes()); authorizationCodeRequestAuthenticationResult.setAuthenticated(true); when(this.authenticationManager.authenticate(any())) .thenReturn(authorizationCodeRequestAuthenticationResult); @@ -647,15 +652,6 @@ public class OAuth2AuthorizationEndpointFilterTests { return request; } - 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 String scopeCheckbox(String scope) { return MessageFormat.format( "",