diff --git a/README.adoc b/README.adoc index c7bfb949..85e10377 100644 --- a/README.adoc +++ b/README.adoc @@ -36,9 +36,9 @@ The goal is to leverage all the knowledge learned thus far and apply the same to Submitted work via pull requests should follow the same coding style/conventions and adopt the same or similar design patterns that have been established in Spring Security's OAuth 2.0 support. == Documentation -Be sure to read the https://docs.spring.io/spring-security/reference[Spring Security Reference], as well as the https://docs.spring.io/spring-security/reference/servlet/oauth2/index.html[OAuth 2.0 Reference], which describes the Client and Resource Server features available. +Be sure to read the https://docs.spring.io/spring-authorization-server/docs/current/reference/html/[Spring Authorization Server Reference] and https://docs.spring.io/spring-security/reference[Spring Security Reference], as well as the https://docs.spring.io/spring-security/reference/servlet/oauth2/index.html[OAuth 2.0 Reference], which describes the Client and Resource Server features available. -Extensive JavaDoc for the Spring Security code is also available in the https://docs.spring.io/spring-security/site/docs/current/api/[Spring Security API Documentation]. +JavaDoc is also available for the https://docs.spring.io/spring-authorization-server/docs/current/api/[Spring Authorization Server API] and https://docs.spring.io/spring-security/site/docs/current/api/[Spring Security API]. == Code of Conduct This project adheres to the Contributor Covenant link:CODE_OF_CONDUCT.adoc[code of conduct]. 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/AbstractOAuth2AuthorizationServerMetadata.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/AbstractOAuth2AuthorizationServerMetadata.java index bef3f0d5..b7c7b81f 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/AbstractOAuth2AuthorizationServerMetadata.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/AbstractOAuth2AuthorizationServerMetadata.java @@ -274,6 +274,17 @@ public abstract class AbstractOAuth2AuthorizationServerMetadata implements OAuth return getThis(); } + /** + * Use this {@code registration_endpoint} in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL. + * + * @param clientRegistrationEndpoint the {@code URL} of the OAuth 2.0 Dynamic Client Registration Endpoint + * @return the {@link AbstractBuilder} for further configuration + * @since 0.4.0 + */ + public B clientRegistrationEndpoint(String clientRegistrationEndpoint) { + return claim(OAuth2AuthorizationServerMetadataClaimNames.REGISTRATION_ENDPOINT, clientRegistrationEndpoint); + } + /** * Add this Proof Key for Code Exchange (PKCE) {@code code_challenge_method} to the collection of {@code code_challenge_methods_supported} * in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL. @@ -369,6 +380,9 @@ public abstract class AbstractOAuth2AuthorizationServerMetadata implements OAuth Assert.isInstanceOf(List.class, getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED), "tokenIntrospectionEndpointAuthenticationMethods must be of type List"); Assert.notEmpty((List) getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED), "tokenIntrospectionEndpointAuthenticationMethods cannot be empty"); } + if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.REGISTRATION_ENDPOINT) != null) { + validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.REGISTRATION_ENDPOINT), "clientRegistrationEndpoint must be a valid URL"); + } if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED) != null) { Assert.isInstanceOf(List.class, getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED), "codeChallengeMethods must be of type List"); Assert.notEmpty((List) getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED), "codeChallengeMethods cannot be empty"); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimAccessor.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimAccessor.java index 35ced717..8767843f 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimAccessor.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimAccessor.java @@ -141,6 +141,16 @@ public interface OAuth2AuthorizationServerMetadataClaimAccessor extends ClaimAcc return getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED); } + /** + * Returns the {@code URL} of the OAuth 2.0 Dynamic Client Registration Endpoint {@code (registration_endpoint)}. + * + * @return the {@code URL} of the OAuth 2.0 Dynamic Client Registration Endpoint + * @since 0.4.0 + */ + default URL getClientRegistrationEndpoint() { + return getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.REGISTRATION_ENDPOINT); + } + /** * Returns the Proof Key for Code Exchange (PKCE) {@code code_challenge_method} values supported {@code (code_challenge_methods_supported)}. * diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimNames.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimNames.java index 46fdb3e7..a4fb116a 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimNames.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimNames.java @@ -86,6 +86,12 @@ public class OAuth2AuthorizationServerMetadataClaimNames { */ public static final String INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED = "introspection_endpoint_auth_methods_supported"; + /** + * {@code registration_endpoint} - the {@code URL} of the OAuth 2.0 Dynamic Client Registration Endpoint + * @since 0.4.0 + */ + public static final String REGISTRATION_ENDPOINT = "registration_endpoint"; + /** * {@code code_challenge_methods_supported} - the Proof Key for Code Exchange (PKCE) {@code code_challenge_method} values supported */ 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; + @Override + public boolean supports(Class authentication) { + return OAuth2AuthorizationCodeRequestAuthenticationToken.class.isAssignableFrom(authentication); + } - OAuth2Authorization authorization = this.authorizationService.findByToken( - authorizationCodeRequestAuthentication.getState(), STATE_TOKEN_TYPE); - if (authorization == null) { - throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE, - authorizationCodeRequestAuthentication, null, null); - } + /** + * 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; + } - // 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); - - String redirectUri = authorizationRequest.getRedirectUri(); - if (!StringUtils.hasText(redirectUri)) { - redirectUri = registeredClient.getRedirectUris().iterator().next(); - } - - return OAuth2AuthorizationCodeRequestAuthenticationToken.with(registeredClient.getClientId(), principal) - .authorizationUri(authorizationRequest.getAuthorizationUri()) - .redirectUri(redirectUri) - .scopes(authorizedScopes) - .state(authorizationRequest.getState()) - .authorizationCode(authorizationCode) - .build(); + /** + * 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. * @@ -113,34 +163,24 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationToken extends Abs 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 + * @return the additional parameters, or an empty {@code Map} if not available */ public Map getAdditionalParameters() { return this.additionalParameters; } - /** - * Returns {@code true} if authorization consent is required, {@code false} otherwise. - * - * @return {@code true} if authorization consent is required, {@code false} otherwise - */ - public boolean isConsentRequired() { - return this.consentRequired; - } - - /** - * Returns {@code true} if this {@code Authentication} represents an authorization consent request, - * {@code false} otherwise. - * - * @return {@code true} if this {@code Authentication} represents an authorization consent request, {@code false} otherwise - */ - public boolean isConsent() { - return this.consent; - } - /** * Returns the {@link OAuth2AuthorizationCode}. * @@ -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 48eef455..1e9275c9 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/config/annotation/web/configurers/OidcConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcConfigurer.java index 9d8b9058..5ce9cbc3 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcConfigurer.java @@ -23,8 +23,12 @@ import java.util.Map; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContext; +import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder; +import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.web.util.UriComponentsBuilder; /** * Configurer for OpenID Connect 1.0 support. @@ -102,6 +106,25 @@ public final class OidcConfigurer extends AbstractOAuth2Configurer { @Override void configure(HttpSecurity httpSecurity) { + OidcClientRegistrationEndpointConfigurer clientRegistrationEndpointConfigurer = + getConfigurer(OidcClientRegistrationEndpointConfigurer.class); + if (clientRegistrationEndpointConfigurer != null) { + OidcProviderConfigurationEndpointConfigurer providerConfigurationEndpointConfigurer = + getConfigurer(OidcProviderConfigurationEndpointConfigurer.class); + + providerConfigurationEndpointConfigurer + .addDefaultProviderConfigurationCustomizer((builder) -> { + AuthorizationServerContext authorizationServerContext = AuthorizationServerContextHolder.getContext(); + String issuer = authorizationServerContext.getIssuer(); + AuthorizationServerSettings authorizationServerSettings = authorizationServerContext.getAuthorizationServerSettings(); + + String clientRegistrationEndpoint = UriComponentsBuilder.fromUriString(issuer) + .path(authorizationServerSettings.getOidcClientRegistrationEndpoint()).build().toUriString(); + + builder.clientRegistrationEndpoint(clientRegistrationEndpoint); + }); + } + this.configurers.values().forEach(configurer -> configurer.configure(httpSecurity)); } 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 ac2386b5..9d81d170 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 jakarta.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)); + Authentication authentication = this.authenticationConverter.convert(request); + if (authentication instanceof AbstractAuthenticationToken) { + ((AbstractAuthenticationToken) authentication) + .setDetails(this.authenticationDetailsSource.buildDetails(request)); + } + Authentication authenticationResult = this.authenticationManager.authenticate(authentication); - OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult = - (OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationManager.authenticate(authorizationCodeRequestAuthentication); - - 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 5a99602a..6431a3be 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 @@ -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)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("clientId cannot be empty"); - } - - @Test - public void withWhenPrincipalNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> OAuth2AuthorizationCodeRequestAuthenticationToken.with(REGISTERED_CLIENT.getClientId(), null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("principal cannot be null"); - } - - @Test - public void buildWhenAuthorizationUriNotProvidedThenThrowIllegalArgumentException() { + public void constructorWhenAuthorizationUriNotProvidedThenThrowIllegalArgumentException() { assertThatThrownBy(() -> - OAuth2AuthorizationCodeRequestAuthenticationToken.with(REGISTERED_CLIENT.getClientId(), PRINCIPAL) - .build()) + new OAuth2AuthorizationCodeRequestAuthenticationToken(null, REGISTERED_CLIENT.getClientId(), PRINCIPAL, + null, null, (Set) null, null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("authorizationUri cannot be empty"); } @Test - public void buildWhenStateNotProvidedThenThrowIllegalArgumentException() { + public void constructorWhenClientIdNotProvidedThenThrowIllegalArgumentException() { assertThatThrownBy(() -> - OAuth2AuthorizationCodeRequestAuthenticationToken.with(REGISTERED_CLIENT.getClientId(), PRINCIPAL) - .authorizationUri(AUTHORIZATION_URI) - .consent(true) - .build()) + new OAuth2AuthorizationCodeRequestAuthenticationToken(AUTHORIZATION_URI, null, PRINCIPAL, + null, null, (Set) null, null)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("state cannot be empty"); + .hasMessage("clientId cannot be empty"); } @Test - public void buildWhenAuthorizationCodeRequestThenValuesAreSet() { + public void constructorWhenPrincipalNotProvidedThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> + new OAuth2AuthorizationCodeRequestAuthenticationToken(AUTHORIZATION_URI, REGISTERED_CLIENT.getClientId(), null, + null, null, (Set) null, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("principal cannot be null"); + } + + @Test + public void constructorWhenAuthorizationCodeNotProvidedThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> + new OAuth2AuthorizationCodeRequestAuthenticationToken(AUTHORIZATION_URI, REGISTERED_CLIENT.getClientId(), PRINCIPAL, + null, null, null, (Set) null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("authorizationCode cannot be null"); + } + + @Test + 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 49a8cf55..c2dc6c27 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)); } @@ -933,7 +934,7 @@ public class OAuth2AuthorizationCodeGrantTests { new OAuth2AuthorizationServerConfigurer(); authorizationServerConfigurer .authorizationEndpoint(authorizationEndpoint -> - authorizationEndpoint.authenticationProvider(createProvider())); + authorizationEndpoint.authenticationProviders(configureAuthenticationProviders())); RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher(); http @@ -965,15 +966,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 { @@ -982,10 +982,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/config/annotation/web/configurers/OidcProviderConfigurationTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcProviderConfigurationTests.java new file mode 100644 index 00000000..035f5933 --- /dev/null +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcProviderConfigurationTests.java @@ -0,0 +1,347 @@ +/* + * 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.config.annotation.web.configurers; + +import java.util.function.Consumer; + +import org.junit.Rule; +import org.junit.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType; +import org.springframework.security.oauth2.core.oidc.OidcScopes; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadataClaimNames; +import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; +import org.springframework.security.oauth2.server.authorization.oidc.OidcProviderConfiguration; +import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; +import org.springframework.security.oauth2.server.authorization.test.SpringTestRule; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultMatcher; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.hamcrest.CoreMatchers.hasItems; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Integration tests for the OpenID Connect 1.0 Provider Configuration endpoint. + * + * @author Sahariar Alam Khandoker + * @author Joe Grandja + * @author Daniel Garnier-Moiroux + */ +public class OidcProviderConfigurationTests { + private static final String DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI = "/.well-known/openid-configuration"; + private static final String ISSUER_URL = "https://example.com/issuer1"; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Autowired + private AuthorizationServerSettings authorizationServerSettings; + + @Autowired + private MockMvc mvc; + + @Test + public void requestWhenConfigurationRequestAndIssuerSetThenReturnDefaultConfigurationResponse() throws Exception { + this.spring.register(AuthorizationServerConfiguration.class).autowire(); + + this.mvc.perform(get(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI)) + .andExpect(status().is2xxSuccessful()) + .andExpectAll(defaultConfigurationMatchers()); + } + + // gh-632 + @Test + public void requestWhenConfigurationRequestAndUserAuthenticatedThenReturnConfigurationResponse() throws Exception { + this.spring.register(AuthorizationServerConfiguration.class).autowire(); + + this.mvc.perform(get(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI) + .with(user("user"))) + .andExpect(status().is2xxSuccessful()) + .andExpectAll(defaultConfigurationMatchers()); + } + + // gh-616 + @Test + public void requestWhenConfigurationRequestAndConfigurationCustomizerSetThenReturnCustomConfigurationResponse() throws Exception { + this.spring.register(AuthorizationServerConfigurationWithProviderConfigurationCustomizer.class).autowire(); + + this.mvc.perform(get(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI)) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, + hasItems(OidcScopes.OPENID, OidcScopes.PROFILE, OidcScopes.EMAIL))); + } + + @Test + public void requestWhenConfigurationRequestAndClientRegistrationEnabledThenConfigurationResponseIncludesRegistrationEndpoint() throws Exception { + this.spring.register(AuthorizationServerConfigurationWithClientRegistrationEnabled.class).autowire(); + + this.mvc.perform(get(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI)) + .andExpect(status().is2xxSuccessful()) + .andExpectAll(defaultConfigurationMatchers()) + .andExpect(jsonPath("$.registration_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getOidcClientRegistrationEndpoint()))); + } + + private ResultMatcher[] defaultConfigurationMatchers() { + // @formatter:off + return new ResultMatcher[] { + jsonPath("issuer").value(ISSUER_URL), + jsonPath("authorization_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getAuthorizationEndpoint())), + jsonPath("token_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getTokenEndpoint())), + jsonPath("$.token_endpoint_auth_methods_supported[0]").value(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()), + jsonPath("$.token_endpoint_auth_methods_supported[1]").value(ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue()), + jsonPath("$.token_endpoint_auth_methods_supported[2]").value(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()), + jsonPath("$.token_endpoint_auth_methods_supported[3]").value(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue()), + jsonPath("jwks_uri").value(ISSUER_URL.concat(this.authorizationServerSettings.getJwkSetEndpoint())), + jsonPath("userinfo_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getOidcUserInfoEndpoint())), + jsonPath("response_types_supported").value(OAuth2AuthorizationResponseType.CODE.getValue()), + jsonPath("$.grant_types_supported[0]").value(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()), + jsonPath("$.grant_types_supported[1]").value(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()), + jsonPath("$.grant_types_supported[2]").value(AuthorizationGrantType.REFRESH_TOKEN.getValue()), + jsonPath("revocation_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getTokenRevocationEndpoint())), + jsonPath("$.revocation_endpoint_auth_methods_supported[0]").value(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()), + jsonPath("$.revocation_endpoint_auth_methods_supported[1]").value(ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue()), + jsonPath("$.revocation_endpoint_auth_methods_supported[2]").value(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()), + jsonPath("$.revocation_endpoint_auth_methods_supported[3]").value(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue()), + jsonPath("introspection_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getTokenIntrospectionEndpoint())), + jsonPath("$.introspection_endpoint_auth_methods_supported[0]").value(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()), + jsonPath("$.introspection_endpoint_auth_methods_supported[1]").value(ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue()), + jsonPath("$.introspection_endpoint_auth_methods_supported[2]").value(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()), + jsonPath("$.introspection_endpoint_auth_methods_supported[3]").value(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue()), + jsonPath("subject_types_supported").value("public"), + jsonPath("id_token_signing_alg_values_supported").value(SignatureAlgorithm.RS256.getName()), + jsonPath("scopes_supported").value(OidcScopes.OPENID) + }; + // @formatter:on + } + + @Test + public void loadContextWhenIssuerNotValidUrlThenThrowException() { + assertThatThrownBy( + () -> this.spring.register(AuthorizationServerConfigurationWithInvalidIssuerUrl.class).autowire() + ); + } + + @Test + public void loadContextWhenIssuerNotValidUriThenThrowException() { + assertThatThrownBy( + () -> this.spring.register(AuthorizationServerConfigurationWithInvalidIssuerUri.class).autowire() + ); + } + + @Test + public void loadContextWhenIssuerWithQueryThenThrowException() { + assertThatThrownBy( + () -> this.spring.register(AuthorizationServerConfigurationWithIssuerQuery.class).autowire() + ); + } + + @Test + public void loadContextWhenIssuerWithFragmentThenThrowException() { + assertThatThrownBy( + () -> this.spring.register(AuthorizationServerConfigurationWithIssuerFragment.class).autowire() + ); + } + + @Test + public void loadContextWhenIssuerWithQueryAndFragmentThenThrowException() { + assertThatThrownBy( + () -> this.spring.register(AuthorizationServerConfigurationWithIssuerQueryAndFragment.class).autowire() + ); + } + + @Test + public void loadContextWhenIssuerWithEmptyQueryThenThrowException() { + assertThatThrownBy( + () -> this.spring.register(AuthorizationServerConfigurationWithIssuerEmptyQuery.class).autowire() + ); + } + + @Test + public void loadContextWhenIssuerWithEmptyFragmentThenThrowException() { + assertThatThrownBy( + () -> this.spring.register(AuthorizationServerConfigurationWithIssuerEmptyFragment.class).autowire() + ); + } + + @EnableWebSecurity + @Import(OAuth2AuthorizationServerConfiguration.class) + static class AuthorizationServerConfiguration { + + @Bean + RegisteredClientRepository registeredClientRepository() { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + return new InMemoryRegisteredClientRepository(registeredClient); + } + + @Bean + AuthorizationServerSettings authorizationServerSettings() { + return AuthorizationServerSettings.builder() + .issuer(ISSUER_URL) + .build(); + } + + } + + @Configuration + @EnableWebSecurity + static class AuthorizationServerConfigurationWithProviderConfigurationCustomizer extends AuthorizationServerConfiguration { + + // @formatter:off + @Bean + public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { + OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = + new OAuth2AuthorizationServerConfigurer(); + http.apply(authorizationServerConfigurer); + + authorizationServerConfigurer + .oidc(oidc -> + oidc.providerConfigurationEndpoint(providerConfigurationEndpoint -> + providerConfigurationEndpoint + .providerConfigurationCustomizer(providerConfigurationCustomizer()))); + + RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher(); + + http + .requestMatcher(endpointsMatcher) + .authorizeRequests(authorizeRequests -> + authorizeRequests.anyRequest().authenticated() + ) + .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher)); + + return http.build(); + } + // @formatter:on + + private Consumer providerConfigurationCustomizer() { + return (providerConfiguration) -> + providerConfiguration.scope(OidcScopes.PROFILE).scope(OidcScopes.EMAIL); + } + + } + + @Configuration + @EnableWebSecurity + static class AuthorizationServerConfigurationWithClientRegistrationEnabled extends AuthorizationServerConfiguration { + + // @formatter:off + @Bean + SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { + OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = + new OAuth2AuthorizationServerConfigurer(); + http.apply(authorizationServerConfigurer); + + authorizationServerConfigurer + .oidc(oidc -> + oidc.clientRegistrationEndpoint(Customizer.withDefaults()) + ); + + return http.build(); + } + // @formatter:on + + } + + @EnableWebSecurity + @Import(OAuth2AuthorizationServerConfiguration.class) + static class AuthorizationServerConfigurationWithInvalidIssuerUrl extends AuthorizationServerConfiguration { + + @Bean + AuthorizationServerSettings authorizationServerSettings() { + return AuthorizationServerSettings.builder().issuer("urn:example").build(); + } + } + + @EnableWebSecurity + @Import(OAuth2AuthorizationServerConfiguration.class) + static class AuthorizationServerConfigurationWithInvalidIssuerUri extends AuthorizationServerConfiguration { + + @Bean + AuthorizationServerSettings authorizationServerSettings() { + return AuthorizationServerSettings.builder().issuer("https://not a valid uri").build(); + } + } + + @EnableWebSecurity + @Import(OAuth2AuthorizationServerConfiguration.class) + static class AuthorizationServerConfigurationWithIssuerQuery extends AuthorizationServerConfiguration { + + @Bean + AuthorizationServerSettings authorizationServerSettings() { + return AuthorizationServerSettings.builder().issuer(ISSUER_URL + "?param=value").build(); + } + } + + @EnableWebSecurity + @Import(OAuth2AuthorizationServerConfiguration.class) + static class AuthorizationServerConfigurationWithIssuerFragment extends AuthorizationServerConfiguration { + + @Bean + AuthorizationServerSettings authorizationServerSettings() { + return AuthorizationServerSettings.builder().issuer(ISSUER_URL + "#fragment").build(); + } + } + + @EnableWebSecurity + @Import(OAuth2AuthorizationServerConfiguration.class) + static class AuthorizationServerConfigurationWithIssuerQueryAndFragment extends AuthorizationServerConfiguration { + + @Bean + AuthorizationServerSettings authorizationServerSettings() { + return AuthorizationServerSettings.builder().issuer(ISSUER_URL + "?param=value#fragment").build(); + } + } + + @EnableWebSecurity + @Import(OAuth2AuthorizationServerConfiguration.class) + static class AuthorizationServerConfigurationWithIssuerEmptyQuery extends AuthorizationServerConfiguration { + + @Bean + AuthorizationServerSettings authorizationServerSettings() { + return AuthorizationServerSettings.builder().issuer(ISSUER_URL + "?").build(); + } + } + + @EnableWebSecurity + @Import(OAuth2AuthorizationServerConfiguration.class) + static class AuthorizationServerConfigurationWithIssuerEmptyFragment extends AuthorizationServerConfiguration { + + @Bean + AuthorizationServerSettings authorizationServerSettings() { + return AuthorizationServerSettings.builder().issuer(ISSUER_URL + "#").build(); + } + } + +} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcTests.java index 40d3fe67..ce26ed9d 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcTests.java @@ -24,7 +24,6 @@ import java.util.Base64; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.function.Consumer; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.source.JWKSource; @@ -37,7 +36,6 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -71,7 +69,6 @@ import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode; -import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadataClaimNames; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations; @@ -82,8 +79,6 @@ import org.springframework.security.oauth2.server.authorization.client.Registere import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; import org.springframework.security.oauth2.server.authorization.jackson2.TestingAuthenticationTokenMixin; -import org.springframework.security.oauth2.server.authorization.oidc.OidcProviderConfiguration; -import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; import org.springframework.security.oauth2.server.authorization.test.SpringTestRule; import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator; import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext; @@ -103,9 +98,7 @@ import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.CoreMatchers.hasItems; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; @@ -126,8 +119,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. public class OidcTests { private static final String DEFAULT_AUTHORIZATION_ENDPOINT_URI = "/oauth2/authorize"; private static final String DEFAULT_TOKEN_ENDPOINT_URI = "/oauth2/token"; - private static final String DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI = "/.well-known/openid-configuration"; - private static final String ISSUER_URL = "https://example.com/issuer1"; private static final String AUTHORITIES_CLAIM = "authorities"; private static final OAuth2TokenType AUTHORIZATION_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.CODE); private static EmbeddedDatabase db; @@ -182,85 +173,6 @@ public class OidcTests { db.shutdown(); } - @Test - public void requestWhenConfigurationRequestAndIssuerSetThenReturnConfigurationResponse() throws Exception { - this.spring.register(AuthorizationServerConfigurationWithIssuer.class).autowire(); - - this.mvc.perform(get(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI)) - .andExpect(status().is2xxSuccessful()) - .andExpect(jsonPath("issuer").value(ISSUER_URL)); - } - - // gh-632 - @Test - public void requestWhenConfigurationRequestAndUserAuthenticatedThenReturnConfigurationResponse() throws Exception { - this.spring.register(AuthorizationServerConfiguration.class).autowire(); - - this.mvc.perform(get(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI) - .with(user("user"))) - .andExpect(status().is2xxSuccessful()); - } - - // gh-616 - @Test - public void requestWhenConfigurationRequestAndConfigurationCustomizerSetThenReturnCustomConfigurationResponse() throws Exception { - this.spring.register(AuthorizationServerConfigurationWithProviderConfigurationCustomizer.class).autowire(); - - this.mvc.perform(get(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI)) - .andExpect(status().is2xxSuccessful()) - .andExpect(jsonPath(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, - hasItems(OidcScopes.OPENID, OidcScopes.PROFILE, OidcScopes.EMAIL))); - } - - @Test - public void loadContextWhenIssuerNotValidUrlThenThrowException() { - assertThatThrownBy( - () -> this.spring.register(AuthorizationServerConfigurationWithInvalidIssuerUrl.class).autowire() - ); - } - - @Test - public void loadContextWhenIssuerNotValidUriThenThrowException() { - assertThatThrownBy( - () -> this.spring.register(AuthorizationServerConfigurationWithInvalidIssuerUri.class).autowire() - ); - } - - @Test - public void loadContextWhenIssuerWithQueryThenThrowException() { - assertThatThrownBy( - () -> this.spring.register(AuthorizationServerConfigurationWithIssuerQuery.class).autowire() - ); - } - - @Test - public void loadContextWhenIssuerWithFragmentThenThrowException() { - assertThatThrownBy( - () -> this.spring.register(AuthorizationServerConfigurationWithIssuerFragment.class).autowire() - ); - } - - @Test - public void loadContextWhenIssuerWithQueryAndFragmentThenThrowException() { - assertThatThrownBy( - () -> this.spring.register(AuthorizationServerConfigurationWithIssuerQueryAndFragment.class).autowire() - ); - } - - @Test - public void loadContextWhenIssuerWithEmptyQueryThenThrowException() { - assertThatThrownBy( - () -> this.spring.register(AuthorizationServerConfigurationWithIssuerEmptyQuery.class).autowire() - ); - } - - @Test - public void loadContextWhenIssuerWithEmptyFragmentThenThrowException() { - assertThatThrownBy( - () -> this.spring.register(AuthorizationServerConfigurationWithIssuerEmptyFragment.class).autowire() - ); - } - @Test public void requestWhenAuthenticationRequestThenTokenResponseIncludesIdToken() throws Exception { this.spring.register(AuthorizationServerConfiguration.class).autowire(); @@ -481,121 +393,4 @@ public class OidcTests { } - @Configuration - @EnableWebSecurity - static class AuthorizationServerConfigurationWithProviderConfigurationCustomizer extends AuthorizationServerConfiguration { - - // @formatter:off - @Bean - public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { - OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = - new OAuth2AuthorizationServerConfigurer(); - http.apply(authorizationServerConfigurer); - - authorizationServerConfigurer - .oidc(oidc -> - oidc.providerConfigurationEndpoint(providerConfigurationEndpoint -> - providerConfigurationEndpoint - .providerConfigurationCustomizer(providerConfigurationCustomizer()))); - - RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher(); - - http - .requestMatcher(endpointsMatcher) - .authorizeRequests(authorizeRequests -> - authorizeRequests.anyRequest().authenticated() - ) - .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher)); - - return http.build(); - } - // @formatter:on - - private Consumer providerConfigurationCustomizer() { - return (providerConfiguration) -> - providerConfiguration.scope(OidcScopes.PROFILE).scope(OidcScopes.EMAIL); - } - - } - - @EnableWebSecurity - @Import(OAuth2AuthorizationServerConfiguration.class) - static class AuthorizationServerConfigurationWithIssuer extends AuthorizationServerConfiguration { - - @Bean - AuthorizationServerSettings authorizationServerSettings() { - return AuthorizationServerSettings.builder().issuer(ISSUER_URL).build(); - } - } - - @EnableWebSecurity - @Import(OAuth2AuthorizationServerConfiguration.class) - static class AuthorizationServerConfigurationWithInvalidIssuerUrl extends AuthorizationServerConfiguration { - - @Bean - AuthorizationServerSettings authorizationServerSettings() { - return AuthorizationServerSettings.builder().issuer("urn:example").build(); - } - } - - @EnableWebSecurity - @Import(OAuth2AuthorizationServerConfiguration.class) - static class AuthorizationServerConfigurationWithInvalidIssuerUri extends AuthorizationServerConfiguration { - - @Bean - AuthorizationServerSettings authorizationServerSettings() { - return AuthorizationServerSettings.builder().issuer("https://not a valid uri").build(); - } - } - - @EnableWebSecurity - @Import(OAuth2AuthorizationServerConfiguration.class) - static class AuthorizationServerConfigurationWithIssuerQuery extends AuthorizationServerConfiguration { - - @Bean - AuthorizationServerSettings authorizationServerSettings() { - return AuthorizationServerSettings.builder().issuer(ISSUER_URL + "?param=value").build(); - } - } - - @EnableWebSecurity - @Import(OAuth2AuthorizationServerConfiguration.class) - static class AuthorizationServerConfigurationWithIssuerFragment extends AuthorizationServerConfiguration { - - @Bean - AuthorizationServerSettings authorizationServerSettings() { - return AuthorizationServerSettings.builder().issuer(ISSUER_URL + "#fragment").build(); - } - } - - @EnableWebSecurity - @Import(OAuth2AuthorizationServerConfiguration.class) - static class AuthorizationServerConfigurationWithIssuerQueryAndFragment extends AuthorizationServerConfiguration { - - @Bean - AuthorizationServerSettings authorizationServerSettings() { - return AuthorizationServerSettings.builder().issuer(ISSUER_URL + "?param=value#fragment").build(); - } - } - - @EnableWebSecurity - @Import(OAuth2AuthorizationServerConfiguration.class) - static class AuthorizationServerConfigurationWithIssuerEmptyQuery extends AuthorizationServerConfiguration { - - @Bean - AuthorizationServerSettings authorizationServerSettings() { - return AuthorizationServerSettings.builder().issuer(ISSUER_URL + "?").build(); - } - } - - @EnableWebSecurity - @Import(OAuth2AuthorizationServerConfiguration.class) - static class AuthorizationServerConfigurationWithIssuerEmptyFragment extends AuthorizationServerConfiguration { - - @Bean - AuthorizationServerSettings authorizationServerSettings() { - return AuthorizationServerSettings.builder().issuer(ISSUER_URL + "#").build(); - } - } - } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderConfigurationTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderConfigurationTests.java index dff34428..ce6d83ff 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderConfigurationTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderConfigurationTests.java @@ -61,6 +61,7 @@ public class OidcProviderConfigurationTests { .idTokenSigningAlgorithm("RS256") .userInfoEndpoint("https://example.com/issuer1/userinfo") .tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()) + .clientRegistrationEndpoint("https://example.com/issuer1/connect/register") .claim("a-claim", "a-value") .build(); @@ -75,6 +76,7 @@ public class OidcProviderConfigurationTests { assertThat(providerConfiguration.getIdTokenSigningAlgorithms()).containsExactly("RS256"); assertThat(providerConfiguration.getUserInfoEndpoint()).isEqualTo(url("https://example.com/issuer1/userinfo")); assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).containsExactly(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()); + assertThat(providerConfiguration.getClientRegistrationEndpoint()).isEqualTo(url("https://example.com/issuer1/connect/register")); assertThat(providerConfiguration.getClaim("a-claim")).isEqualTo("a-value"); } @@ -115,6 +117,7 @@ public class OidcProviderConfigurationTests { claims.put(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED, Collections.singletonList("public")); claims.put(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED, Collections.singletonList("RS256")); claims.put(OidcProviderMetadataClaimNames.USER_INFO_ENDPOINT, "https://example.com/issuer1/userinfo"); + claims.put(OidcProviderMetadataClaimNames.REGISTRATION_ENDPOINT, "https://example.com/issuer1/connect/register"); claims.put("some-claim", "some-value"); OidcProviderConfiguration providerConfiguration = OidcProviderConfiguration.withClaims(claims).build(); @@ -130,6 +133,7 @@ public class OidcProviderConfigurationTests { assertThat(providerConfiguration.getIdTokenSigningAlgorithms()).containsExactly("RS256"); assertThat(providerConfiguration.getUserInfoEndpoint()).isEqualTo(url("https://example.com/issuer1/userinfo")); assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).isNull(); + assertThat(providerConfiguration.getClientRegistrationEndpoint()).isEqualTo(url("https://example.com/issuer1/connect/register")); assertThat(providerConfiguration.getClaim("some-claim")).isEqualTo("some-value"); } @@ -145,6 +149,7 @@ public class OidcProviderConfigurationTests { claims.put(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED, Collections.singletonList("public")); claims.put(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED, Collections.singletonList("RS256")); claims.put(OidcProviderMetadataClaimNames.USER_INFO_ENDPOINT, url("https://example.com/issuer1/userinfo")); + claims.put(OidcProviderMetadataClaimNames.REGISTRATION_ENDPOINT, url("https://example.com/issuer1/connect/register")); claims.put("some-claim", "some-value"); OidcProviderConfiguration providerConfiguration = OidcProviderConfiguration.withClaims(claims).build(); @@ -160,6 +165,7 @@ public class OidcProviderConfigurationTests { assertThat(providerConfiguration.getIdTokenSigningAlgorithms()).containsExactly("RS256"); assertThat(providerConfiguration.getUserInfoEndpoint()).isEqualTo(url("https://example.com/issuer1/userinfo")); assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).isNull(); + assertThat(providerConfiguration.getClientRegistrationEndpoint()).isEqualTo(url("https://example.com/issuer1/connect/register")); assertThat(providerConfiguration.getClaim("some-claim")).isEqualTo("some-value"); } @@ -396,6 +402,16 @@ public class OidcProviderConfigurationTests { .withMessage("userInfoEndpoint must be a valid URL"); } + @Test + public void buildWhenClientRegistrationEndpointNotUrlThenThrowIllegalArgumentException() { + OidcProviderConfiguration.Builder builder = this.minimalConfigurationBuilder + .claims((claims) -> claims.put(OidcProviderMetadataClaimNames.REGISTRATION_ENDPOINT, "not an url")); + + assertThatIllegalArgumentException() + .isThrownBy(builder::build) + .withMessage("clientRegistrationEndpoint must be a valid URL"); + } + @Test public void responseTypesWhenAddingOrRemovingThenCorrectValues() { OidcProviderConfiguration configuration = this.minimalConfigurationBuilder 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 b49157f9..e9075c92 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( "",