diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java index 736cc479..e2340f11 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java @@ -49,7 +49,7 @@ import org.springframework.security.oauth2.server.authorization.config.ProviderS import org.springframework.security.oauth2.server.authorization.oidc.web.OidcProviderConfigurationEndpointFilter; import org.springframework.security.oauth2.server.authorization.web.NimbusJwkSetEndpointFilter; import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter; -import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationServerConfigurationEndpointFilter; +import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationServerMetadataEndpointFilter; import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientAuthenticationFilter; import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter; import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenIntrospectionEndpointFilter; @@ -79,6 +79,7 @@ import org.springframework.util.StringUtils; * @see OAuth2TokenRevocationEndpointFilter * @see NimbusJwkSetEndpointFilter * @see OidcProviderConfigurationEndpointFilter + * @see OAuth2AuthorizationServerMetadataEndpointFilter * @see OAuth2ClientAuthenticationFilter */ public final class OAuth2AuthorizationServerConfigurer> @@ -90,7 +91,7 @@ public final class OAuth2AuthorizationServerConfigurer this.authorizationEndpointMatcher.matches(request) || this.tokenEndpointMatcher.matches(request) || @@ -98,7 +99,7 @@ public final class OAuth2AuthorizationServerConfigurer jwkSource = getJwkSource(builder); @@ -284,8 +285,8 @@ public final class OAuth2AuthorizationServerConfigurer4.2. OpenID Provider Configuration Response - * @see 3.2. Authorization Server Metadata Response - */ -public abstract class AbstractOAuth2AuthorizationServerConfiguration implements OAuth2AuthorizationServerMetadataClaimAccessor, Serializable { - private static final long serialVersionUID = Version.SERIAL_VERSION_UID; - - protected final Map claims; - - protected AbstractOAuth2AuthorizationServerConfiguration(Map claims) { - Assert.notEmpty(claims, "claims cannot be empty"); - this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims)); - } - - /** - * Returns the Authorization Server metadata. - * - * @return a {@code Map} of the metadata values - */ - @Override - public Map getClaims() { - return this.claims; - } - - /** - * An abstract builder for subclasses of {@link AbstractOAuth2AuthorizationServerConfiguration}. - */ - protected static abstract class AbstractBuilder> { - - protected final Map claims = new LinkedHashMap<>(); - - protected AbstractBuilder() { - } - - @SuppressWarnings("unchecked") - protected B getThis() { return (B) this; }; // avoid unchecked casts in subclasses by using "getThis()" instead of "(B) this" - - /** - * Use this {@code issuer} in the resulting {@link AbstractOAuth2AuthorizationServerConfiguration}, REQUIRED. - * - * @param issuer the {@code URL} of the Authorization Server's Issuer Identifier - * @return the {@link AbstractBuilder} for further configuration - */ - public B issuer(String issuer) { - return claim(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, issuer); - } - - /** - * Use this {@code authorization_endpoint} in the resulting {@link AbstractOAuth2AuthorizationServerConfiguration}, REQUIRED. - * - * @param authorizationEndpoint the {@code URL} of the Authorization Server's OAuth 2.0 Authorization Endpoint - * @return the {@link AbstractBuilder} for further configuration - */ - public B authorizationEndpoint(String authorizationEndpoint) { - return claim(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, authorizationEndpoint); - } - - /** - * Use this {@code token_endpoint} in the resulting {@link AbstractOAuth2AuthorizationServerConfiguration}, REQUIRED. - * - * @param tokenEndpoint the {@code URL} of the Authorization Server's OAuth 2.0 Token Endpoint - * @return the {@link AbstractBuilder} for further configuration - */ - public B tokenEndpoint(String tokenEndpoint) { - return claim(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, tokenEndpoint); - } - - /** - * Add this Authentication Method to the collection of {@code token_endpoint_auth_methods_supported} - * in the resulting {@link AbstractOAuth2AuthorizationServerConfiguration}, OPTIONAL. - * - * @param authenticationMethod the OAuth 2.0 Authentication Method supported by the Token Endpoint - * @return the {@link AbstractBuilder} for further configuration - */ - public B tokenEndpointAuthenticationMethod(String authenticationMethod) { - addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, authenticationMethod); - return getThis(); - } - - /** - * A {@code Consumer} of the Token Endpoint Authentication Method(s) allowing the ability to add, replace, or remove. - * - * @param authenticationMethodsConsumer a {@code Consumer} of the Token Endpoint Authentication Method(s) - * @return the {@link AbstractBuilder} for further configuration - */ - public B tokenEndpointAuthenticationMethods(Consumer> authenticationMethodsConsumer) { - acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, authenticationMethodsConsumer); - return getThis(); - } - - /** - * Use this {@code jwks_uri} in the resulting {@link AbstractOAuth2AuthorizationServerConfiguration}, REQUIRED. - * - * @param jwkSetUri the {@code URL} of the Authorization Server's JSON Web Key Set document - * @return the {@link AbstractBuilder} for further configuration - */ - public B jwkSetUri(String jwkSetUri) { - return claim(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI, jwkSetUri); - } - - /** - * Add this Response Type to the collection of {@code response_types_supported} in the resulting - * {@link AbstractOAuth2AuthorizationServerConfiguration}. - * - * @param responseType the OAuth 2.0 {@code response_type} value that the Authorization Server supports - * @return the {@link AbstractBuilder} for further configuration - */ - public B responseType(String responseType) { - addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, responseType); - return getThis(); - } - - /** - * A {@code Consumer} of the Response Type(s) allowing the ability to add, replace, or remove. - * - * @param responseTypesConsumer a {@code Consumer} of the Response Type(s) - * @return the {@link AbstractBuilder} for further configuration - */ - public B responseTypes(Consumer> responseTypesConsumer) { - acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, responseTypesConsumer); - return getThis(); - } - - /** - * Add this Grant Type to the collection of {@code grant_types_supported} in the resulting - * {@link AbstractOAuth2AuthorizationServerConfiguration}, OPTIONAL. - * - * @param grantType the OAuth 2.0 {@code grant_type} value that the Authorization Server supports - * @return the {@link AbstractBuilder} for further configuration - */ - public B grantType(String grantType) { - addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, grantType); - return getThis(); - } - - /** - * A {@code Consumer} of the Grant Type(s) allowing the ability to add, replace, or remove. - * - * @param grantTypesConsumer a {@code Consumer} of the Grant Type(s) - * @return the {@link AbstractBuilder} for further configuration - */ - public B grantTypes(Consumer> grantTypesConsumer) { - acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, grantTypesConsumer); - return getThis(); - } - - /** - * Add this Scope to the collection of {@code scopes_supported} in the resulting - * {@link AbstractOAuth2AuthorizationServerConfiguration}, RECOMMENDED. - * - * @param scope the OAuth 2.0 {@code scope} value that the Authorization Server supports - * @return the {@link AbstractBuilder} for further configuration - */ - public B scope(String scope) { - addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, scope); - return getThis(); - } - - /** - * A {@code Consumer} of the Scopes(s) allowing the ability to add, replace, or remove. - * - * @param scopesConsumer a {@code Consumer} of the Scopes(s) - * @return the {@link AbstractBuilder} for further configuration - */ - public B scopes(Consumer> scopesConsumer) { - acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, scopesConsumer); - return getThis(); - } - - /** - * Use this claim in the resulting {@link AbstractOAuth2AuthorizationServerConfiguration} - * - * @param name the claim name - * @param value the claim value - * @return the {@link AbstractBuilder} for further configuration - */ - public B claim(String name, Object value) { - Assert.hasText(name, "name cannot be empty"); - Assert.notNull(value, "value cannot be null"); - this.claims.put(name, value); - return getThis(); - } - - /** - * Provides access to every {@link #claim(String, Object)} declared so far with - * the possibility to add, replace, or remove. - * - * @param claimsConsumer a {@code Consumer} of the claims - * @return the {@link AbstractBuilder} for further configurations - */ - public B claims(Consumer> claimsConsumer) { - claimsConsumer.accept(this.claims); - return getThis(); - } - - /** - * Use this {@code revocation_endpoint} in the resulting {@link AbstractOAuth2AuthorizationServerConfiguration}, OPTIONAL. - * - * @param tokenRevocationEndpoint the {@code URL} of the OAuth 2.0 Authorization Server's Token Revocation Endpoint - * @return the {@link AbstractBuilder} for further configuration - */ - public B tokenRevocationEndpoint(String tokenRevocationEndpoint) { - return claim(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT, tokenRevocationEndpoint); - } - - /** - * Add this Authentication Method to the collection of {@code revocation_endpoint_auth_methods_supported} - * in the resulting {@link AbstractOAuth2AuthorizationServerConfiguration}, OPTIONAL. - * - * @param authenticationMethod the OAuth 2.0 Authentication Method supported by the Revocation Endpoint - * @return the {@link AbstractBuilder} for further configuration - */ - public B tokenRevocationEndpointAuthenticationMethod(String authenticationMethod) { - addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED, authenticationMethod); - return getThis(); - } - - /** - * A {@code Consumer} of the Token Revocation Endpoint Authentication Method(s) allowing the ability to add, - * replace, or remove. - * - * @param authenticationMethodsConsumer a {@code Consumer} of the OAuth 2.0 Token Revocation Endpoint Authentication Method(s) - * @return the {@link AbstractBuilder} for further configuration - */ - public B tokenRevocationEndpointAuthenticationMethods(Consumer> authenticationMethodsConsumer) { - acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED, authenticationMethodsConsumer); - return getThis(); - } - - /** - * Add this Proof Key for Code Exchange (PKCE) Code Challenge Method to the collection of - * {@code code_challenge_methods_supported} in the resulting {@link AbstractOAuth2AuthorizationServerConfiguration}, OPTIONAL. - * - * @param codeChallengeMethod the Proof Key for Code Exchange (PKCE) Code Challenge Method - * supported by the Authorization Server - * @return the {@link AbstractBuilder} for further configuration - */ - public B codeChallengeMethod(String codeChallengeMethod) { - addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED, codeChallengeMethod); - return getThis(); - } - - /** - * A {@code Consumer} of the Proof Key for Code Exchange (PKCE) Code Challenge Method(s) allowing - * the ability to add, replace, or remove. - * - * @param codeChallengeMethodsConsumer a {@code Consumer} of the Proof Key for Code Exchange (PKCE) - * Code Challenge Method(s) - * @return the {@link AbstractBuilder} for further configuration - */ - public B codeChallengeMethods(Consumer> codeChallengeMethodsConsumer) { - acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED, codeChallengeMethodsConsumer); - return getThis(); - } - - /** - * Creates the {@link AbstractOAuth2AuthorizationServerConfiguration}. - * - * @return the {@link AbstractOAuth2AuthorizationServerConfiguration} - */ - public abstract T build(); - - protected void validateCommonClaims() { - Assert.notNull(this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.ISSUER), "issuer cannot be null"); - validateURL(this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.ISSUER), "issuer must be a valid URL"); - Assert.notNull(this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT), "authorizationEndpoint cannot be null"); - validateURL(this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT), "authorizationEndpoint must be a valid URL"); - Assert.notNull(this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT), "tokenEndpoint cannot be null"); - validateURL(this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT), "tokenEndpoint must be a valid URL"); - Assert.notNull(this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI), "jwksUri cannot be null"); - validateURL(this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI), "jwksUri must be a valid URL"); - Assert.notNull(this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED), "responseTypes cannot be null"); - Assert.isInstanceOf(List.class, this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED), "responseTypes must be of type List"); - Assert.notEmpty((List) this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED), "responseTypes cannot be empty"); - if (this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT) != null) { - validateURL(this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT), "tokenRevocationEndpoint must be a valid URL"); - } - } - - /** - * Remove claims of type Collection that have a size of zero. - *

- * Both 3.2. Authorization Server Metadata Response - * and 4.2. OpenID Provider Configuration Response - * state "Claims with zero elements MUST be omitted from the response." - */ - protected void removeEmptyClaims() { - Set claimsToRemove = this.claims.entrySet() - .stream() - .filter(entry -> entry.getValue() != null) - .filter(entry -> Collection.class.isAssignableFrom(entry.getValue().getClass())) - .filter(entry -> ((Collection) entry.getValue()).size() == 0) - .map(Map.Entry::getKey) - .collect(Collectors.toSet()); - - for (String claimToRemove : claimsToRemove) { - this.claims.remove(claimToRemove); - } - } - - protected static void validateURL(Object url, String errorMessage) { - if (URL.class.isAssignableFrom(url.getClass())) { - return; - } - - try { - new URI(url.toString()).toURL(); - } catch (Exception ex) { - throw new IllegalArgumentException(errorMessage, ex); - } - } - - @SuppressWarnings("unchecked") - protected void addClaimToClaimList(String name, String value) { - Assert.hasText(name, "name cannot be empty"); - Assert.notNull(value, "value cannot be null"); - this.claims.computeIfAbsent(name, k -> new LinkedList()); - ((List) this.claims.get(name)).add(value); - } - - @SuppressWarnings("unchecked") - protected void acceptClaimValues(String name, Consumer> valuesConsumer) { - Assert.hasText(name, "name cannot be empty"); - Assert.notNull(valuesConsumer, "valuesConsumer cannot be null"); - this.claims.computeIfAbsent(name, k -> new LinkedList()); - List values = (List) this.claims.get(name); - valuesConsumer.accept(values); - } - } -} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/AbstractOAuth2AuthorizationServerMetadata.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/AbstractOAuth2AuthorizationServerMetadata.java new file mode 100644 index 00000000..76a8406d --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/AbstractOAuth2AuthorizationServerMetadata.java @@ -0,0 +1,407 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.core; + +import java.io.Serializable; +import java.net.URI; +import java.net.URL; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import org.springframework.util.Assert; + +/** + * A base representation of OAuth 2.0 Authorization Server metadata, + * returned by an endpoint defined in OAuth 2.0 Authorization Server Metadata and OpenID Connect Discovery 1.0. + * The metadata endpoint returns a set of claims an Authorization Server describes about its configuration. + * + * @author Daniel Garnier-Moiroux + * @see OAuth2AuthorizationServerMetadataClaimAccessor + * @since 0.1.1 + * @see 3.2. Authorization Server Metadata Response + * @see 4.2. OpenID Provider Configuration Response + */ +public abstract class AbstractOAuth2AuthorizationServerMetadata implements OAuth2AuthorizationServerMetadataClaimAccessor, Serializable { + private static final long serialVersionUID = Version.SERIAL_VERSION_UID; + private final Map claims; + + protected AbstractOAuth2AuthorizationServerMetadata(Map claims) { + Assert.notEmpty(claims, "claims cannot be empty"); + this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims)); + } + + /** + * Returns the metadata as claims. + * + * @return a {@code Map} of the metadata as claims + */ + @Override + public Map getClaims() { + return this.claims; + } + + /** + * A builder for subclasses of {@link AbstractOAuth2AuthorizationServerMetadata}. + */ + protected static abstract class AbstractBuilder> { + private final Map claims = new LinkedHashMap<>(); + + protected AbstractBuilder() { + } + + protected Map getClaims() { + return this.claims; + } + + @SuppressWarnings("unchecked") + protected final B getThis() { + return (B) this; // avoid unchecked casts in subclasses by using "getThis()" instead of "(B) this" + }; + + /** + * Use this {@code issuer} in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, REQUIRED. + * + * @param issuer the {@code URL} of the Authorization Server's Issuer Identifier + * @return the {@link AbstractBuilder} for further configuration + */ + public B issuer(String issuer) { + return claim(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, issuer); + } + + /** + * Use this {@code authorization_endpoint} in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, REQUIRED. + * + * @param authorizationEndpoint the {@code URL} of the OAuth 2.0 Authorization Endpoint + * @return the {@link AbstractBuilder} for further configuration + */ + public B authorizationEndpoint(String authorizationEndpoint) { + return claim(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, authorizationEndpoint); + } + + /** + * Use this {@code token_endpoint} in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, REQUIRED. + * + * @param tokenEndpoint the {@code URL} of the OAuth 2.0 Token Endpoint + * @return the {@link AbstractBuilder} for further configuration + */ + public B tokenEndpoint(String tokenEndpoint) { + return claim(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, tokenEndpoint); + } + + /** + * Add this client authentication method to the collection of {@code token_endpoint_auth_methods_supported} + * in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL. + * + * @param authenticationMethod the client authentication method supported by the OAuth 2.0 Token Endpoint + * @return the {@link AbstractBuilder} for further configuration + */ + public B tokenEndpointAuthenticationMethod(String authenticationMethod) { + addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, authenticationMethod); + return getThis(); + } + + /** + * A {@code Consumer} of the client authentication method(s) allowing the ability to add, replace, or remove. + * + * @param authenticationMethodsConsumer a {@code Consumer} of the client authentication method(s) supported by the OAuth 2.0 Token Endpoint + * @return the {@link AbstractBuilder} for further configuration + */ + public B tokenEndpointAuthenticationMethods(Consumer> authenticationMethodsConsumer) { + acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, authenticationMethodsConsumer); + return getThis(); + } + + /** + * Use this {@code jwks_uri} in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL. + * + * @param jwkSetUrl the {@code URL} of the JSON Web Key Set + * @return the {@link AbstractBuilder} for further configuration + */ + public B jwkSetUrl(String jwkSetUrl) { + return claim(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI, jwkSetUrl); + } + + /** + * Add this OAuth 2.0 {@code scope} to the collection of {@code scopes_supported} + * in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, RECOMMENDED. + * + * @param scope the OAuth 2.0 {@code scope} value supported + * @return the {@link AbstractBuilder} for further configuration + */ + public B scope(String scope) { + addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, scope); + return getThis(); + } + + /** + * A {@code Consumer} of the OAuth 2.0 {@code scope} values supported allowing the ability to add, replace, or remove. + * + * @param scopesConsumer a {@code Consumer} of the OAuth 2.0 {@code scope} values supported + * @return the {@link AbstractBuilder} for further configuration + */ + public B scopes(Consumer> scopesConsumer) { + acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, scopesConsumer); + return getThis(); + } + + /** + * Add this OAuth 2.0 {@code response_type} to the collection of {@code response_types_supported} + * in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, REQUIRED. + * + * @param responseType the OAuth 2.0 {@code response_type} value supported + * @return the {@link AbstractBuilder} for further configuration + */ + public B responseType(String responseType) { + addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, responseType); + return getThis(); + } + + /** + * A {@code Consumer} of the OAuth 2.0 {@code response_type} values supported allowing the ability to add, replace, or remove. + * + * @param responseTypesConsumer a {@code Consumer} of the OAuth 2.0 {@code response_type} values supported + * @return the {@link AbstractBuilder} for further configuration + */ + public B responseTypes(Consumer> responseTypesConsumer) { + acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, responseTypesConsumer); + return getThis(); + } + + /** + * Add this OAuth 2.0 {@code grant_type} to the collection of {@code grant_types_supported} + * in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL. + * + * @param grantType the OAuth 2.0 {@code grant_type} value supported + * @return the {@link AbstractBuilder} for further configuration + */ + public B grantType(String grantType) { + addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, grantType); + return getThis(); + } + + /** + * A {@code Consumer} of the OAuth 2.0 {@code grant_type} values supported allowing the ability to add, replace, or remove. + * + * @param grantTypesConsumer a {@code Consumer} of the OAuth 2.0 {@code grant_type} values supported + * @return the {@link AbstractBuilder} for further configuration + */ + public B grantTypes(Consumer> grantTypesConsumer) { + acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, grantTypesConsumer); + return getThis(); + } + + /** + * Use this {@code revocation_endpoint} in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL. + * + * @param tokenRevocationEndpoint the {@code URL} of the OAuth 2.0 Token Revocation Endpoint + * @return the {@link AbstractBuilder} for further configuration + */ + public B tokenRevocationEndpoint(String tokenRevocationEndpoint) { + return claim(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT, tokenRevocationEndpoint); + } + + /** + * Add this client authentication method to the collection of {@code revocation_endpoint_auth_methods_supported} + * in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL. + * + * @param authenticationMethod the client authentication method supported by the OAuth 2.0 Token Revocation Endpoint + * @return the {@link AbstractBuilder} for further configuration + */ + public B tokenRevocationEndpointAuthenticationMethod(String authenticationMethod) { + addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED, authenticationMethod); + return getThis(); + } + + /** + * A {@code Consumer} of the client authentication method(s) allowing the ability to add, replace, or remove. + * + * @param authenticationMethodsConsumer a {@code Consumer} of the client authentication method(s) supported by the OAuth 2.0 Token Revocation Endpoint + * @return the {@link AbstractBuilder} for further configuration + */ + public B tokenRevocationEndpointAuthenticationMethods(Consumer> authenticationMethodsConsumer) { + acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED, authenticationMethodsConsumer); + return getThis(); + } + + /** + * Use this {@code introspection_endpoint} in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL. + * + * @param tokenIntrospectionEndpoint the {@code URL} of the OAuth 2.0 Token Introspection Endpoint + * @return the {@link AbstractBuilder} for further configuration + */ + public B tokenIntrospectionEndpoint(String tokenIntrospectionEndpoint) { + return claim(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT, tokenIntrospectionEndpoint); + } + + /** + * Add this client authentication method to the collection of {@code introspection_endpoint_auth_methods_supported} + * in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL. + * + * @param authenticationMethod the client authentication method supported by the OAuth 2.0 Token Introspection Endpoint + * @return the {@link AbstractBuilder} for further configuration + */ + public B tokenIntrospectionEndpointAuthenticationMethod(String authenticationMethod) { + addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED, authenticationMethod); + return getThis(); + } + + /** + * A {@code Consumer} of the client authentication method(s) allowing the ability to add, replace, or remove. + * + * @param authenticationMethodsConsumer a {@code Consumer} of the client authentication method(s) supported by the OAuth 2.0 Token Introspection Endpoint + * @return the {@link AbstractBuilder} for further configuration + */ + public B tokenIntrospectionEndpointAuthenticationMethods(Consumer> authenticationMethodsConsumer) { + acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED, authenticationMethodsConsumer); + return getThis(); + } + + /** + * 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. + * + * @param codeChallengeMethod the {@code code_challenge_method} value supported + * @return the {@link AbstractBuilder} for further configuration + */ + public B codeChallengeMethod(String codeChallengeMethod) { + addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED, codeChallengeMethod); + return getThis(); + } + + /** + * A {@code Consumer} of the Proof Key for Code Exchange (PKCE) {@code code_challenge_method} values supported allowing the ability to add, replace, or remove. + * + * @param codeChallengeMethodsConsumer a {@code Consumer} of the {@code code_challenge_method} values supported + * @return the {@link AbstractBuilder} for further configuration + */ + public B codeChallengeMethods(Consumer> codeChallengeMethodsConsumer) { + acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED, codeChallengeMethodsConsumer); + return getThis(); + } + + /** + * Use this claim in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}. + * + * @param name the claim name + * @param value the claim value + * @return the {@link AbstractBuilder} for further configuration + */ + public B claim(String name, Object value) { + Assert.hasText(name, "name cannot be empty"); + Assert.notNull(value, "value cannot be null"); + this.claims.put(name, value); + return getThis(); + } + + /** + * Provides access to every {@link #claim(String, Object)} declared so far with + * the possibility to add, replace, or remove. + * + * @param claimsConsumer a {@code Consumer} of the claims + * @return the {@link AbstractBuilder} for further configurations + */ + public B claims(Consumer> claimsConsumer) { + claimsConsumer.accept(this.claims); + return getThis(); + } + + /** + * Creates the {@link AbstractOAuth2AuthorizationServerMetadata}. + * + * @return the {@link AbstractOAuth2AuthorizationServerMetadata} + */ + public abstract T build(); + + protected void validate() { + Assert.notNull(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.ISSUER), "issuer cannot be null"); + validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.ISSUER), "issuer must be a valid URL"); + Assert.notNull(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT), "authorizationEndpoint cannot be null"); + validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT), "authorizationEndpoint must be a valid URL"); + Assert.notNull(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT), "tokenEndpoint cannot be null"); + validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT), "tokenEndpoint must be a valid URL"); + if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED) != null) { + Assert.isInstanceOf(List.class, getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED), "tokenEndpointAuthenticationMethods must be of type List"); + Assert.notEmpty((List) getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED), "tokenEndpointAuthenticationMethods cannot be empty"); + } + if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI) != null) { + validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI), "jwksUri must be a valid URL"); + } + if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED) != null) { + Assert.isInstanceOf(List.class, getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED), "scopes must be of type List"); + Assert.notEmpty((List) getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED), "scopes cannot be empty"); + } + Assert.notNull(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED), "responseTypes cannot be null"); + Assert.isInstanceOf(List.class, getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED), "responseTypes must be of type List"); + Assert.notEmpty((List) getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED), "responseTypes cannot be empty"); + if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED) != null) { + Assert.isInstanceOf(List.class, getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED), "grantTypes must be of type List"); + Assert.notEmpty((List) getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED), "grantTypes cannot be empty"); + } + if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT) != null) { + validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT), "tokenRevocationEndpoint must be a valid URL"); + } + if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED) != null) { + Assert.isInstanceOf(List.class, getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED), "tokenRevocationEndpointAuthenticationMethods must be of type List"); + Assert.notEmpty((List) getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED), "tokenRevocationEndpointAuthenticationMethods cannot be empty"); + } + if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT) != null) { + validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT), "tokenIntrospectionEndpoint must be a valid URL"); + } + if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED) != null) { + 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.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"); + } + } + + @SuppressWarnings("unchecked") + private void addClaimToClaimList(String name, String value) { + Assert.hasText(name, "name cannot be empty"); + Assert.notNull(value, "value cannot be null"); + getClaims().computeIfAbsent(name, k -> new LinkedList()); + ((List) getClaims().get(name)).add(value); + } + + @SuppressWarnings("unchecked") + private void acceptClaimValues(String name, Consumer> valuesConsumer) { + Assert.hasText(name, "name cannot be empty"); + Assert.notNull(valuesConsumer, "valuesConsumer cannot be null"); + getClaims().computeIfAbsent(name, k -> new LinkedList()); + List values = (List) getClaims().get(name); + valuesConsumer.accept(values); + } + + private static void validateURL(Object url, String errorMessage) { + if (URL.class.isAssignableFrom(url.getClass())) { + return; + } + + try { + new URI(url.toString()).toURL(); + } catch (Exception ex) { + throw new IllegalArgumentException(errorMessage, ex); + } + } + + } +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationServerConfiguration.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2AuthorizationServerMetadata.java similarity index 53% rename from oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationServerConfiguration.java rename to oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2AuthorizationServerMetadata.java index be5e4474..55685da0 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationServerConfiguration.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2AuthorizationServerMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,34 +13,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.security.oauth2.core.endpoint; +package org.springframework.security.oauth2.core; -import org.springframework.security.oauth2.core.AbstractOAuth2AuthorizationServerConfiguration; -import org.springframework.security.oauth2.core.OAuth2AuthorizationServerMetadataClaimAccessor; -import org.springframework.security.oauth2.core.Version; -import org.springframework.util.Assert; - -import java.io.Serializable; import java.util.Map; +import org.springframework.util.Assert; + /** - * A representation of an OAuth 2.0 Authorization Server Configuration response, - * which is returned form an OAuth 2.0 Authorization Server's Configuration Endpoint, + * A representation of an OAuth 2.0 Authorization Server Metadata response, + * which is returned from an OAuth 2.0 Authorization Server's Metadata Endpoint, * and contains a set of claims about the Authorization Server's configuration. * The claims are defined by the OAuth 2.0 Authorization Server Metadata * specification (RFC 8414). * * @author Daniel Garnier-Moiroux * @since 0.1.1 - * @see AbstractOAuth2AuthorizationServerConfiguration - * @see OAuth2AuthorizationServerMetadataClaimAccessor + * @see AbstractOAuth2AuthorizationServerMetadata * @see 3.2. Authorization Server Metadata Response */ -public final class OAuth2AuthorizationServerConfiguration extends AbstractOAuth2AuthorizationServerConfiguration - implements OAuth2AuthorizationServerMetadataClaimAccessor, Serializable { - private static final long serialVersionUID = Version.SERIAL_VERSION_UID; +public final class OAuth2AuthorizationServerMetadata extends AbstractOAuth2AuthorizationServerMetadata { - private OAuth2AuthorizationServerConfiguration(Map claims) { + private OAuth2AuthorizationServerMetadata(Map claims) { super(claims); } @@ -66,27 +59,28 @@ public final class OAuth2AuthorizationServerConfiguration extends AbstractOAuth2 } /** - * Helps configure an {@link OAuth2AuthorizationServerConfiguration}. + * Helps configure an {@link OAuth2AuthorizationServerMetadata}. */ - public static class Builder - extends AbstractOAuth2AuthorizationServerConfiguration.AbstractBuilder { + public static class Builder extends AbstractBuilder { + private Builder() { } /** - * Validate the claims and build the {@link OAuth2AuthorizationServerConfiguration}. + * Validate the claims and build the {@link OAuth2AuthorizationServerMetadata}. *

* The following claims are REQUIRED: - * {@code issuer}, {@code authorization_endpoint}, {@code token_endpoint}, - * {@code jwks_uri} and {@code response_types_supported}. + * {@code issuer}, {@code authorization_endpoint}, {@code token_endpoint} + * and {@code response_types_supported}. * - * @return the {@link OAuth2AuthorizationServerConfiguration} + * @return the {@link OAuth2AuthorizationServerMetadata} */ - public OAuth2AuthorizationServerConfiguration build() { - validateCommonClaims(); - removeEmptyClaims(); - return new OAuth2AuthorizationServerConfiguration(this.claims); + @Override + public OAuth2AuthorizationServerMetadata build() { + validate(); + return new OAuth2AuthorizationServerMetadata(getClaims()); } } + } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2AuthorizationServerMetadataClaimAccessor.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2AuthorizationServerMetadataClaimAccessor.java index f83df1c8..d79130aa 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2AuthorizationServerMetadataClaimAccessor.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2AuthorizationServerMetadataClaimAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,18 +15,17 @@ */ package org.springframework.security.oauth2.core; - import java.net.URL; import java.util.List; /** - * A base {@link ClaimAccessor} for the "claims" the Authorization Server can make about - * its configuration, used either in OpenID Connect Discovery 1.0 or OAuth 2.0 Authorization - * Server Metadata. + * A {@link ClaimAccessor} for the "claims" an Authorization Server describes about its configuration, + * used in OAuth 2.0 Authorization Server Metadata and OpenID Connect Discovery 1.0. * * @author Daniel Garnier-Moiroux * @since 0.1.1 * @see ClaimAccessor + * @see OAuth2AuthorizationServerMetadataClaimNames * @see 2. Authorization Server Metadata * @see 3. OpenID Provider Metadata */ @@ -73,10 +72,19 @@ public interface OAuth2AuthorizationServerMetadataClaimAccessor extends ClaimAcc * * @return the {@code URL} of the JSON Web Key Set */ - default URL getJwkSetUri() { + default URL getJwkSetUrl() { return getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI); } + /** + * Returns the OAuth 2.0 {@code scope} values supported {@code (scopes_supported)}. + * + * @return the OAuth 2.0 {@code scope} values supported + */ + default List getScopes() { + return getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED); + } + /** * Returns the OAuth 2.0 {@code response_type} values supported {@code (response_types_supported)}. * @@ -95,22 +103,13 @@ public interface OAuth2AuthorizationServerMetadataClaimAccessor extends ClaimAcc return getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED); } - /** - * Returns the OAuth 2.0 {@code scope} values supported {@code (scopes_supported)}. - * - * @return the OAuth 2.0 {@code scope} values supported - */ - default List getScopes() { - return this.getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED); - } - /** * Returns the {@code URL} of the OAuth 2.0 Token Revocation Endpoint {@code (revocation_endpoint)}. * * @return the {@code URL} of the OAuth 2.0 Token Revocation Endpoint */ default URL getTokenRevocationEndpoint() { - return this.getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT); + return getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT); } /** @@ -119,16 +118,34 @@ public interface OAuth2AuthorizationServerMetadataClaimAccessor extends ClaimAcc * @return the client authentication methods supported by the OAuth 2.0 Token Revocation Endpoint */ default List getTokenRevocationEndpointAuthenticationMethods() { - return this.getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED); + return getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED); } /** - * Returns the Proof Key for Code Exchange (PKCE) code challenge methods supported by the - * OAuth 2.0 Authorization Server {@code (code_challenge_methods_supported)}. + * Returns the {@code URL} of the OAuth 2.0 Token Introspection Endpoint {@code (introspection_endpoint)}. * - * @return the code challenge methods supported by the OAuth 2.0 Authorization Server + * @return the {@code URL} of the OAuth 2.0 Token Introspection Endpoint + */ + default URL getTokenIntrospectionEndpoint() { + return getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT); + } + + /** + * Returns the client authentication methods supported by the OAuth 2.0 Token Introspection Endpoint {@code (introspection_endpoint_auth_methods_supported)}. + * + * @return the client authentication methods supported by the OAuth 2.0 Token Introspection Endpoint + */ + default List getTokenIntrospectionEndpointAuthenticationMethods() { + return getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED); + } + + /** + * Returns the Proof Key for Code Exchange (PKCE) {@code code_challenge_method} values supported {@code (code_challenge_methods_supported)}. + * + * @return the {@code code_challenge_method} values supported */ default List getCodeChallengeMethods() { - return this.getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED); + return getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED); } + } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2AuthorizationServerMetadataClaimNames.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2AuthorizationServerMetadataClaimNames.java index a9c126cb..e6bd62be 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2AuthorizationServerMetadataClaimNames.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2AuthorizationServerMetadataClaimNames.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,15 +15,12 @@ */ package org.springframework.security.oauth2.core; -import org.springframework.security.oauth2.core.oidc.OidcProviderMetadataClaimNames; - /** - * The names of the "claims" an Authorization Server can make about its configuration, - * used either in OpenID Connect Discovery 1.0 or OAuth 2.0 Authorization Server Metadata. + * The names of the "claims" an Authorization Server describes about its configuration, + * used in OAuth 2.0 Authorization Server Metadata and OpenID Connect Discovery 1.0. * * @author Daniel Garnier-Moiroux * @since 0.1.1 - * @see OidcProviderMetadataClaimNames * @see 2. Authorization Server Metadata * @see 3. OpenID Provider Metadata */ @@ -54,6 +51,11 @@ public interface OAuth2AuthorizationServerMetadataClaimNames { */ String JWKS_URI = "jwks_uri"; + /** + * {@code scopes_supported} - the OAuth 2.0 {@code scope} values supported + */ + String SCOPES_SUPPORTED = "scopes_supported"; + /** * {@code response_types_supported} - the OAuth 2.0 {@code response_type} values supported */ @@ -64,24 +66,29 @@ public interface OAuth2AuthorizationServerMetadataClaimNames { */ String GRANT_TYPES_SUPPORTED = "grant_types_supported"; - /** - * {@code scopes_supported} - the OAuth 2.0 {@code scope} values supported - */ - String SCOPES_SUPPORTED = "scopes_supported"; - /** * {@code revocation_endpoint} - the {@code URL} of the OAuth 2.0 Token Revocation Endpoint */ String REVOCATION_ENDPOINT = "revocation_endpoint"; /** - * {@code token_endpoint_auth_methods_supported} - the client authentication methods supported by the OAuth 2.0 Token Revocation Endpoint + * {@code revocation_endpoint_auth_methods_supported} - the client authentication methods supported by the OAuth 2.0 Token Revocation Endpoint */ String REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED = "revocation_endpoint_auth_methods_supported"; /** - * {@code code_challenge_methods_supported} - the Proof Key for Code Exchange (PKCE) code challenge methods - * supported by the OAuth 2.0 Authorization Server + * {@code introspection_endpoint} - the {@code URL} of the OAuth 2.0 Token Introspection Endpoint + */ + String INTROSPECTION_ENDPOINT = "introspection_endpoint"; + + /** + * {@code introspection_endpoint_auth_methods_supported} - the client authentication methods supported by the OAuth 2.0 Token Introspection Endpoint + */ + String INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED = "introspection_endpoint_auth_methods_supported"; + + /** + * {@code code_challenge_methods_supported} - the Proof Key for Code Exchange (PKCE) {@code code_challenge_method} values supported */ String CODE_CHALLENGE_METHODS_SUPPORTED = "code_challenge_methods_supported"; + } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/endpoint/PkceCodeChallengeMethod2.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/endpoint/PkceCodeChallengeMethod2.java deleted file mode 100644 index 558a9c3a..00000000 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/endpoint/PkceCodeChallengeMethod2.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.oauth2.core.endpoint; - -import org.springframework.security.oauth2.core.Version; -import org.springframework.util.Assert; - -import java.io.Serializable; - - -/** - * TODO - * This class is temporary and will be removed after upgrading to Spring Security 5.5.0 GA. - * - * The {@code code_challenge_method} is consumed by the authorization endpoint. The client sets - * the {@code code_challenge_method} parameter and a {@code code_challenge} derived from the - * {@code code_verifier} using the {@code code_challenge_method}. - * - *

- * The {@code code_challenge_method} parameter value may be one of "S256" or - * "plain". If the client is capable of using "S256", it MUST use - * "S256", as it is mandatory to implement on the server. - * - * @author Daniel Garnier-Moiroux - * @since 0.1.1 - * @see 6.2. PKCE Code Challenge Method Registry - */ -public final class PkceCodeChallengeMethod2 implements Serializable { - - private static final long serialVersionUID = Version.SERIAL_VERSION_UID; - - public static final PkceCodeChallengeMethod2 S256 = new PkceCodeChallengeMethod2("S256"); - - public static final PkceCodeChallengeMethod2 PLAIN = new PkceCodeChallengeMethod2("plain"); - - private final String value; - - private PkceCodeChallengeMethod2(String value) { - Assert.hasText(value, "value cannot be empty"); - this.value = value; - } - - /** - * Returns the value of the authorization response type. - * - * @return the value of the authorization response type - */ - public String getValue() { - return this.value; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || this.getClass() != obj.getClass()) { - return false; - } - PkceCodeChallengeMethod2 that = (PkceCodeChallengeMethod2) obj; - return this.getValue().equals(that.getValue()); - } - - @Override - public int hashCode() { - return this.getValue().hashCode(); - } -} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2AuthorizationServerConfigurationHttpMessageConverter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2AuthorizationServerConfigurationHttpMessageConverter.java deleted file mode 100644 index 608da96b..00000000 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2AuthorizationServerConfigurationHttpMessageConverter.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.security.oauth2.core.http.converter; - -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.core.convert.TypeDescriptor; -import org.springframework.core.convert.converter.Converter; -import org.springframework.http.HttpInputMessage; -import org.springframework.http.HttpOutputMessage; -import org.springframework.http.MediaType; -import org.springframework.http.converter.AbstractHttpMessageConverter; -import org.springframework.http.converter.GenericHttpMessageConverter; -import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.http.converter.HttpMessageNotWritableException; -import org.springframework.security.oauth2.core.OAuth2AuthorizationServerMetadataClaimNames; -import org.springframework.security.oauth2.core.converter.ClaimConversionService; -import org.springframework.security.oauth2.core.converter.ClaimTypeConverter; -import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationServerConfiguration; -import org.springframework.util.Assert; - -import java.net.URL; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; - - -/** - * A {@link HttpMessageConverter} for an {@link OAuth2AuthorizationServerConfiguration OAuth 2.0 Authorization Server Configuration Response}. - * - * @author Daniel Garnier-Moiroux - * @since 0.1.1 - * @see AbstractHttpMessageConverter - * @see OAuth2AuthorizationServerConfiguration - */ -public class OAuth2AuthorizationServerConfigurationHttpMessageConverter - extends AbstractHttpMessageConverter { - - private static final ParameterizedTypeReference> STRING_OBJECT_MAP = - new ParameterizedTypeReference>() {}; - - private final GenericHttpMessageConverter jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter(); - - private Converter, OAuth2AuthorizationServerConfiguration> authorizationServerConfigurationConverter = new OAuth2AuthorizationServerConfigurationConverter(); - private Converter> authorizationServerConfigurationParametersConverter = OAuth2AuthorizationServerConfiguration::getClaims; - - public OAuth2AuthorizationServerConfigurationHttpMessageConverter() { - super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json")); - } - - @Override - protected boolean supports(Class clazz) { - return OAuth2AuthorizationServerConfiguration.class.isAssignableFrom(clazz); - } - - @Override - @SuppressWarnings("unchecked") - protected OAuth2AuthorizationServerConfiguration readInternal(Class clazz, HttpInputMessage inputMessage) - throws HttpMessageNotReadableException { - try { - Map serverConfigurationParameters = - (Map) this.jsonMessageConverter.read(STRING_OBJECT_MAP.getType(), null, inputMessage); - return this.authorizationServerConfigurationConverter.convert(serverConfigurationParameters); - } catch (Exception ex) { - throw new HttpMessageNotReadableException( - "An error occurred reading the OAuth 2.0 Authorization Server Configuration: " + ex.getMessage(), ex, inputMessage); - } - } - - @Override - protected void writeInternal(OAuth2AuthorizationServerConfiguration providerConfiguration, HttpOutputMessage outputMessage) - throws HttpMessageNotWritableException { - try { - Map providerConfigurationResponseParameters = - this.authorizationServerConfigurationParametersConverter.convert(providerConfiguration); - this.jsonMessageConverter.write( - providerConfigurationResponseParameters, - STRING_OBJECT_MAP.getType(), - MediaType.APPLICATION_JSON, - outputMessage - ); - } catch (Exception ex) { - throw new HttpMessageNotWritableException( - "An error occurred writing the OAuth 2.0 Authorization Server Configuration: " + ex.getMessage(), ex); - } - } - - /** - * Sets the {@link Converter} used for converting the OAuth 2.0 Authorization Server Configuration - * parameters to an {@link OAuth2AuthorizationServerConfiguration}. - * - * @param authorizationServerConfigurationConverter the {@link Converter} used for converting to - * an {@link OAuth2AuthorizationServerConfiguration}. - */ - public void setAuthorizationServerConfigurationConverter(Converter, OAuth2AuthorizationServerConfiguration> authorizationServerConfigurationConverter) { - Assert.notNull(authorizationServerConfigurationConverter, "authorizationServerConfigurationConverter cannot be null"); - this.authorizationServerConfigurationConverter = authorizationServerConfigurationConverter; - } - - /** - * Sets the {@link Converter} used for converting the {@link OAuth2AuthorizationServerConfiguration} to a - * {@code Map} representation of the OAuth 2.0 Authorization Server Configuration. - * - * @param authorizationServerConfigurationParametersConverter the {@link Converter} used for converting to a - * {@code Map} representation of the OAuth 2.0 Authorization Server Configuration. - */ - public void setAuthorizationServerConfigurationParametersConverter(Converter> authorizationServerConfigurationParametersConverter) { - Assert.notNull(authorizationServerConfigurationParametersConverter, "authorizationServerConfigurationParametersConverter cannot be null"); - this.authorizationServerConfigurationParametersConverter = authorizationServerConfigurationParametersConverter; - } - - private static final class OAuth2AuthorizationServerConfigurationConverter implements Converter, OAuth2AuthorizationServerConfiguration> { - private static final ClaimConversionService CLAIM_CONVERSION_SERVICE = ClaimConversionService.getSharedInstance(); - private static final TypeDescriptor OBJECT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Object.class); - private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class); - private static final TypeDescriptor URL_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(URL.class); - private final ClaimTypeConverter claimTypeConverter; - - private OAuth2AuthorizationServerConfigurationConverter() { - Map> claimNameToConverter = new HashMap<>(); - Converter collectionStringConverter = getConverter( - TypeDescriptor.collection(Collection.class, STRING_TYPE_DESCRIPTOR)); - Converter urlConverter = getConverter(URL_TYPE_DESCRIPTOR); - - claimNameToConverter.put(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, urlConverter); - claimNameToConverter.put(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, urlConverter); - claimNameToConverter.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, urlConverter); - claimNameToConverter.put(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT, urlConverter); - claimNameToConverter.put(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI, urlConverter); - claimNameToConverter.put(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, collectionStringConverter); - claimNameToConverter.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, collectionStringConverter); - claimNameToConverter.put(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED, collectionStringConverter); - claimNameToConverter.put(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, collectionStringConverter); - claimNameToConverter.put(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, collectionStringConverter); - claimNameToConverter.put(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED, collectionStringConverter); - this.claimTypeConverter = new ClaimTypeConverter(claimNameToConverter); - } - - @Override - public OAuth2AuthorizationServerConfiguration convert(Map source) { - Map parsedClaims = this.claimTypeConverter.convert(source); - return OAuth2AuthorizationServerConfiguration.withClaims(parsedClaims).build(); - } - - private static Converter getConverter(TypeDescriptor targetDescriptor) { - return (source) -> CLAIM_CONVERSION_SERVICE.convert(source, OBJECT_TYPE_DESCRIPTOR, targetDescriptor); - } - } -} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2AuthorizationServerMetadataHttpMessageConverter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2AuthorizationServerMetadataHttpMessageConverter.java new file mode 100644 index 00000000..60a5e87c --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2AuthorizationServerMetadataHttpMessageConverter.java @@ -0,0 +1,164 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.core.http.converter; + +import java.net.URL; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.Converter; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.AbstractHttpMessageConverter; +import org.springframework.http.converter.GenericHttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.security.oauth2.core.OAuth2AuthorizationServerMetadata; +import org.springframework.security.oauth2.core.OAuth2AuthorizationServerMetadataClaimNames; +import org.springframework.security.oauth2.core.converter.ClaimConversionService; +import org.springframework.security.oauth2.core.converter.ClaimTypeConverter; +import org.springframework.util.Assert; + +/** + * A {@link HttpMessageConverter} for an {@link OAuth2AuthorizationServerMetadata OAuth 2.0 Authorization Server Metadata Response}. + * + * @author Daniel Garnier-Moiroux + * @since 0.1.1 + * @see AbstractHttpMessageConverter + * @see OAuth2AuthorizationServerMetadata + */ +public class OAuth2AuthorizationServerMetadataHttpMessageConverter + extends AbstractHttpMessageConverter { + + private static final ParameterizedTypeReference> STRING_OBJECT_MAP = + new ParameterizedTypeReference>() {}; + + private final GenericHttpMessageConverter jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter(); + + private Converter, OAuth2AuthorizationServerMetadata> authorizationServerMetadataConverter = new OAuth2AuthorizationServerMetadataConverter(); + private Converter> authorizationServerMetadataParametersConverter = OAuth2AuthorizationServerMetadata::getClaims; + + public OAuth2AuthorizationServerMetadataHttpMessageConverter() { + super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json")); + } + + @Override + protected boolean supports(Class clazz) { + return OAuth2AuthorizationServerMetadata.class.isAssignableFrom(clazz); + } + + @Override + @SuppressWarnings("unchecked") + protected OAuth2AuthorizationServerMetadata readInternal(Class clazz, HttpInputMessage inputMessage) + throws HttpMessageNotReadableException { + try { + Map authorizationServerMetadataParameters = + (Map) this.jsonMessageConverter.read(STRING_OBJECT_MAP.getType(), null, inputMessage); + return this.authorizationServerMetadataConverter.convert(authorizationServerMetadataParameters); + } catch (Exception ex) { + throw new HttpMessageNotReadableException( + "An error occurred reading the OAuth 2.0 Authorization Server Metadata: " + ex.getMessage(), ex, inputMessage); + } + } + + @Override + protected void writeInternal(OAuth2AuthorizationServerMetadata authorizationServerMetadata, HttpOutputMessage outputMessage) + throws HttpMessageNotWritableException { + try { + Map authorizationServerMetadataResponseParameters = + this.authorizationServerMetadataParametersConverter.convert(authorizationServerMetadata); + this.jsonMessageConverter.write( + authorizationServerMetadataResponseParameters, + STRING_OBJECT_MAP.getType(), + MediaType.APPLICATION_JSON, + outputMessage + ); + } catch (Exception ex) { + throw new HttpMessageNotWritableException( + "An error occurred writing the OAuth 2.0 Authorization Server Metadata: " + ex.getMessage(), ex); + } + } + + /** + * Sets the {@link Converter} used for converting the OAuth 2.0 Authorization Server Metadata + * parameters to an {@link OAuth2AuthorizationServerMetadata}. + * + * @param authorizationServerMetadataConverter the {@link Converter} used for converting to + * an {@link OAuth2AuthorizationServerMetadata}. + */ + public final void setAuthorizationServerMetadataConverter(Converter, OAuth2AuthorizationServerMetadata> authorizationServerMetadataConverter) { + Assert.notNull(authorizationServerMetadataConverter, "authorizationServerMetadataConverter cannot be null"); + this.authorizationServerMetadataConverter = authorizationServerMetadataConverter; + } + + /** + * Sets the {@link Converter} used for converting the {@link OAuth2AuthorizationServerMetadata} to a + * {@code Map} representation of the OAuth 2.0 Authorization Server Metadata. + * + * @param authorizationServerMetadataParametersConverter the {@link Converter} used for converting to a + * {@code Map} representation of the OAuth 2.0 Authorization Server Metadata. + */ + public final void setAuthorizationServerMetadataParametersConverter(Converter> authorizationServerMetadataParametersConverter) { + Assert.notNull(authorizationServerMetadataParametersConverter, "authorizationServerMetadataParametersConverter cannot be null"); + this.authorizationServerMetadataParametersConverter = authorizationServerMetadataParametersConverter; + } + + private static final class OAuth2AuthorizationServerMetadataConverter implements Converter, OAuth2AuthorizationServerMetadata> { + private static final ClaimConversionService CLAIM_CONVERSION_SERVICE = ClaimConversionService.getSharedInstance(); + private static final TypeDescriptor OBJECT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Object.class); + private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class); + private static final TypeDescriptor URL_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(URL.class); + private final ClaimTypeConverter claimTypeConverter; + + private OAuth2AuthorizationServerMetadataConverter() { + Converter collectionStringConverter = getConverter( + TypeDescriptor.collection(Collection.class, STRING_TYPE_DESCRIPTOR)); + Converter urlConverter = getConverter(URL_TYPE_DESCRIPTOR); + + Map> claimConverters = new HashMap<>(); + claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, urlConverter); + claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, urlConverter); + claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, urlConverter); + claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, collectionStringConverter); + claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI, urlConverter); + claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, collectionStringConverter); + claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, collectionStringConverter); + claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, collectionStringConverter); + claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT, urlConverter); + claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED, collectionStringConverter); + claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT, urlConverter); + claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED, collectionStringConverter); + claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED, collectionStringConverter); + this.claimTypeConverter = new ClaimTypeConverter(claimConverters); + } + + @Override + public OAuth2AuthorizationServerMetadata convert(Map source) { + Map parsedClaims = this.claimTypeConverter.convert(source); + return OAuth2AuthorizationServerMetadata.withClaims(parsedClaims).build(); + } + + private static Converter getConverter(TypeDescriptor targetDescriptor) { + return (source) -> CLAIM_CONVERSION_SERVICE.convert(source, OBJECT_TYPE_DESCRIPTOR, targetDescriptor); + } + } + +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderConfiguration.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderConfiguration.java index d1d10572..20c690e8 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderConfiguration.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,16 +15,15 @@ */ package org.springframework.security.oauth2.core.oidc; -import org.springframework.security.oauth2.core.AbstractOAuth2AuthorizationServerConfiguration; -import org.springframework.security.oauth2.core.Version; -import org.springframework.security.oauth2.jose.jws.JwsAlgorithm; -import org.springframework.util.Assert; - -import java.io.Serializable; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.function.Consumer; +import org.springframework.security.oauth2.core.AbstractOAuth2AuthorizationServerMetadata; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithm; +import org.springframework.util.Assert; + /** * A representation of an OpenID Provider Configuration Response, * which is returned from an Issuer's Discovery Endpoint, @@ -33,13 +32,12 @@ import java.util.function.Consumer; * * @author Daniel Garnier-Moiroux * @since 0.1.0 + * @see AbstractOAuth2AuthorizationServerMetadata * @see OidcProviderMetadataClaimAccessor - * @see AbstractOAuth2AuthorizationServerConfiguration * @see 4.2. OpenID Provider Configuration Response */ -public final class OidcProviderConfiguration extends AbstractOAuth2AuthorizationServerConfiguration - implements OidcProviderMetadataClaimAccessor, Serializable { - private static final long serialVersionUID = Version.SERIAL_VERSION_UID; +public final class OidcProviderConfiguration extends AbstractOAuth2AuthorizationServerMetadata + implements OidcProviderMetadataClaimAccessor { private OidcProviderConfiguration(Map claims) { super(claims); @@ -66,9 +64,10 @@ public final class OidcProviderConfiguration extends AbstractOAuth2Authorization } /** - * Helps configure an {@link OidcProviderConfiguration} + * Helps configure an {@link OidcProviderConfiguration}. */ - public static class Builder extends AbstractOAuth2AuthorizationServerConfiguration.AbstractBuilder { + public static class Builder extends AbstractBuilder { + private Builder() { } @@ -119,32 +118,6 @@ public final class OidcProviderConfiguration extends AbstractOAuth2Authorization return this; } - /** - * Use this claim in the resulting {@link OidcProviderConfiguration}. - * - * @param name the claim name - * @param value the claim value - * @return the {@link Builder} for further configuration - */ - public Builder claim(String name, Object value) { - Assert.hasText(name, "name cannot be empty"); - Assert.notNull(value, "value cannot be null"); - this.claims.put(name, value); - return this; - } - - /** - * Provides access to every {@link #claim(String, Object)} declared so far with - * the possibility to add, replace, or remove. - * - * @param claimsConsumer a {@code Consumer} of the claims - * @return the {@link Builder} for further configurations - */ - public Builder claims(Consumer> claimsConsumer) { - claimsConsumer.accept(this.claims); - return this; - } - /** * Validate the claims and build the {@link OidcProviderConfiguration}. *

@@ -157,19 +130,38 @@ public final class OidcProviderConfiguration extends AbstractOAuth2Authorization */ @Override public OidcProviderConfiguration build() { - validateCommonClaims(); - validateOidcSpecificClaims(); - removeEmptyClaims(); - return new OidcProviderConfiguration(this.claims); + validate(); + return new OidcProviderConfiguration(getClaims()); } - private void validateOidcSpecificClaims() { - Assert.notNull(this.claims.get(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED), "subjectTypes cannot be null"); - Assert.isInstanceOf(List.class, this.claims.get(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED), "subjectTypes must be of type List"); - Assert.notEmpty((List) this.claims.get(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED), "subjectTypes cannot be empty"); - Assert.notNull(this.claims.get(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED), "idTokenSigningAlgorithms cannot be null"); - Assert.isInstanceOf(List.class, this.claims.get(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED), "idTokenSigningAlgorithms must be of type List"); - Assert.notEmpty((List) this.claims.get(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED), "idTokenSigningAlgorithms cannot be empty"); + @Override + protected void validate() { + super.validate(); + Assert.notNull(getClaims().get(OidcProviderMetadataClaimNames.JWKS_URI), "jwksUri cannot be null"); + Assert.notNull(getClaims().get(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED), "subjectTypes cannot be null"); + Assert.isInstanceOf(List.class, getClaims().get(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED), "subjectTypes must be of type List"); + Assert.notEmpty((List) getClaims().get(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED), "subjectTypes cannot be empty"); + Assert.notNull(getClaims().get(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED), "idTokenSigningAlgorithms cannot be null"); + Assert.isInstanceOf(List.class, getClaims().get(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED), "idTokenSigningAlgorithms must be of type List"); + Assert.notEmpty((List) getClaims().get(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED), "idTokenSigningAlgorithms cannot be empty"); } + + @SuppressWarnings("unchecked") + private void addClaimToClaimList(String name, String value) { + Assert.hasText(name, "name cannot be empty"); + Assert.notNull(value, "value cannot be null"); + getClaims().computeIfAbsent(name, k -> new LinkedList()); + ((List) getClaims().get(name)).add(value); + } + + @SuppressWarnings("unchecked") + private void acceptClaimValues(String name, Consumer> valuesConsumer) { + Assert.hasText(name, "name cannot be empty"); + Assert.notNull(valuesConsumer, "valuesConsumer cannot be null"); + getClaims().computeIfAbsent(name, k -> new LinkedList()); + List values = (List) getClaims().get(name); + valuesConsumer.accept(values); + } + } } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderMetadataClaimAccessor.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderMetadataClaimAccessor.java index 4b9a3e0a..c30a94d0 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderMetadataClaimAccessor.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderMetadataClaimAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,13 @@ package org.springframework.security.oauth2.core.oidc; -import org.springframework.security.oauth2.core.OAuth2AuthorizationServerMetadataClaimAccessor; +import java.util.List; + import org.springframework.security.oauth2.core.ClaimAccessor; +import org.springframework.security.oauth2.core.OAuth2AuthorizationServerMetadataClaimAccessor; import org.springframework.security.oauth2.jose.jws.JwsAlgorithm; import org.springframework.security.oauth2.jwt.Jwt; - -import java.util.List; - /** * A {@link ClaimAccessor} for the "claims" that can be returned * in the OpenID Provider Configuration Response. diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderMetadataClaimNames.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderMetadataClaimNames.java index dbd54327..ef3722be 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderMetadataClaimNames.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderMetadataClaimNames.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import org.springframework.security.oauth2.jose.jws.JwsAlgorithm; * * @author Daniel Garnier-Moiroux * @since 0.1.0 + * @see OAuth2AuthorizationServerMetadataClaimNames * @see 3. OpenID Provider Metadata */ public interface OidcProviderMetadataClaimNames extends OAuth2AuthorizationServerMetadataClaimNames { diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProvider.java index 20530c4f..058154de 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProvider.java @@ -31,7 +31,6 @@ import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.OAuth2TokenType; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; -import org.springframework.security.oauth2.core.endpoint.PkceCodeChallengeMethod2; import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; @@ -157,9 +156,9 @@ public class OAuth2ClientAuthenticationProvider implements AuthenticationProvide private static boolean codeVerifierValid(String codeVerifier, String codeChallenge, String codeChallengeMethod) { if (!StringUtils.hasText(codeVerifier)) { return false; - } else if (!StringUtils.hasText(codeChallengeMethod) || PkceCodeChallengeMethod2.PLAIN.getValue().equals(codeChallengeMethod)) { + } else if (!StringUtils.hasText(codeChallengeMethod) || "plain".equals(codeChallengeMethod)) { return codeVerifier.equals(codeChallenge); - } else if (PkceCodeChallengeMethod2.S256.getValue().equals(codeChallengeMethod)) { + } else if ("S256".equals(codeChallengeMethod)) { try { MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII)); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ProviderSettings.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ProviderSettings.java index 11cf0325..12c7d498 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ProviderSettings.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ProviderSettings.java @@ -70,8 +70,7 @@ public class ProviderSettings extends Settings { } /** - * Returns the Provider's OAuth 2.0 Authorization endpoint. - * The default is {@code /oauth2/authorize}. + * Returns the Provider's OAuth 2.0 Authorization endpoint. The default is {@code /oauth2/authorize}. * * @return the Authorization endpoint */ @@ -90,8 +89,7 @@ public class ProviderSettings extends Settings { } /** - * Returns the Provider's OAuth 2.0 Token endpoint. - * The default is {@code /oauth2/token}. + * Returns the Provider's OAuth 2.0 Token endpoint. The default is {@code /oauth2/token}. * * @return the Token endpoint */ diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java index 0160013b..0b99a609 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -75,21 +75,20 @@ public class OidcProviderConfigurationEndpointFilter extends OncePerRequestFilte return; } - OidcProviderConfiguration providerConfiguration = OidcProviderConfiguration - .builder() + OidcProviderConfiguration providerConfiguration = OidcProviderConfiguration.builder() .issuer(this.providerSettings.issuer()) .authorizationEndpoint(asUrl(this.providerSettings.issuer(), this.providerSettings.authorizationEndpoint())) .tokenEndpoint(asUrl(this.providerSettings.issuer(), this.providerSettings.tokenEndpoint())) - .tokenEndpointAuthenticationMethod("client_secret_basic") // TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_BASIC in Spring Security 5.5.0 - .tokenEndpointAuthenticationMethod("client_secret_post") // TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_POST in Spring Security 5.5.0 - .jwkSetUri(asUrl(this.providerSettings.issuer(), this.providerSettings.jwkSetEndpoint())) + .tokenEndpointAuthenticationMethod("client_secret_basic") // TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_BASIC in Spring Security 5.5.0 + .tokenEndpointAuthenticationMethod("client_secret_post") // TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_POST in Spring Security 5.5.0 + .jwkSetUrl(asUrl(this.providerSettings.issuer(), this.providerSettings.jwkSetEndpoint())) + .responseType(OAuth2AuthorizationResponseType.CODE.getValue()) .grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()) .grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) .grantType(AuthorizationGrantType.REFRESH_TOKEN.getValue()) - .responseType(OAuth2AuthorizationResponseType.CODE.getValue()) - .scope(OidcScopes.OPENID) .subjectType("public") .idTokenSigningAlgorithm(SignatureAlgorithm.RS256.getName()) + .scope(OidcScopes.OPENID) .build(); ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response); 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 02ba5bad..2be1d5de 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 @@ -48,7 +48,6 @@ import org.springframework.security.oauth2.core.OAuth2TokenType; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; -import org.springframework.security.oauth2.core.endpoint.PkceCodeChallengeMethod2; import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; import org.springframework.security.oauth2.core.oidc.OidcScopes; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; @@ -371,7 +370,7 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter { String codeChallengeMethod = authorizationRequestContext.getParameters().getFirst(PkceParameterNames.CODE_CHALLENGE_METHOD); if (StringUtils.hasText(codeChallengeMethod)) { if (authorizationRequestContext.getParameters().get(PkceParameterNames.CODE_CHALLENGE_METHOD).size() != 1 || - (!PkceCodeChallengeMethod2.S256.getValue().equals(codeChallengeMethod) && !PkceCodeChallengeMethod2.PLAIN.getValue().equals(codeChallengeMethod))) { + (!"S256".equals(codeChallengeMethod) && !"plain".equals(codeChallengeMethod))) { authorizationRequestContext.setError( createError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, PKCE_ERROR_URI)); return; diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerConfigurationEndpointFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java similarity index 61% rename from oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerConfigurationEndpointFilter.java rename to oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java index 50572ae0..02a6e0ad 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerConfigurationEndpointFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,15 +15,22 @@ */ package org.springframework.security.oauth2.server.authorization.web; +import java.io.IOException; +import java.util.List; +import java.util.function.Consumer; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.server.ServletServerHttpResponse; import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2AuthorizationServerMetadata; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType; -import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationServerConfiguration; -import org.springframework.security.oauth2.core.endpoint.PkceCodeChallengeMethod2; -import org.springframework.security.oauth2.core.http.converter.OAuth2AuthorizationServerConfigurationHttpMessageConverter; -import org.springframework.security.oauth2.core.oidc.OidcScopes; +import org.springframework.security.oauth2.core.http.converter.OAuth2AuthorizationServerMetadataHttpMessageConverter; import org.springframework.security.oauth2.server.authorization.config.ProviderSettings; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; @@ -31,36 +38,31 @@ import org.springframework.util.Assert; import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.util.UriComponentsBuilder; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; - /** - * A {@code Filter} that processes OAuth 2.0 Authorization Server Configuration Requests. + * A {@code Filter} that processes OAuth 2.0 Authorization Server Metadata Requests. * * @author Daniel Garnier-Moiroux * @since 0.1.1 + * @see OAuth2AuthorizationServerMetadata * @see ProviderSettings * @see 3. Obtaining Authorization Server Metadata */ -public class OAuth2AuthorizationServerConfigurationEndpointFilter extends OncePerRequestFilter { +public class OAuth2AuthorizationServerMetadataEndpointFilter extends OncePerRequestFilter { /** - * The default endpoint {@code URI} for OAuth 2.0 Authorization Server Configuration requests. + * The default endpoint {@code URI} for OAuth 2.0 Authorization Server Metadata requests. */ - public static final String DEFAULT_OAUTH2_AUTHORIZATION_SERVER_CONFIGURATION_ENDPOINT_URI = "/.well-known/oauth-authorization-server"; + public static final String DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI = "/.well-known/oauth-authorization-server"; - private final RequestMatcher requestMatcher; private final ProviderSettings providerSettings; - private final OAuth2AuthorizationServerConfigurationHttpMessageConverter authorizationServerConfigurationHttpMessageConverter - = new OAuth2AuthorizationServerConfigurationHttpMessageConverter(); + private final RequestMatcher requestMatcher; + private final OAuth2AuthorizationServerMetadataHttpMessageConverter authorizationServerMetadataHttpMessageConverter = + new OAuth2AuthorizationServerMetadataHttpMessageConverter(); - public OAuth2AuthorizationServerConfigurationEndpointFilter(ProviderSettings providerSettings) { + public OAuth2AuthorizationServerMetadataEndpointFilter(ProviderSettings providerSettings) { Assert.notNull(providerSettings, "providerSettings cannot be null"); this.providerSettings = providerSettings; this.requestMatcher = new AntPathRequestMatcher( - DEFAULT_OAUTH2_AUTHORIZATION_SERVER_CONFIGURATION_ENDPOINT_URI, + DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI, HttpMethod.GET.name() ); } @@ -68,37 +70,44 @@ public class OAuth2AuthorizationServerConfigurationEndpointFilter extends OncePe @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + if (!this.requestMatcher.matches(request)) { filterChain.doFilter(request, response); return; } - OAuth2AuthorizationServerConfiguration authorizationServerConfiguration = OAuth2AuthorizationServerConfiguration - .builder() + OAuth2AuthorizationServerMetadata authorizationServerMetadata = OAuth2AuthorizationServerMetadata.builder() .issuer(this.providerSettings.issuer()) .authorizationEndpoint(asUrl(this.providerSettings.issuer(), this.providerSettings.authorizationEndpoint())) .tokenEndpoint(asUrl(this.providerSettings.issuer(), this.providerSettings.tokenEndpoint())) - .tokenEndpointAuthenticationMethod("client_secret_basic") // TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_BASIC in Spring Security 5.5.0 - .tokenEndpointAuthenticationMethod("client_secret_post") // TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_POST in Spring Security 5.5.0 - .tokenRevocationEndpoint(asUrl(this.providerSettings.issuer(), this.providerSettings.tokenRevocationEndpoint())) - .tokenRevocationEndpointAuthenticationMethod("client_secret_basic") // TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_BASIC in Spring Security 5.5.0 - .tokenRevocationEndpointAuthenticationMethod("client_secret_post") // TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_POST in Spring Security 5.5.0 - .jwkSetUri(asUrl(this.providerSettings.issuer(), this.providerSettings.jwkSetEndpoint())) + .tokenEndpointAuthenticationMethods(clientAuthenticationMethods()) + .jwkSetUrl(asUrl(this.providerSettings.issuer(), this.providerSettings.jwkSetEndpoint())) + .responseType(OAuth2AuthorizationResponseType.CODE.getValue()) .grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()) .grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) .grantType(AuthorizationGrantType.REFRESH_TOKEN.getValue()) - .responseType(OAuth2AuthorizationResponseType.CODE.getValue()) - .scope(OidcScopes.OPENID) - .codeChallengeMethod(PkceCodeChallengeMethod2.PLAIN.getValue()) - .codeChallengeMethod(PkceCodeChallengeMethod2.S256.getValue()) + .tokenRevocationEndpoint(asUrl(this.providerSettings.issuer(), this.providerSettings.tokenRevocationEndpoint())) + .tokenRevocationEndpointAuthenticationMethods(clientAuthenticationMethods()) + .tokenIntrospectionEndpoint(asUrl(this.providerSettings.issuer(), this.providerSettings.tokenIntrospectionEndpoint())) + .tokenIntrospectionEndpointAuthenticationMethods(clientAuthenticationMethods()) + .codeChallengeMethod("plain") + .codeChallengeMethod("S256") .build(); - ServletServerHttpResponse resp = new ServletServerHttpResponse(response); - this.authorizationServerConfigurationHttpMessageConverter.write( - authorizationServerConfiguration, MediaType.APPLICATION_JSON, resp); + ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response); + this.authorizationServerMetadataHttpMessageConverter.write( + authorizationServerMetadata, MediaType.APPLICATION_JSON, httpResponse); + } + + private static Consumer> clientAuthenticationMethods() { + return (authenticationMethods) -> { + authenticationMethods.add("client_secret_basic"); // TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_BASIC in Spring Security 5.5.0 + authenticationMethods.add("client_secret_post"); // TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_POST in Spring Security 5.5.0 + }; } private static String asUrl(String issuer, String endpoint) { return UriComponentsBuilder.fromUriString(issuer).path(endpoint).toUriString(); } + } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurationTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerMetadataTests.java similarity index 86% rename from oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurationTests.java rename to oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerMetadataTests.java index 51e2f5fb..a2ae28c8 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurationTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerMetadataTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import com.nimbusds.jose.proc.SecurityContext; import org.junit.BeforeClass; 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.Import; @@ -30,7 +31,7 @@ import org.springframework.security.config.test.SpringTestRule; import org.springframework.security.oauth2.jose.TestJwks; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.config.ProviderSettings; -import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationServerConfigurationEndpointFilter; +import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationServerMetadataEndpointFilter; import org.springframework.test.web.servlet.MockMvc; import static org.mockito.Mockito.mock; @@ -39,11 +40,11 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** - * Integration tests for OAuth 2.0 Authorization Server Configuration. + * Integration tests for the OAuth 2.0 Authorization Server Metadata endpoint. * * @author Daniel Garnier-Moiroux */ -public class OAuth2AuthorizationServerConfigurationTests { +public class OAuth2AuthorizationServerMetadataTests { private static final String issuerUrl = "https://example.com/issuer1"; private static JWKSource jwkSource; @@ -60,10 +61,10 @@ public class OAuth2AuthorizationServerConfigurationTests { } @Test - public void requestWhenServerConfigurationRequestAndIssuerSetThenReturnServerConfigurationResponse() throws Exception { + public void requestWhenAuthorizationServerMetadataRequestAndIssuerSetThenReturnMetadataResponse() throws Exception { this.spring.register(AuthorizationServerConfiguration.class).autowire(); - this.mvc.perform(get(OAuth2AuthorizationServerConfigurationEndpointFilter.DEFAULT_OAUTH2_AUTHORIZATION_SERVER_CONFIGURATION_ENDPOINT_URI)) + this.mvc.perform(get(OAuth2AuthorizationServerMetadataEndpointFilter.DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI)) .andExpect(status().is2xxSuccessful()) .andExpect(jsonPath("issuer").value(issuerUrl)) .andReturn(); @@ -88,4 +89,5 @@ public class OAuth2AuthorizationServerConfigurationTests { return new ProviderSettings().issuer(issuerUrl); } } + } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/OAuth2AuthorizationServerMetadataTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/OAuth2AuthorizationServerMetadataTests.java new file mode 100644 index 00000000..3295d1b3 --- /dev/null +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/OAuth2AuthorizationServerMetadataTests.java @@ -0,0 +1,585 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.core; + +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +import org.junit.Test; + +import org.springframework.security.oauth2.core.OAuth2AuthorizationServerMetadata.Builder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link OAuth2AuthorizationServerMetadata}. + * + * @author Daniel Garnier-Moiroux + */ +public class OAuth2AuthorizationServerMetadataTests { + // @formatter:off + private final Builder minimalBuilder = + OAuth2AuthorizationServerMetadata.builder() + .issuer("https://example.com/issuer1") + .authorizationEndpoint("https://example.com/issuer1/oauth2/authorize") + .tokenEndpoint("https://example.com/issuer1/oauth2/token") + .responseType("code"); + // @formatter:on + + @Test + public void buildWhenAllClaimsProvidedThenCreated() { + OAuth2AuthorizationServerMetadata authorizationServerMetadata = OAuth2AuthorizationServerMetadata.builder() + .issuer("https://example.com/issuer1") + .authorizationEndpoint("https://example.com/issuer1/oauth2/authorize") + .tokenEndpoint("https://example.com/issuer1/oauth2/token") + .tokenEndpointAuthenticationMethod("client_secret_basic") + .jwkSetUrl("https://example.com/issuer1/oauth2/jwks") + .scope("openid") + .responseType("code") + .grantType("authorization_code") + .grantType("client_credentials") + .tokenRevocationEndpoint("https://example.com/issuer1/oauth2/revoke") + .tokenRevocationEndpointAuthenticationMethod("client_secret_basic") + .tokenIntrospectionEndpoint("https://example.com/issuer1/oauth2/introspect") + .tokenIntrospectionEndpointAuthenticationMethod("client_secret_basic") + .codeChallengeMethod("plain") + .codeChallengeMethod("S256") + .claim("a-claim", "a-value") + .build(); + + assertThat(authorizationServerMetadata.getIssuer()).isEqualTo(url("https://example.com/issuer1")); + assertThat(authorizationServerMetadata.getAuthorizationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/authorize")); + assertThat(authorizationServerMetadata.getTokenEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/token")); + assertThat(authorizationServerMetadata.getTokenEndpointAuthenticationMethods()).containsExactly("client_secret_basic"); + assertThat(authorizationServerMetadata.getJwkSetUrl()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks")); + assertThat(authorizationServerMetadata.getScopes()).containsExactly("openid"); + assertThat(authorizationServerMetadata.getResponseTypes()).containsExactly("code"); + assertThat(authorizationServerMetadata.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "client_credentials"); + assertThat(authorizationServerMetadata.getTokenRevocationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/revoke")); + assertThat(authorizationServerMetadata.getTokenRevocationEndpointAuthenticationMethods()).containsExactly("client_secret_basic"); + assertThat(authorizationServerMetadata.getTokenIntrospectionEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/introspect")); + assertThat(authorizationServerMetadata.getTokenIntrospectionEndpointAuthenticationMethods()).containsExactly("client_secret_basic"); + assertThat(authorizationServerMetadata.getCodeChallengeMethods()).containsExactlyInAnyOrder("plain", "S256"); + assertThat(authorizationServerMetadata.getClaimAsString("a-claim")).isEqualTo("a-value"); + } + + @Test + public void buildWhenOnlyRequiredClaimsProvidedThenCreated() { + OAuth2AuthorizationServerMetadata authorizationServerMetadata = OAuth2AuthorizationServerMetadata.builder() + .issuer("https://example.com/issuer1") + .authorizationEndpoint("https://example.com/issuer1/oauth2/authorize") + .tokenEndpoint("https://example.com/issuer1/oauth2/token") + .responseType("code") + .build(); + + assertThat(authorizationServerMetadata.getIssuer()).isEqualTo(url("https://example.com/issuer1")); + assertThat(authorizationServerMetadata.getAuthorizationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/authorize")); + assertThat(authorizationServerMetadata.getTokenEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/token")); + assertThat(authorizationServerMetadata.getTokenEndpointAuthenticationMethods()).isNull(); + assertThat(authorizationServerMetadata.getJwkSetUrl()).isNull(); + assertThat(authorizationServerMetadata.getScopes()).isNull(); + assertThat(authorizationServerMetadata.getResponseTypes()).containsExactly("code"); + assertThat(authorizationServerMetadata.getGrantTypes()).isNull(); + assertThat(authorizationServerMetadata.getTokenRevocationEndpoint()).isNull(); + assertThat(authorizationServerMetadata.getTokenRevocationEndpointAuthenticationMethods()).isNull(); + assertThat(authorizationServerMetadata.getTokenIntrospectionEndpoint()).isNull(); + assertThat(authorizationServerMetadata.getTokenIntrospectionEndpointAuthenticationMethods()).isNull(); + assertThat(authorizationServerMetadata.getCodeChallengeMethods()).isNull(); + } + + @Test + public void withClaimsWhenClaimsProvidedThenCreated() { + HashMap claims = new HashMap<>(); + claims.put(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, "https://example.com/issuer1"); + claims.put(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, "https://example.com/issuer1/oauth2/authorize"); + claims.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, "https://example.com/issuer1/oauth2/token"); + claims.put(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI, "https://example.com/issuer1/oauth2/jwks"); + claims.put(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, Collections.singletonList("openid")); + claims.put(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, Collections.singletonList("code")); + claims.put(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT, "https://example.com/issuer1/oauth2/revoke"); + claims.put(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT, "https://example.com/issuer1/oauth2/introspect"); + claims.put("some-claim", "some-value"); + + OAuth2AuthorizationServerMetadata authorizationServerMetadata = OAuth2AuthorizationServerMetadata.withClaims(claims).build(); + + assertThat(authorizationServerMetadata.getIssuer()).isEqualTo(url("https://example.com/issuer1")); + assertThat(authorizationServerMetadata.getAuthorizationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/authorize")); + assertThat(authorizationServerMetadata.getTokenEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/token")); + assertThat(authorizationServerMetadata.getTokenEndpointAuthenticationMethods()).isNull(); + assertThat(authorizationServerMetadata.getJwkSetUrl()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks")); + assertThat(authorizationServerMetadata.getScopes()).containsExactly("openid"); + assertThat(authorizationServerMetadata.getResponseTypes()).containsExactly("code"); + assertThat(authorizationServerMetadata.getGrantTypes()).isNull(); + assertThat(authorizationServerMetadata.getTokenRevocationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/revoke")); + assertThat(authorizationServerMetadata.getTokenRevocationEndpointAuthenticationMethods()).isNull(); + assertThat(authorizationServerMetadata.getTokenIntrospectionEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/introspect")); + assertThat(authorizationServerMetadata.getTokenIntrospectionEndpointAuthenticationMethods()).isNull(); + assertThat(authorizationServerMetadata.getCodeChallengeMethods()).isNull(); + assertThat(authorizationServerMetadata.getClaimAsString("some-claim")).isEqualTo("some-value"); + } + + @Test + public void withClaimsWhenClaimsWithUrlsProvidedThenCreated() { + HashMap claims = new HashMap<>(); + claims.put(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, url("https://example.com/issuer1")); + claims.put(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, url("https://example.com/issuer1/oauth2/authorize")); + claims.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, url("https://example.com/issuer1/oauth2/token")); + claims.put(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI, url("https://example.com/issuer1/oauth2/jwks")); + claims.put(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, Collections.singletonList("code")); + claims.put(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT, url("https://example.com/issuer1/oauth2/revoke")); + claims.put(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT, url("https://example.com/issuer1/oauth2/introspect")); + claims.put("some-claim", "some-value"); + + OAuth2AuthorizationServerMetadata authorizationServerMetadata = OAuth2AuthorizationServerMetadata.withClaims(claims).build(); + + assertThat(authorizationServerMetadata.getIssuer()).isEqualTo(url("https://example.com/issuer1")); + assertThat(authorizationServerMetadata.getAuthorizationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/authorize")); + assertThat(authorizationServerMetadata.getTokenEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/token")); + assertThat(authorizationServerMetadata.getTokenEndpointAuthenticationMethods()).isNull(); + assertThat(authorizationServerMetadata.getJwkSetUrl()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks")); + assertThat(authorizationServerMetadata.getScopes()).isNull(); + assertThat(authorizationServerMetadata.getResponseTypes()).containsExactly("code"); + assertThat(authorizationServerMetadata.getGrantTypes()).isNull(); + assertThat(authorizationServerMetadata.getTokenRevocationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/revoke")); + assertThat(authorizationServerMetadata.getTokenRevocationEndpointAuthenticationMethods()).isNull(); + assertThat(authorizationServerMetadata.getTokenIntrospectionEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/introspect")); + assertThat(authorizationServerMetadata.getTokenIntrospectionEndpointAuthenticationMethods()).isNull(); + assertThat(authorizationServerMetadata.getCodeChallengeMethods()).isNull(); + assertThat(authorizationServerMetadata.getClaimAsString("some-claim")).isEqualTo("some-value"); + } + + @Test + public void withClaimsWhenNullThenThrowIllegalArgumentException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> OAuth2AuthorizationServerMetadata.withClaims(null)) + .withMessage("claims cannot be empty"); + } + + @Test + public void withClaimsWhenMissingRequiredClaimsThenThrowIllegalArgumentException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> OAuth2AuthorizationServerMetadata.withClaims(Collections.emptyMap())) + .withMessage("claims cannot be empty"); + } + + @Test + public void buildWhenCalledTwiceThenGeneratesTwoConfigurations() { + OAuth2AuthorizationServerMetadata first = this.minimalBuilder + .grantType("client_credentials") + .build(); + + OAuth2AuthorizationServerMetadata second = this.minimalBuilder + .claims((claims) -> + { + List newGrantTypes = new ArrayList<>(); + newGrantTypes.add("authorization_code"); + newGrantTypes.add("custom_grant"); + claims.put(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, newGrantTypes); + } + ) + .build(); + + assertThat(first.getGrantTypes()).containsExactly("client_credentials"); + assertThat(second.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "custom_grant"); + } + + @Test + public void buildWhenMissingIssuerThenThrowIllegalArgumentException() { + Builder builder = this.minimalBuilder + .claims((claims) -> claims.remove(OAuth2AuthorizationServerMetadataClaimNames.ISSUER)); + + assertThatIllegalArgumentException() + .isThrownBy(builder::build) + .withMessage("issuer cannot be null"); + } + + @Test + public void buildWhenIssuerNotUrlThenThrowIllegalArgumentException() { + Builder builder = this.minimalBuilder + .claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, "not an url")); + + assertThatIllegalArgumentException() + .isThrownBy(builder::build) + .withMessage("issuer must be a valid URL"); + } + + @Test + public void buildWhenMissingAuthorizationEndpointThenThrowIllegalArgumentException() { + Builder builder = this.minimalBuilder + .claims((claims) -> claims.remove(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT)); + + assertThatIllegalArgumentException() + .isThrownBy(builder::build) + .withMessage("authorizationEndpoint cannot be null"); + } + + @Test + public void buildWhenAuthorizationEndpointNotUrlThenThrowIllegalArgumentException() { + Builder builder = this.minimalBuilder + .claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, "not an url")); + + assertThatIllegalArgumentException() + .isThrownBy(builder::build) + .withMessage("authorizationEndpoint must be a valid URL"); + } + + @Test + public void buildWhenMissingTokenEndpointThenThrowsIllegalArgumentException() { + Builder builder = this.minimalBuilder + .claims((claims) -> claims.remove(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT)); + + assertThatIllegalArgumentException() + .isThrownBy(builder::build) + .withMessage("tokenEndpoint cannot be null"); + } + + @Test + public void buildWhenTokenEndpointNotUrlThenThrowIllegalArgumentException() { + Builder builder = this.minimalBuilder + .claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, "not an url")); + + assertThatIllegalArgumentException() + .isThrownBy(builder::build) + .withMessage("tokenEndpoint must be a valid URL"); + } + + @Test + public void buildWhenTokenEndpointAuthenticationMethodsNotListThenThrowIllegalArgumentException() { + Builder builder = this.minimalBuilder + .claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, "not-a-list")); + + assertThatIllegalArgumentException() + .isThrownBy(builder::build) + .withMessageStartingWith("tokenEndpointAuthenticationMethods must be of type List"); + } + + @Test + public void buildWhenTokenEndpointAuthenticationMethodsEmptyListThenThrowIllegalArgumentException() { + Builder builder = this.minimalBuilder + .claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, Collections.emptyList())); + + assertThatIllegalArgumentException() + .isThrownBy(builder::build) + .withMessage("tokenEndpointAuthenticationMethods cannot be empty"); + } + + @Test + public void buildWhenTokenEndpointAuthenticationMethodsAddingOrRemovingThenCorrectValues() { + OAuth2AuthorizationServerMetadata authorizationServerMetadata = this.minimalBuilder + .tokenEndpointAuthenticationMethod("should-be-removed") + .tokenEndpointAuthenticationMethods(authMethods -> { + authMethods.clear(); + authMethods.add("some-authentication-method"); + }) + .build(); + + assertThat(authorizationServerMetadata.getTokenEndpointAuthenticationMethods()).containsExactly("some-authentication-method"); + } + + @Test + public void buildWhenJwksUriNotUrlThenThrowIllegalArgumentException() { + Builder builder = this.minimalBuilder + .claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI, "not an url")); + + assertThatIllegalArgumentException() + .isThrownBy(builder::build) + .withMessage("jwksUri must be a valid URL"); + } + + @Test + public void buildWhenScopesNotListThenThrowIllegalArgumentException() { + Builder builder = this.minimalBuilder + .claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, "not-a-list")); + + assertThatIllegalArgumentException() + .isThrownBy(builder::build) + .withMessageStartingWith("scopes must be of type List"); + } + + @Test + public void buildWhenScopesEmptyListThenThrowIllegalArgumentException() { + Builder builder = this.minimalBuilder + .claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, Collections.emptyList())); + + assertThatIllegalArgumentException() + .isThrownBy(builder::build) + .withMessage("scopes cannot be empty"); + } + + @Test + public void buildWhenScopesAddingOrRemovingThenCorrectValues() { + OAuth2AuthorizationServerMetadata authorizationServerMetadata = this.minimalBuilder + .scope("should-be-removed") + .scopes(scopes -> { + scopes.clear(); + scopes.add("some-scope"); + }) + .build(); + + assertThat(authorizationServerMetadata.getScopes()).containsExactly("some-scope"); + } + + @Test + public void buildWhenMissingResponseTypesThenThrowIllegalArgumentException() { + Builder builder = this.minimalBuilder + .claims((claims) -> claims.remove(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED)); + + assertThatIllegalArgumentException() + .isThrownBy(builder::build) + .withMessage("responseTypes cannot be null"); + } + + @Test + public void buildWhenResponseTypesNotListThenThrowIllegalArgumentException() { + Builder builder = this.minimalBuilder + .claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, "not-a-list")); + + assertThatIllegalArgumentException() + .isThrownBy(builder::build) + .withMessageStartingWith("responseTypes must be of type List"); + } + + @Test + public void buildWhenResponseTypesEmptyListThenThrowIllegalArgumentException() { + Builder builder = this.minimalBuilder + .claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, Collections.emptyList())); + + assertThatIllegalArgumentException() + .isThrownBy(builder::build) + .withMessage("responseTypes cannot be empty"); + } + + @Test + public void buildWhenResponseTypesAddingOrRemovingThenCorrectValues() { + OAuth2AuthorizationServerMetadata authorizationServerMetadata = this.minimalBuilder + .responseType("should-be-removed") + .responseTypes(responseTypes -> { + responseTypes.clear(); + responseTypes.add("some-response-type"); + }) + .build(); + + assertThat(authorizationServerMetadata.getResponseTypes()).containsExactly("some-response-type"); + } + + @Test + public void buildWhenResponseTypesNotPresentAndAddingThenCorrectValues() { + OAuth2AuthorizationServerMetadata authorizationServerMetadata = this.minimalBuilder + .claims(claims -> claims.remove(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED)) + .responseTypes(responseTypes -> responseTypes.add("some-response-type")) + .build(); + + assertThat(authorizationServerMetadata.getResponseTypes()).containsExactly("some-response-type"); + } + + @Test + public void buildWhenGrantTypesNotListThenThrowIllegalArgumentException() { + Builder builder = this.minimalBuilder + .claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, "not-a-list")); + + assertThatIllegalArgumentException() + .isThrownBy(builder::build) + .withMessageStartingWith("grantTypes must be of type List"); + } + + @Test + public void buildWhenGrantTypesEmptyListThenThrowIllegalArgumentException() { + Builder builder = this.minimalBuilder + .claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, Collections.emptyList())); + + assertThatIllegalArgumentException() + .isThrownBy(builder::build) + .withMessage("grantTypes cannot be empty"); + } + + @Test + public void buildWhenGrantTypesAddingOrRemovingThenCorrectValues() { + OAuth2AuthorizationServerMetadata authorizationServerMetadata = this.minimalBuilder + .grantType("should-be-removed") + .grantTypes(grantTypes -> { + grantTypes.clear(); + grantTypes.add("some-grant-type"); + }) + .build(); + + assertThat(authorizationServerMetadata.getGrantTypes()).containsExactly("some-grant-type"); + } + + @Test + public void buildWhenTokenRevocationEndpointNotUrlThenThrowIllegalArgumentException() { + Builder builder = this.minimalBuilder + .tokenRevocationEndpoint("not a valid URL"); + + assertThatIllegalArgumentException() + .isThrownBy(builder::build) + .withMessage("tokenRevocationEndpoint must be a valid URL"); + } + + @Test + public void buildWhenTokenRevocationEndpointAuthenticationMethodsNotListThenThrowIllegalArgumentException() { + Builder builder = this.minimalBuilder + .claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED, "not-a-list")); + + assertThatIllegalArgumentException() + .isThrownBy(builder::build) + .withMessageStartingWith("tokenRevocationEndpointAuthenticationMethods must be of type List"); + } + + @Test + public void buildWhenTokenRevocationEndpointAuthenticationMethodsEmptyListThenThrowIllegalArgumentException() { + Builder builder = this.minimalBuilder + .claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED, Collections.emptyList())); + + assertThatIllegalArgumentException() + .isThrownBy(builder::build) + .withMessage("tokenRevocationEndpointAuthenticationMethods cannot be empty"); + } + + @Test + public void buildWhenTokenRevocationEndpointAuthenticationMethodsAddingOrRemovingThenCorrectValues() { + OAuth2AuthorizationServerMetadata authorizationServerMetadata = this.minimalBuilder + .tokenRevocationEndpointAuthenticationMethod("should-be-removed") + .tokenRevocationEndpointAuthenticationMethods(authMethods -> { + authMethods.clear(); + authMethods.add("some-authentication-method"); + }) + .build(); + + assertThat(authorizationServerMetadata.getTokenRevocationEndpointAuthenticationMethods()).containsExactly("some-authentication-method"); + } + + @Test + public void buildWhenTokenIntrospectionEndpointNotUrlThenThrowIllegalArgumentException() { + Builder builder = this.minimalBuilder + .tokenIntrospectionEndpoint("not a valid URL"); + + assertThatIllegalArgumentException() + .isThrownBy(builder::build) + .withMessage("tokenIntrospectionEndpoint must be a valid URL"); + } + + @Test + public void buildWhenTokenIntrospectionEndpointAuthenticationMethodsNotListThenThrowIllegalArgumentException() { + Builder builder = this.minimalBuilder + .claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED, "not-a-list")); + + assertThatIllegalArgumentException() + .isThrownBy(builder::build) + .withMessageStartingWith("tokenIntrospectionEndpointAuthenticationMethods must be of type List"); + } + + @Test + public void buildWhenTokenIntrospectionEndpointAuthenticationMethodsEmptyListThenThrowIllegalArgumentException() { + Builder builder = this.minimalBuilder + .claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED, Collections.emptyList())); + + assertThatIllegalArgumentException() + .isThrownBy(builder::build) + .withMessage("tokenIntrospectionEndpointAuthenticationMethods cannot be empty"); + } + + @Test + public void buildWhenTokenIntrospectionEndpointAuthenticationMethodsAddingOrRemovingThenCorrectValues() { + OAuth2AuthorizationServerMetadata authorizationServerMetadata = this.minimalBuilder + .tokenIntrospectionEndpointAuthenticationMethod("should-be-removed") + .tokenIntrospectionEndpointAuthenticationMethods(authMethods -> { + authMethods.clear(); + authMethods.add("some-authentication-method"); + }) + .build(); + + assertThat(authorizationServerMetadata.getTokenIntrospectionEndpointAuthenticationMethods()).containsExactly("some-authentication-method"); + } + + @Test + public void buildWhenCodeChallengeMethodsNotListThenThrowIllegalArgumentException() { + Builder builder = this.minimalBuilder + .claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED, "not-a-list")); + + assertThatIllegalArgumentException() + .isThrownBy(builder::build) + .withMessageStartingWith("codeChallengeMethods must be of type List"); + } + + @Test + public void buildWhenCodeChallengeMethodsEmptyListThenThrowIllegalArgumentException() { + Builder builder = this.minimalBuilder + .claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED, Collections.emptyList())); + + assertThatIllegalArgumentException() + .isThrownBy(builder::build) + .withMessage("codeChallengeMethods cannot be empty"); + } + + @Test + public void buildWhenCodeChallengeMethodsAddingOrRemovingThenCorrectValues() { + OAuth2AuthorizationServerMetadata authorizationServerMetadata = this.minimalBuilder + .codeChallengeMethod("should-be-removed") + .codeChallengeMethods(codeChallengeMethods -> { + codeChallengeMethods.clear(); + codeChallengeMethods.add("some-authentication-method"); + }) + .build(); + + assertThat(authorizationServerMetadata.getCodeChallengeMethods()).containsExactly("some-authentication-method"); + } + + @Test + public void claimWhenNameNullThenThrowIllegalArgumentException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> OAuth2AuthorizationServerMetadata.builder().claim(null, "claim-value")) + .withMessage("name cannot be empty"); + } + + @Test + public void claimWhenValueNullThenThrowIllegalArgumentException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> OAuth2AuthorizationServerMetadata.builder().claim("claim-name", null)) + .withMessage("value cannot be null"); + } + + @Test + public void claimsWhenRemovingClaimThenNotPresent() { + OAuth2AuthorizationServerMetadata authorizationServerMetadata = + this.minimalBuilder + .claim("claim-name", "claim-value") + .claims((claims) -> claims.remove("claim-name")) + .build(); + assertThat(authorizationServerMetadata.containsClaim("claim-name")).isFalse(); + } + + @Test + public void claimsWhenAddingClaimThenPresent() { + OAuth2AuthorizationServerMetadata authorizationServerMetadata = + this.minimalBuilder + .claim("claim-name", "claim-value") + .build(); + assertThat(authorizationServerMetadata.containsClaim("claim-name")).isTrue(); + } + + private static URL url(String urlString) { + try { + return new URL(urlString); + } catch (Exception ex) { + throw new IllegalArgumentException("urlString must be a valid URL and valid URI"); + } + } + +} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationServerConfigurationTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationServerConfigurationTests.java deleted file mode 100644 index 61af91b1..00000000 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationServerConfigurationTests.java +++ /dev/null @@ -1,451 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.security.oauth2.core.endpoint; - -import org.junit.Test; -import org.springframework.security.oauth2.core.OAuth2AuthorizationServerMetadataClaimNames; -import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationServerConfiguration.Builder; - -import java.net.URL; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashSet; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; - -/** - * Tests for {@link OAuth2AuthorizationServerConfiguration}. - * - * @author Daniel Garnier-Moiroux - */ -public class OAuth2AuthorizationServerConfigurationTests { - private final Builder minimalConfigurationBuilder = - OAuth2AuthorizationServerConfiguration.builder() - .issuer("https://example.com/issuer1") - .authorizationEndpoint("https://example.com/issuer1/oauth2/authorize") - .tokenEndpoint("https://example.com/issuer1/oauth2/token") - .jwkSetUri("https://example.com/issuer1/oauth2/jwks") - .scope("openid") - .responseType("code"); - - @Test - public void buildWhenAllRequiredClaimsAndAdditionalClaimsThenCreated() { - OAuth2AuthorizationServerConfiguration authorizationServerConfiguration = OAuth2AuthorizationServerConfiguration.builder() - .issuer("https://example.com/issuer1") - .authorizationEndpoint("https://example.com/issuer1/oauth2/authorize") - .tokenEndpoint("https://example.com/issuer1/oauth2/token") - .tokenRevocationEndpoint("https://example.com/issuer1/oauth2/revoke") - .jwkSetUri("https://example.com/issuer1/oauth2/jwks") - .scope("openid") - .responseType("code") - .grantType("authorization_code") - .grantType("client_credentials") - .tokenEndpointAuthenticationMethod("client_secret_basic") - .tokenRevocationEndpointAuthenticationMethod("client_secret_basic") - .codeChallengeMethod("plain") - .codeChallengeMethod("S256") - .claim("a-claim", "a-value") - .build(); - - assertThat(authorizationServerConfiguration.getIssuer()).isEqualTo(url("https://example.com/issuer1")); - assertThat(authorizationServerConfiguration.getAuthorizationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/authorize")); - assertThat(authorizationServerConfiguration.getTokenEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/token")); - assertThat(authorizationServerConfiguration.getTokenRevocationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/revoke")); - assertThat(authorizationServerConfiguration.getJwkSetUri()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks")); - assertThat(authorizationServerConfiguration.getScopes()).containsExactly("openid"); - assertThat(authorizationServerConfiguration.getResponseTypes()).containsExactly("code"); - assertThat(authorizationServerConfiguration.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "client_credentials"); - assertThat(authorizationServerConfiguration.getTokenEndpointAuthenticationMethods()).containsExactly("client_secret_basic"); - assertThat(authorizationServerConfiguration.getTokenRevocationEndpointAuthenticationMethods()).containsExactly("client_secret_basic"); - assertThat(authorizationServerConfiguration.getCodeChallengeMethods()).containsExactlyInAnyOrder("plain", "S256"); - assertThat(authorizationServerConfiguration.getClaimAsString("a-claim")).isEqualTo("a-value"); - } - - @Test - public void buildWhenOnlyRequiredClaimsThenCreated() { - OAuth2AuthorizationServerConfiguration authorizationServerConfiguration = OAuth2AuthorizationServerConfiguration.builder() - .issuer("https://example.com/issuer1") - .authorizationEndpoint("https://example.com/issuer1/oauth2/authorize") - .tokenEndpoint("https://example.com/issuer1/oauth2/token") - .jwkSetUri("https://example.com/issuer1/oauth2/jwks") - .scope("openid") - .responseType("code") - .build(); - - assertThat(authorizationServerConfiguration.getIssuer()).isEqualTo(url("https://example.com/issuer1")); - assertThat(authorizationServerConfiguration.getAuthorizationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/authorize")); - assertThat(authorizationServerConfiguration.getTokenEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/token")); - assertThat(authorizationServerConfiguration.getJwkSetUri()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks")); - assertThat(authorizationServerConfiguration.getScopes()).containsExactly("openid"); - assertThat(authorizationServerConfiguration.getResponseTypes()).containsExactly("code"); - assertThat(authorizationServerConfiguration.getGrantTypes()).isNull(); - assertThat(authorizationServerConfiguration.getTokenEndpointAuthenticationMethods()).isNull(); - assertThat(authorizationServerConfiguration.getTokenRevocationEndpoint()).isNull(); - assertThat(authorizationServerConfiguration.getTokenRevocationEndpointAuthenticationMethods()).isNull(); - assertThat(authorizationServerConfiguration.getCodeChallengeMethods()).isNull(); - } - - @Test - public void buildFromClaimsThenCreated() { - HashMap claims = new HashMap<>(); - claims.put(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, "https://example.com/issuer1"); - claims.put(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, "https://example.com/issuer1/oauth2/authorize"); - claims.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, "https://example.com/issuer1/oauth2/token"); - claims.put(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI, "https://example.com/issuer1/oauth2/jwks"); - claims.put(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, Collections.singletonList("openid")); - claims.put(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, Collections.singletonList("code")); - claims.put("some-claim", "some-value"); - - OAuth2AuthorizationServerConfiguration authorizationServerConfiguration = OAuth2AuthorizationServerConfiguration.withClaims(claims).build(); - - assertThat(authorizationServerConfiguration.getIssuer()).isEqualTo(url("https://example.com/issuer1")); - assertThat(authorizationServerConfiguration.getAuthorizationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/authorize")); - assertThat(authorizationServerConfiguration.getTokenEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/token")); - assertThat(authorizationServerConfiguration.getJwkSetUri()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks")); - assertThat(authorizationServerConfiguration.getScopes()).containsExactly("openid"); - assertThat(authorizationServerConfiguration.getResponseTypes()).containsExactly("code"); - assertThat(authorizationServerConfiguration.getGrantTypes()).isNull(); - assertThat(authorizationServerConfiguration.getTokenEndpointAuthenticationMethods()).isNull(); - assertThat(authorizationServerConfiguration.getTokenRevocationEndpoint()).isNull(); - assertThat(authorizationServerConfiguration.getTokenRevocationEndpointAuthenticationMethods()).isNull(); - assertThat(authorizationServerConfiguration.getCodeChallengeMethods()).isNull(); - assertThat(authorizationServerConfiguration.getClaimAsString("some-claim")).isEqualTo("some-value"); - } - - @Test - public void buildFromClaimsWhenUsingUrlsThenCreated() { - HashMap claims = new HashMap<>(); - claims.put(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, url("https://example.com/issuer1")); - claims.put(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, url("https://example.com/issuer1/oauth2/authorize")); - claims.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, url("https://example.com/issuer1/oauth2/token")); - claims.put(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT, url("https://example.com/issuer1/oauth2/revoke")); - claims.put(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI, url("https://example.com/issuer1/oauth2/jwks")); - claims.put(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, Collections.singletonList("openid")); - claims.put(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, Collections.singletonList("code")); - claims.put("some-claim", "some-value"); - - OAuth2AuthorizationServerConfiguration authorizationServerConfiguration = OAuth2AuthorizationServerConfiguration.withClaims(claims).build(); - - assertThat(authorizationServerConfiguration.getIssuer()).isEqualTo(url("https://example.com/issuer1")); - assertThat(authorizationServerConfiguration.getAuthorizationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/authorize")); - assertThat(authorizationServerConfiguration.getTokenEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/token")); - assertThat(authorizationServerConfiguration.getTokenRevocationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/revoke")); - assertThat(authorizationServerConfiguration.getJwkSetUri()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks")); - assertThat(authorizationServerConfiguration.getScopes()).containsExactly("openid"); - assertThat(authorizationServerConfiguration.getResponseTypes()).containsExactly("code"); - assertThat(authorizationServerConfiguration.getGrantTypes()).isNull(); - assertThat(authorizationServerConfiguration.getTokenEndpointAuthenticationMethods()).isNull(); - assertThat(authorizationServerConfiguration.getTokenRevocationEndpointAuthenticationMethods()).isNull(); - assertThat(authorizationServerConfiguration.getCodeChallengeMethods()).isNull(); - assertThat(authorizationServerConfiguration.getClaimAsString("some-claim")).isEqualTo("some-value"); - } - - @Test - public void withClaimsWhenNullThenThrowsIllegalArgumentException() { - assertThatIllegalArgumentException() - .isThrownBy(() -> OAuth2AuthorizationServerConfiguration.withClaims(null)) - .withMessage("claims cannot be empty"); - } - - @Test - public void withClaimsWhenMissingRequiredClaimsThenThrowsIllegalArgumentException() { - assertThatIllegalArgumentException() - .isThrownBy(() -> OAuth2AuthorizationServerConfiguration.withClaims(Collections.emptyMap())) - .withMessage("claims cannot be empty"); - } - - @Test - public void buildWhenCalledTwiceThenGeneratesTwoConfigurations() { - OAuth2AuthorizationServerConfiguration first = this.minimalConfigurationBuilder - .grantType("client_credentials") - .build(); - - OAuth2AuthorizationServerConfiguration second = this.minimalConfigurationBuilder - .claims((claims) -> - { - LinkedHashSet newGrantTypes = new LinkedHashSet<>(); - newGrantTypes.add("authorization_code"); - newGrantTypes.add("custom_grant"); - claims.put(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, newGrantTypes); - } - ) - .build(); - - assertThat(first.getGrantTypes()).containsExactly("client_credentials"); - assertThat(second.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "custom_grant"); - } - - @Test - public void buildWhenEmptyClaimsThenOmitted() { - OAuth2AuthorizationServerConfiguration authorizationServerConfiguration = this.minimalConfigurationBuilder - .claim("some-claim", Collections.emptyList()) - .claims(claims -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, Collections.emptyList())) - .build(); - - assertThat(authorizationServerConfiguration.getClaimAsStringList("some-claim")).isNull(); - assertThat(authorizationServerConfiguration.getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED)).isNull(); - } - - @Test - public void buildWhenMissingIssuerThenThrowsIllegalArgumentException() { - Builder builder = this.minimalConfigurationBuilder - .claims((claims) -> claims.remove(OAuth2AuthorizationServerMetadataClaimNames.ISSUER)); - - assertThatIllegalArgumentException() - .isThrownBy(builder::build) - .withMessage("issuer cannot be null"); - } - - @Test - public void buildWhenIssuerIsNotAnUrlThenThrowsIllegalArgumentException() { - Builder builder = this.minimalConfigurationBuilder - .claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, "not an url")); - - assertThatIllegalArgumentException() - .isThrownBy(builder::build) - .withMessageStartingWith("issuer must be a valid URL"); - } - - @Test - public void buildWhenMissingAuthorizationEndpointThenThrowsIllegalArgumentException() { - Builder builder = this.minimalConfigurationBuilder - .claims((claims) -> claims.remove(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT)); - - assertThatIllegalArgumentException() - .isThrownBy(builder::build) - .withMessage("authorizationEndpoint cannot be null"); - } - - @Test - public void buildWhenAuthorizationEndpointIsNotAnUrlThenThrowsIllegalArgumentException() { - Builder builder = this.minimalConfigurationBuilder - .claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, "not an url")); - - assertThatIllegalArgumentException() - .isThrownBy(builder::build) - .withMessageStartingWith("authorizationEndpoint must be a valid URL"); - } - - @Test - public void buildWhenMissingTokenEndpointThenThrowsIllegalArgumentException() { - Builder builder = this.minimalConfigurationBuilder - .claims((claims) -> claims.remove(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT)); - - assertThatIllegalArgumentException() - .isThrownBy(builder::build) - .withMessage("tokenEndpoint cannot be null"); - } - - @Test - public void buildWhenTokenEndpointIsNotAnUrlThenThrowsIllegalArgumentException() { - Builder builder = this.minimalConfigurationBuilder - .claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, "not an url")); - - assertThatIllegalArgumentException() - .isThrownBy(builder::build) - .withMessageStartingWith("tokenEndpoint must be a valid URL"); - } - - @Test - public void buildWhenMissingJwksUriThenThrowsIllegalArgumentException() { - Builder builder = this.minimalConfigurationBuilder - .claims((claims) -> claims.remove(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI)); - - assertThatIllegalArgumentException() - .isThrownBy(builder::build) - .withMessage("jwksUri cannot be null"); - } - - @Test - public void buildWhenJwksUriIsNotAnUrlThenThrowsIllegalArgumentException() { - Builder builder = this.minimalConfigurationBuilder - .claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI, "not an url")); - - assertThatIllegalArgumentException() - .isThrownBy(builder::build) - .withMessageStartingWith("jwksUri must be a valid URL"); - } - - @Test - public void buildWhenMissingResponseTypesThenThrowsIllegalArgumentException() { - Builder builder = this.minimalConfigurationBuilder - .claims((claims) -> claims.remove(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED)); - - assertThatIllegalArgumentException() - .isThrownBy(builder::build) - .withMessage("responseTypes cannot be null"); - } - - @Test - public void buildWhenResponseTypesNotListThenThrowIllegalArgumentException() { - Builder builder = this.minimalConfigurationBuilder - .claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, "not-a-list")); - - assertThatIllegalArgumentException() - .isThrownBy(builder::build) - .withMessageStartingWith("responseTypes must be of type List"); - } - - @Test - public void buildWhenResponseTypesEmptyListThenThrowIllegalArgumentException() { - Builder builder = this.minimalConfigurationBuilder - .claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, Collections.emptyList())); - - assertThatIllegalArgumentException() - .isThrownBy(builder::build) - .withMessage("responseTypes cannot be empty"); - } - - @Test - public void buildWhenInvalidTokenRevocationEndpointThenThrowsIllegalArgumentException() { - Builder builder = this.minimalConfigurationBuilder - .tokenRevocationEndpoint("not a valid URL"); - - assertThatIllegalArgumentException() - .isThrownBy(builder::build) - .withMessage("tokenRevocationEndpoint must be a valid URL"); - } - - @Test - public void responseTypesWhenAddingOrRemovingThenCorrectValues() { - OAuth2AuthorizationServerConfiguration configuration = this.minimalConfigurationBuilder - .responseType("should-be-removed") - .responseTypes(responseTypes -> { - responseTypes.clear(); - responseTypes.add("some-response-type"); - }) - .build(); - - assertThat(configuration.getResponseTypes()).containsExactly("some-response-type"); - } - - @Test - public void responseTypesWhenNotPresentAndAddingThenCorrectValues() { - OAuth2AuthorizationServerConfiguration configuration = this.minimalConfigurationBuilder - .claims(claims -> claims.remove(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED)) - .responseTypes(responseTypes -> responseTypes.add("some-response-type")) - .build(); - - assertThat(configuration.getResponseTypes()).containsExactly("some-response-type"); - } - - @Test - public void scopesWhenAddingOrRemovingThenCorrectValues() { - OAuth2AuthorizationServerConfiguration configuration = this.minimalConfigurationBuilder - .scope("should-be-removed") - .scopes(scopes -> { - scopes.clear(); - scopes.add("some-scope"); - }) - .build(); - - assertThat(configuration.getScopes()).containsExactly("some-scope"); - } - - @Test - public void grantTypesWhenAddingOrRemovingThenCorrectValues() { - OAuth2AuthorizationServerConfiguration configuration = this.minimalConfigurationBuilder - .grantType("should-be-removed") - .grantTypes(grantTypes -> { - grantTypes.clear(); - grantTypes.add("some-grant-type"); - }) - .build(); - - assertThat(configuration.getGrantTypes()).containsExactly("some-grant-type"); - } - - @Test - public void tokenEndpointAuthenticationMethodsWhenAddingOrRemovingThenCorrectValues() { - OAuth2AuthorizationServerConfiguration configuration = this.minimalConfigurationBuilder - .tokenEndpointAuthenticationMethod("should-be-removed") - .tokenEndpointAuthenticationMethods(authMethods -> { - authMethods.clear(); - authMethods.add("some-authentication-method"); - }) - .build(); - - assertThat(configuration.getTokenEndpointAuthenticationMethods()).containsExactly("some-authentication-method"); - } - - @Test - public void tokenRevocationEndpointAuthenticationMethodsWhenAddingOrRemovingThenCorrectValues() { - OAuth2AuthorizationServerConfiguration configuration = this.minimalConfigurationBuilder - .tokenRevocationEndpointAuthenticationMethod("should-be-removed") - .tokenRevocationEndpointAuthenticationMethods(authMethods -> { - authMethods.clear(); - authMethods.add("some-authentication-method"); - }) - .build(); - - assertThat(configuration.getTokenRevocationEndpointAuthenticationMethods()).containsExactly("some-authentication-method"); - } - - @Test - public void codeChallengeMethodsMethodsWhenAddingOrRemovingThenCorrectValues() { - OAuth2AuthorizationServerConfiguration configuration = this.minimalConfigurationBuilder - .codeChallengeMethod("should-be-removed") - .codeChallengeMethods(codeChallengeMethods -> { - codeChallengeMethods.clear(); - codeChallengeMethods.add("some-authentication-method"); - }) - .build(); - - assertThat(configuration.getCodeChallengeMethods()).containsExactly("some-authentication-method"); - } - - @Test - public void claimWhenNameIsNullThenThrowIllegalArgumentException() { - assertThatIllegalArgumentException() - .isThrownBy(() -> OAuth2AuthorizationServerConfiguration.builder().claim(null, "value")) - .withMessage("name cannot be empty"); - } - - @Test - public void claimWhenValueIsNullThenThrowIllegalArgumentException() { - assertThatIllegalArgumentException() - .isThrownBy(() -> OAuth2AuthorizationServerConfiguration.builder().claim("claim-name", null)) - .withMessage("value cannot be null"); - } - - @Test - public void claimsWhenRemovingClaimThenNotPresent() { - OAuth2AuthorizationServerConfiguration configuration = - this.minimalConfigurationBuilder - .grantType("some-grant-type") - .claims((claims) -> claims.remove(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED)) - .build(); - assertThat(configuration.getGrantTypes()).isNull(); - } - - @Test - public void claimsWhenAddingClaimThenPresent() { - OAuth2AuthorizationServerConfiguration configuration = - this.minimalConfigurationBuilder - .claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, "authorization_code")) - .build(); - assertThat(configuration.getGrantTypes()).containsExactly("authorization_code"); - } - - private static URL url(String urlString) { - try { - return new URL(urlString); - } catch (Exception ex) { - throw new IllegalArgumentException("urlString must be a valid URL and valid URI"); - } - } -} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/endpoint/PkceCodeChallengeMethod2Test.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/endpoint/PkceCodeChallengeMethod2Test.java deleted file mode 100644 index e31f04cb..00000000 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/endpoint/PkceCodeChallengeMethod2Test.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.security.oauth2.core.endpoint; - -import org.junit.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * TODO - * This class is temporary and will be removed after upgrading to Spring Security 5.5.0 GA. - * - * Tests for {@link PkceCodeChallengeMethod2}. - * - * @author Daniel Garnier-Moiroux - */ -public class PkceCodeChallengeMethod2Test { - - @Test - public void getValueWhenCodeChallengeMethodPlainThenReturnPlain() { - assertThat(PkceCodeChallengeMethod2.PLAIN.getValue()).isEqualTo("plain"); - } - - @Test - public void getValueWhenCodeChallengeMethodS256ThenReturnS256() { - assertThat(PkceCodeChallengeMethod2.S256.getValue()).isEqualTo("S256"); - } -} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/http/converter/OAuth2AuthorizationServerConfigurationHttpMessageConverterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/http/converter/OAuth2AuthorizationServerConfigurationHttpMessageConverterTests.java deleted file mode 100644 index d96147aa..00000000 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/http/converter/OAuth2AuthorizationServerConfigurationHttpMessageConverterTests.java +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.security.oauth2.core.http.converter; - - -import org.junit.Test; -import org.springframework.core.convert.converter.Converter; -import org.springframework.http.HttpStatus; -import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.http.converter.HttpMessageNotWritableException; -import org.springframework.mock.http.MockHttpOutputMessage; -import org.springframework.mock.http.client.MockClientHttpResponse; -import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationServerConfiguration; - -import java.net.URL; -import java.util.Arrays; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; - -/** - * Tests for {@link OAuth2AuthorizationServerConfigurationHttpMessageConverter} - * - * @author Daniel Garnier-Moiroux - */ -public class OAuth2AuthorizationServerConfigurationHttpMessageConverterTests { - private final OAuth2AuthorizationServerConfigurationHttpMessageConverter messageConverter = new OAuth2AuthorizationServerConfigurationHttpMessageConverter(); - - @Test - public void supportsWhenOAuth2AuthorizationServerConfigurationThenTrue() { - assertThat(this.messageConverter.supports(OAuth2AuthorizationServerConfiguration.class)).isTrue(); - } - - @Test - public void setAuthorizationServerConfigurationParametersConverterWhenConverterIsNullThenThrowIllegalArgumentException() { - assertThatIllegalArgumentException().isThrownBy(() -> this.messageConverter.setAuthorizationServerConfigurationParametersConverter(null)); - } - - @Test - public void setAuthorizationServerConfigurationConverterWhenConverterIsNullThenThrowIllegalArgumentException() { - assertThatIllegalArgumentException().isThrownBy(() -> this.messageConverter.setAuthorizationServerConfigurationConverter(null)); - } - - @Test - public void readInternalWhenRequiredParametersThenSuccess() throws Exception { - // @formatter:off - String serverConfigurationResponse = "{\n" - + " \"issuer\": \"https://example.com/issuer1\",\n" - + " \"authorization_endpoint\": \"https://example.com/issuer1/oauth2/authorize\",\n" - + " \"token_endpoint\": \"https://example.com/issuer1/oauth2/token\",\n" - + " \"jwks_uri\": \"https://example.com/issuer1/oauth2/jwks\",\n" - + " \"response_types_supported\": [\"code\"]\n" - + "}\n"; - // @formatter:on - MockClientHttpResponse response = new MockClientHttpResponse(serverConfigurationResponse.getBytes(), HttpStatus.OK); - OAuth2AuthorizationServerConfiguration serverConfiguration = this.messageConverter - .readInternal(OAuth2AuthorizationServerConfiguration.class, response); - - assertThat(serverConfiguration.getIssuer()).isEqualTo(new URL("https://example.com/issuer1")); - assertThat(serverConfiguration.getAuthorizationEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/authorize")); - assertThat(serverConfiguration.getTokenEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/token")); - assertThat(serverConfiguration.getJwkSetUri()).isEqualTo(new URL("https://example.com/issuer1/oauth2/jwks")); - assertThat(serverConfiguration.getResponseTypes()).containsExactly("code"); - assertThat(serverConfiguration.getScopes()).isNull(); - assertThat(serverConfiguration.getGrantTypes()).isNull(); - assertThat(serverConfiguration.getTokenEndpointAuthenticationMethods()).isNull(); - assertThat(serverConfiguration.getCodeChallengeMethods()).isNull(); - assertThat(serverConfiguration.getTokenRevocationEndpoint()).isNull(); - assertThat(serverConfiguration.getTokenRevocationEndpointAuthenticationMethods()).isNull(); - } - - @Test - public void readInternalWhenValidParametersThenSuccess() throws Exception { - // @formatter:off - String serverConfigurationResponse = "{\n" - + " \"issuer\": \"https://example.com/issuer1\",\n" - + " \"authorization_endpoint\": \"https://example.com/issuer1/oauth2/authorize\",\n" - + " \"token_endpoint\": \"https://example.com/issuer1/oauth2/token\",\n" - + " \"revocation_endpoint\": \"https://example.com/issuer1/oauth2/revoke\",\n" - + " \"jwks_uri\": \"https://example.com/issuer1/oauth2/jwks\",\n" - + " \"response_types_supported\": [\"code\"],\n" - + " \"grant_types_supported\": [\"authorization_code\", \"client_credentials\"],\n" - + " \"scopes_supported\": [\"openid\"],\n" - + " \"token_endpoint_auth_methods_supported\": [\"client_secret_basic\"],\n" - + " \"revocation_endpoint_auth_methods_supported\": [\"client_secret_basic\"],\n" - + " \"code_challenge_methods_supported\": [\"plain\",\"S256\"],\n" - + " \"custom_claim\": \"value\",\n" - + " \"custom_collection_claim\": [\"value1\", \"value2\"]\n" - + "}\n"; - // @formatter:on - MockClientHttpResponse response = new MockClientHttpResponse(serverConfigurationResponse.getBytes(), HttpStatus.OK); - OAuth2AuthorizationServerConfiguration serverConfiguration = this.messageConverter - .readInternal(OAuth2AuthorizationServerConfiguration.class, response); - - assertThat(serverConfiguration.getClaims()).hasSize(13); - assertThat(serverConfiguration.getIssuer()).isEqualTo(new URL("https://example.com/issuer1")); - assertThat(serverConfiguration.getAuthorizationEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/authorize")); - assertThat(serverConfiguration.getTokenEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/token")); - assertThat(serverConfiguration.getTokenRevocationEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/revoke")); - assertThat(serverConfiguration.getJwkSetUri()).isEqualTo(new URL("https://example.com/issuer1/oauth2/jwks")); - assertThat(serverConfiguration.getResponseTypes()).containsExactly("code"); - assertThat(serverConfiguration.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "client_credentials"); - assertThat(serverConfiguration.getScopes()).containsExactly("openid"); - assertThat(serverConfiguration.getTokenEndpointAuthenticationMethods()).containsExactly("client_secret_basic"); - assertThat(serverConfiguration.getTokenRevocationEndpointAuthenticationMethods()).containsExactly("client_secret_basic"); - assertThat(serverConfiguration.getCodeChallengeMethods()).containsExactlyInAnyOrder("plain", "S256"); - assertThat(serverConfiguration.getClaimAsString("custom_claim")).isEqualTo("value"); - assertThat(serverConfiguration.getClaimAsStringList("custom_collection_claim")).containsExactlyInAnyOrder("value1", "value2"); - } - - @Test - public void readInternalWhenFailingConverterThenThrowException() { - String errorMessage = "this is not a valid converter"; - this.messageConverter.setAuthorizationServerConfigurationConverter(source -> { - throw new RuntimeException(errorMessage); - }); - MockClientHttpResponse response = new MockClientHttpResponse("{}".getBytes(), HttpStatus.OK); - - assertThatExceptionOfType(HttpMessageNotReadableException.class) - .isThrownBy(() -> this.messageConverter.readInternal(OAuth2AuthorizationServerConfiguration.class, response)) - .withMessageContaining("An error occurred reading the OAuth 2.0 Authorization Server Configuration") - .withMessageContaining(errorMessage); - } - - @Test - public void readInternalWhenInvalidOAuth2AuthorizationServerConfigurationThenThrowException() { - String providerConfigurationResponse = "{ \"issuer\": null }"; - MockClientHttpResponse response = new MockClientHttpResponse(providerConfigurationResponse.getBytes(), HttpStatus.OK); - - assertThatExceptionOfType(HttpMessageNotReadableException.class) - .isThrownBy(() -> this.messageConverter.readInternal(OAuth2AuthorizationServerConfiguration.class, response)) - .withMessageContaining("An error occurred reading the OAuth 2.0 Authorization Server Configuration") - .withMessageContaining("issuer cannot be null"); - } - - @Test - public void writeInternalWhenOAuth2AuthorizationServerConfigurationThenSuccess() { - OAuth2AuthorizationServerConfiguration serverConfiguration = - OAuth2AuthorizationServerConfiguration - .builder() - .issuer("https://example.com/issuer1") - .authorizationEndpoint("https://example.com/issuer1/oauth2/authorize") - .tokenEndpoint("https://example.com/issuer1/oauth2/token") - .tokenRevocationEndpoint("https://example.com/issuer1/oauth2/revoke") - .jwkSetUri("https://example.com/issuer1/oauth2/jwks") - .scope("openid") - .responseType("code") - .grantType("authorization_code") - .grantType("client_credentials") - .tokenEndpointAuthenticationMethod("client_secret_basic") - .tokenRevocationEndpointAuthenticationMethod("client_secret_basic") - .codeChallengeMethod("plain") - .codeChallengeMethod("S256") - .claim("custom_claim", "value") - .claim("custom_collection_claim", Arrays.asList("value1", "value2")) - .build(); - MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); - - this.messageConverter.writeInternal(serverConfiguration, outputMessage); - - String serverConfigurationResponse = outputMessage.getBodyAsString(); - assertThat(serverConfigurationResponse).contains("\"issuer\":\"https://example.com/issuer1\""); - assertThat(serverConfigurationResponse).contains("\"authorization_endpoint\":\"https://example.com/issuer1/oauth2/authorize\""); - assertThat(serverConfigurationResponse).contains("\"token_endpoint\":\"https://example.com/issuer1/oauth2/token\""); - assertThat(serverConfigurationResponse).contains("\"revocation_endpoint\":\"https://example.com/issuer1/oauth2/revoke\""); - assertThat(serverConfigurationResponse).contains("\"jwks_uri\":\"https://example.com/issuer1/oauth2/jwks\""); - assertThat(serverConfigurationResponse).contains("\"scopes_supported\":[\"openid\"]"); - assertThat(serverConfigurationResponse).contains("\"response_types_supported\":[\"code\"]"); - assertThat(serverConfigurationResponse).contains("\"grant_types_supported\":[\"authorization_code\",\"client_credentials\"]"); - assertThat(serverConfigurationResponse).contains("\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\"]"); - assertThat(serverConfigurationResponse).contains("\"revocation_endpoint_auth_methods_supported\":[\"client_secret_basic\"]"); - assertThat(serverConfigurationResponse).contains("\"code_challenge_methods_supported\":[\"plain\",\"S256\"]"); - assertThat(serverConfigurationResponse).contains("\"custom_claim\":\"value\""); - assertThat(serverConfigurationResponse).contains("\"custom_collection_claim\":[\"value1\",\"value2\"]"); - - } - - @Test - public void writeInternalWhenWriteFailsThenThrowsException() { - String errorMessage = "this is not a valid converter"; - Converter> failingConverter = - source -> { - throw new RuntimeException(errorMessage); - }; - this.messageConverter.setAuthorizationServerConfigurationParametersConverter(failingConverter); - - MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); - OAuth2AuthorizationServerConfiguration serverConfiguration = - OAuth2AuthorizationServerConfiguration - .builder() - .issuer("https://example.com/issuer1") - .authorizationEndpoint("https://example.com/issuer1/oauth2/authorize") - .tokenEndpoint("https://example.com/issuer1/oauth2/token") - .jwkSetUri("https://example.com/issuer1/oauth2/jwks") - .responseType("code") - .build(); - - assertThatExceptionOfType(HttpMessageNotWritableException.class) - .isThrownBy(() -> this.messageConverter.writeInternal(serverConfiguration, outputMessage)) - .withMessageContaining("An error occurred writing the OAuth 2.0 Authorization Server Configuration") - .withMessageContaining(errorMessage); - } -} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/http/converter/OAuth2AuthorizationServerMetadataHttpMessageConverterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/http/converter/OAuth2AuthorizationServerMetadataHttpMessageConverterTests.java new file mode 100644 index 00000000..1cff7d4f --- /dev/null +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/http/converter/OAuth2AuthorizationServerMetadataHttpMessageConverterTests.java @@ -0,0 +1,224 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.core.http.converter; + + +import java.net.URL; +import java.util.Arrays; +import java.util.Map; + +import org.junit.Test; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.mock.http.MockHttpOutputMessage; +import org.springframework.mock.http.client.MockClientHttpResponse; +import org.springframework.security.oauth2.core.OAuth2AuthorizationServerMetadata; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link OAuth2AuthorizationServerMetadataHttpMessageConverter} + * + * @author Daniel Garnier-Moiroux + */ +public class OAuth2AuthorizationServerMetadataHttpMessageConverterTests { + private final OAuth2AuthorizationServerMetadataHttpMessageConverter messageConverter = new OAuth2AuthorizationServerMetadataHttpMessageConverter(); + + @Test + public void supportsWhenOAuth2AuthorizationServerMetadataThenTrue() { + assertThat(this.messageConverter.supports(OAuth2AuthorizationServerMetadata.class)).isTrue(); + } + + @Test + public void setAuthorizationServerMetadataParametersConverterWhenConverterIsNullThenThrowIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.messageConverter.setAuthorizationServerMetadataParametersConverter(null)); + } + + @Test + public void setAuthorizationServerMetadataConverterWhenConverterIsNullThenThrowIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.messageConverter.setAuthorizationServerMetadataConverter(null)); + } + + @Test + public void readInternalWhenRequiredParametersThenSuccess() throws Exception { + // @formatter:off + String authorizationServerMetadataResponse = "{\n" + + " \"issuer\": \"https://example.com/issuer1\",\n" + + " \"authorization_endpoint\": \"https://example.com/issuer1/oauth2/authorize\",\n" + + " \"token_endpoint\": \"https://example.com/issuer1/oauth2/token\",\n" + + " \"response_types_supported\": [\"code\"]\n" + + "}\n"; + // @formatter:on + MockClientHttpResponse response = new MockClientHttpResponse(authorizationServerMetadataResponse.getBytes(), HttpStatus.OK); + OAuth2AuthorizationServerMetadata authorizationServerMetadata = this.messageConverter + .readInternal(OAuth2AuthorizationServerMetadata.class, response); + + assertThat(authorizationServerMetadata.getIssuer()).isEqualTo(new URL("https://example.com/issuer1")); + assertThat(authorizationServerMetadata.getAuthorizationEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/authorize")); + assertThat(authorizationServerMetadata.getTokenEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/token")); + assertThat(authorizationServerMetadata.getTokenEndpointAuthenticationMethods()).isNull(); + assertThat(authorizationServerMetadata.getJwkSetUrl()).isNull(); + assertThat(authorizationServerMetadata.getResponseTypes()).containsExactly("code"); + assertThat(authorizationServerMetadata.getScopes()).isNull(); + assertThat(authorizationServerMetadata.getGrantTypes()).isNull(); + assertThat(authorizationServerMetadata.getTokenRevocationEndpoint()).isNull(); + assertThat(authorizationServerMetadata.getTokenRevocationEndpointAuthenticationMethods()).isNull(); + assertThat(authorizationServerMetadata.getTokenIntrospectionEndpoint()).isNull(); + assertThat(authorizationServerMetadata.getTokenIntrospectionEndpointAuthenticationMethods()).isNull(); + assertThat(authorizationServerMetadata.getCodeChallengeMethods()).isNull(); + } + + @Test + public void readInternalWhenValidParametersThenSuccess() throws Exception { + // @formatter:off + String authorizationServerMetadataResponse = "{\n" + + " \"issuer\": \"https://example.com/issuer1\",\n" + + " \"authorization_endpoint\": \"https://example.com/issuer1/oauth2/authorize\",\n" + + " \"token_endpoint\": \"https://example.com/issuer1/oauth2/token\",\n" + + " \"token_endpoint_auth_methods_supported\": [\"client_secret_basic\"],\n" + + " \"jwks_uri\": \"https://example.com/issuer1/oauth2/jwks\",\n" + + " \"scopes_supported\": [\"openid\"],\n" + + " \"response_types_supported\": [\"code\"],\n" + + " \"grant_types_supported\": [\"authorization_code\", \"client_credentials\"],\n" + + " \"revocation_endpoint\": \"https://example.com/issuer1/oauth2/revoke\",\n" + + " \"revocation_endpoint_auth_methods_supported\": [\"client_secret_basic\"],\n" + + " \"introspection_endpoint\": \"https://example.com/issuer1/oauth2/introspect\",\n" + + " \"introspection_endpoint_auth_methods_supported\": [\"client_secret_basic\"],\n" + + " \"code_challenge_methods_supported\": [\"plain\",\"S256\"],\n" + + " \"custom_claim\": \"value\",\n" + + " \"custom_collection_claim\": [\"value1\", \"value2\"]\n" + + "}\n"; + // @formatter:on + MockClientHttpResponse response = new MockClientHttpResponse(authorizationServerMetadataResponse.getBytes(), HttpStatus.OK); + OAuth2AuthorizationServerMetadata authorizationServerMetadata = this.messageConverter + .readInternal(OAuth2AuthorizationServerMetadata.class, response); + + assertThat(authorizationServerMetadata.getClaims()).hasSize(15); + assertThat(authorizationServerMetadata.getIssuer()).isEqualTo(new URL("https://example.com/issuer1")); + assertThat(authorizationServerMetadata.getAuthorizationEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/authorize")); + assertThat(authorizationServerMetadata.getTokenEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/token")); + assertThat(authorizationServerMetadata.getTokenEndpointAuthenticationMethods()).containsExactly("client_secret_basic"); + assertThat(authorizationServerMetadata.getJwkSetUrl()).isEqualTo(new URL("https://example.com/issuer1/oauth2/jwks")); + assertThat(authorizationServerMetadata.getScopes()).containsExactly("openid"); + assertThat(authorizationServerMetadata.getResponseTypes()).containsExactly("code"); + assertThat(authorizationServerMetadata.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "client_credentials"); + assertThat(authorizationServerMetadata.getTokenRevocationEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/revoke")); + assertThat(authorizationServerMetadata.getTokenRevocationEndpointAuthenticationMethods()).containsExactly("client_secret_basic"); + assertThat(authorizationServerMetadata.getTokenIntrospectionEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/introspect")); + assertThat(authorizationServerMetadata.getTokenIntrospectionEndpointAuthenticationMethods()).containsExactly("client_secret_basic"); + assertThat(authorizationServerMetadata.getCodeChallengeMethods()).containsExactlyInAnyOrder("plain", "S256"); + assertThat(authorizationServerMetadata.getClaimAsString("custom_claim")).isEqualTo("value"); + assertThat(authorizationServerMetadata.getClaimAsStringList("custom_collection_claim")).containsExactlyInAnyOrder("value1", "value2"); + } + + @Test + public void readInternalWhenFailingConverterThenThrowException() { + String errorMessage = "this is not a valid converter"; + this.messageConverter.setAuthorizationServerMetadataConverter(source -> { + throw new RuntimeException(errorMessage); + }); + MockClientHttpResponse response = new MockClientHttpResponse("{}".getBytes(), HttpStatus.OK); + + assertThatExceptionOfType(HttpMessageNotReadableException.class) + .isThrownBy(() -> this.messageConverter.readInternal(OAuth2AuthorizationServerMetadata.class, response)) + .withMessageContaining("An error occurred reading the OAuth 2.0 Authorization Server Metadata") + .withMessageContaining(errorMessage); + } + + @Test + public void readInternalWhenInvalidOAuth2AuthorizationServerMetadataThenThrowException() { + String authorizationServerMetadataResponse = "{ \"issuer\": null }"; + MockClientHttpResponse response = new MockClientHttpResponse(authorizationServerMetadataResponse.getBytes(), HttpStatus.OK); + + assertThatExceptionOfType(HttpMessageNotReadableException.class) + .isThrownBy(() -> this.messageConverter.readInternal(OAuth2AuthorizationServerMetadata.class, response)) + .withMessageContaining("An error occurred reading the OAuth 2.0 Authorization Server Metadata") + .withMessageContaining("issuer cannot be null"); + } + + @Test + public void writeInternalWhenOAuth2AuthorizationServerMetadataThenSuccess() { + OAuth2AuthorizationServerMetadata authorizationServerMetadata = + OAuth2AuthorizationServerMetadata.builder() + .issuer("https://example.com/issuer1") + .authorizationEndpoint("https://example.com/issuer1/oauth2/authorize") + .tokenEndpoint("https://example.com/issuer1/oauth2/token") + .tokenEndpointAuthenticationMethod("client_secret_basic") + .jwkSetUrl("https://example.com/issuer1/oauth2/jwks") + .scope("openid") + .responseType("code") + .grantType("authorization_code") + .grantType("client_credentials") + .tokenRevocationEndpoint("https://example.com/issuer1/oauth2/revoke") + .tokenRevocationEndpointAuthenticationMethod("client_secret_basic") + .tokenIntrospectionEndpoint("https://example.com/issuer1/oauth2/introspect") + .tokenIntrospectionEndpointAuthenticationMethod("client_secret_basic") + .codeChallengeMethod("plain") + .codeChallengeMethod("S256") + .claim("custom_claim", "value") + .claim("custom_collection_claim", Arrays.asList("value1", "value2")) + .build(); + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + + this.messageConverter.writeInternal(authorizationServerMetadata, outputMessage); + + String authorizationServerMetadataResponse = outputMessage.getBodyAsString(); + assertThat(authorizationServerMetadataResponse).contains("\"issuer\":\"https://example.com/issuer1\""); + assertThat(authorizationServerMetadataResponse).contains("\"authorization_endpoint\":\"https://example.com/issuer1/oauth2/authorize\""); + assertThat(authorizationServerMetadataResponse).contains("\"token_endpoint\":\"https://example.com/issuer1/oauth2/token\""); + assertThat(authorizationServerMetadataResponse).contains("\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\"]"); + assertThat(authorizationServerMetadataResponse).contains("\"jwks_uri\":\"https://example.com/issuer1/oauth2/jwks\""); + assertThat(authorizationServerMetadataResponse).contains("\"scopes_supported\":[\"openid\"]"); + assertThat(authorizationServerMetadataResponse).contains("\"response_types_supported\":[\"code\"]"); + assertThat(authorizationServerMetadataResponse).contains("\"grant_types_supported\":[\"authorization_code\",\"client_credentials\"]"); + assertThat(authorizationServerMetadataResponse).contains("\"revocation_endpoint\":\"https://example.com/issuer1/oauth2/revoke\""); + assertThat(authorizationServerMetadataResponse).contains("\"revocation_endpoint_auth_methods_supported\":[\"client_secret_basic\"]"); + assertThat(authorizationServerMetadataResponse).contains("\"introspection_endpoint\":\"https://example.com/issuer1/oauth2/introspect\""); + assertThat(authorizationServerMetadataResponse).contains("\"introspection_endpoint_auth_methods_supported\":[\"client_secret_basic\"]"); + assertThat(authorizationServerMetadataResponse).contains("\"code_challenge_methods_supported\":[\"plain\",\"S256\"]"); + assertThat(authorizationServerMetadataResponse).contains("\"custom_claim\":\"value\""); + assertThat(authorizationServerMetadataResponse).contains("\"custom_collection_claim\":[\"value1\",\"value2\"]"); + } + + @Test + public void writeInternalWhenWriteFailsThenThrowException() { + String errorMessage = "this is not a valid converter"; + Converter> failingConverter = + source -> { + throw new RuntimeException(errorMessage); + }; + this.messageConverter.setAuthorizationServerMetadataParametersConverter(failingConverter); + + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + OAuth2AuthorizationServerMetadata authorizationServerMetadata = + OAuth2AuthorizationServerMetadata.builder() + .issuer("https://example.com/issuer1") + .authorizationEndpoint("https://example.com/issuer1/oauth2/authorize") + .tokenEndpoint("https://example.com/issuer1/oauth2/token") + .responseType("code") + .build(); + + assertThatExceptionOfType(HttpMessageNotWritableException.class) + .isThrownBy(() -> this.messageConverter.writeInternal(authorizationServerMetadata, outputMessage)) + .withMessageContaining("An error occurred writing the OAuth 2.0 Authorization Server Metadata") + .withMessageContaining(errorMessage); + } +} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/OidcProviderConfigurationTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/OidcProviderConfigurationTests.java index ebc979cf..25e8b1f3 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/OidcProviderConfigurationTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/OidcProviderConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,14 +15,14 @@ */ package org.springframework.security.oauth2.core.oidc; -import org.junit.Test; - import java.net.URL; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; -import java.util.Set; + +import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -38,7 +38,7 @@ public class OidcProviderConfigurationTests { .issuer("https://example.com/issuer1") .authorizationEndpoint("https://example.com/issuer1/oauth2/authorize") .tokenEndpoint("https://example.com/issuer1/oauth2/token") - .jwkSetUri("https://example.com/issuer1/oauth2/jwks") + .jwkSetUrl("https://example.com/issuer1/oauth2/jwks") .scope("openid") .responseType("code") .subjectType("public") @@ -50,7 +50,7 @@ public class OidcProviderConfigurationTests { .issuer("https://example.com/issuer1") .authorizationEndpoint("https://example.com/issuer1/oauth2/authorize") .tokenEndpoint("https://example.com/issuer1/oauth2/token") - .jwkSetUri("https://example.com/issuer1/oauth2/jwks") + .jwkSetUrl("https://example.com/issuer1/oauth2/jwks") .scope("openid") .responseType("code") .grantType("authorization_code") @@ -64,7 +64,7 @@ public class OidcProviderConfigurationTests { assertThat(providerConfiguration.getIssuer()).isEqualTo(url("https://example.com/issuer1")); assertThat(providerConfiguration.getAuthorizationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/authorize")); assertThat(providerConfiguration.getTokenEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/token")); - assertThat(providerConfiguration.getJwkSetUri()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks")); + assertThat(providerConfiguration.getJwkSetUrl()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks")); assertThat(providerConfiguration.getScopes()).containsExactly("openid"); assertThat(providerConfiguration.getResponseTypes()).containsExactly("code"); assertThat(providerConfiguration.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "client_credentials"); @@ -80,7 +80,7 @@ public class OidcProviderConfigurationTests { .issuer("https://example.com/issuer1") .authorizationEndpoint("https://example.com/issuer1/oauth2/authorize") .tokenEndpoint("https://example.com/issuer1/oauth2/token") - .jwkSetUri("https://example.com/issuer1/oauth2/jwks") + .jwkSetUrl("https://example.com/issuer1/oauth2/jwks") .scope("openid") .responseType("code") .subjectType("public") @@ -90,7 +90,7 @@ public class OidcProviderConfigurationTests { assertThat(providerConfiguration.getIssuer()).isEqualTo(url("https://example.com/issuer1")); assertThat(providerConfiguration.getAuthorizationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/authorize")); assertThat(providerConfiguration.getTokenEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/token")); - assertThat(providerConfiguration.getJwkSetUri()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks")); + assertThat(providerConfiguration.getJwkSetUrl()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks")); assertThat(providerConfiguration.getScopes()).containsExactly("openid"); assertThat(providerConfiguration.getResponseTypes()).containsExactly("code"); assertThat(providerConfiguration.getGrantTypes()).isNull(); @@ -117,7 +117,7 @@ public class OidcProviderConfigurationTests { assertThat(providerConfiguration.getIssuer()).isEqualTo(url("https://example.com/issuer1")); assertThat(providerConfiguration.getAuthorizationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/authorize")); assertThat(providerConfiguration.getTokenEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/token")); - assertThat(providerConfiguration.getJwkSetUri()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks")); + assertThat(providerConfiguration.getJwkSetUrl()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks")); assertThat(providerConfiguration.getScopes()).containsExactly("openid"); assertThat(providerConfiguration.getResponseTypes()).containsExactly("code"); assertThat(providerConfiguration.getGrantTypes()).isNull(); @@ -145,7 +145,7 @@ public class OidcProviderConfigurationTests { assertThat(providerConfiguration.getIssuer()).isEqualTo(url("https://example.com/issuer1")); assertThat(providerConfiguration.getAuthorizationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/authorize")); assertThat(providerConfiguration.getTokenEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/token")); - assertThat(providerConfiguration.getJwkSetUri()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks")); + assertThat(providerConfiguration.getJwkSetUrl()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks")); assertThat(providerConfiguration.getScopes()).containsExactly("openid"); assertThat(providerConfiguration.getResponseTypes()).containsExactly("code"); assertThat(providerConfiguration.getGrantTypes()).isNull(); @@ -178,7 +178,7 @@ public class OidcProviderConfigurationTests { OidcProviderConfiguration second = this.minimalConfigurationBuilder .claims((claims) -> { - Set newGrantTypes = new LinkedHashSet<>(); + List newGrantTypes = new ArrayList<>(); newGrantTypes.add("authorization_code"); newGrantTypes.add("custom_grant"); claims.put(OidcProviderMetadataClaimNames.GRANT_TYPES_SUPPORTED, newGrantTypes); @@ -190,17 +190,6 @@ public class OidcProviderConfigurationTests { assertThat(second.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "custom_grant"); } - @Test - public void buildWhenEmptyClaimsThenOmitted() { - OidcProviderConfiguration providerConfiguration = this.minimalConfigurationBuilder - .claim("some-claim", Collections.emptyList()) - .claims(claims -> claims.put(OidcProviderMetadataClaimNames.GRANT_TYPES_SUPPORTED, Collections.emptyList())) - .build(); - - assertThat(providerConfiguration.getClaimAsStringList("some-claim")).isNull(); - assertThat(providerConfiguration.getClaimAsStringList(OidcProviderMetadataClaimNames.GRANT_TYPES_SUPPORTED)).isNull(); - } - @Test public void buildWhenMissingIssuerThenThrowIllegalArgumentException() { OidcProviderConfiguration.Builder builder = this.minimalConfigurationBuilder @@ -505,9 +494,9 @@ public class OidcProviderConfigurationTests { public void claimsWhenAddingClaimThenPresent() { OidcProviderConfiguration configuration = this.minimalConfigurationBuilder - .claims((claims) -> claims.put(OidcProviderMetadataClaimNames.GRANT_TYPES_SUPPORTED, "authorization_code")) + .claim("claim-name", "claim-value") .build(); - assertThat(configuration.getGrantTypes()).containsExactly("authorization_code"); + assertThat(configuration.containsClaim("claim-name")).isTrue(); } private static URL url(String urlString) { diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcProviderConfigurationHttpMessageConverterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcProviderConfigurationHttpMessageConverterTests.java index 78725034..39395fe2 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcProviderConfigurationHttpMessageConverterTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcProviderConfigurationHttpMessageConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,12 @@ */ package org.springframework.security.oauth2.core.oidc.http.converter; +import java.net.URL; +import java.util.Arrays; +import java.util.Map; + import org.junit.Test; + import org.springframework.core.convert.converter.Converter; import org.springframework.http.HttpStatus; import org.springframework.http.converter.HttpMessageNotReadableException; @@ -24,10 +29,6 @@ import org.springframework.mock.http.MockHttpOutputMessage; import org.springframework.mock.http.client.MockClientHttpResponse; import org.springframework.security.oauth2.core.oidc.OidcProviderConfiguration; -import java.net.URL; -import java.util.Arrays; -import java.util.Map; - import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -75,7 +76,7 @@ public class OidcProviderConfigurationHttpMessageConverterTests { assertThat(providerConfiguration.getIssuer()).isEqualTo(new URL("https://example.com/issuer1")); assertThat(providerConfiguration.getAuthorizationEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/authorize")); assertThat(providerConfiguration.getTokenEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/token")); - assertThat(providerConfiguration.getJwkSetUri()).isEqualTo(new URL("https://example.com/issuer1/oauth2/jwks")); + assertThat(providerConfiguration.getJwkSetUrl()).isEqualTo(new URL("https://example.com/issuer1/oauth2/jwks")); assertThat(providerConfiguration.getResponseTypes()).containsExactly("code"); assertThat(providerConfiguration.getSubjectTypes()).containsExactly("public"); assertThat(providerConfiguration.getIdTokenSigningAlgorithms()).containsExactly("RS256"); @@ -109,7 +110,7 @@ public class OidcProviderConfigurationHttpMessageConverterTests { assertThat(providerConfiguration.getIssuer()).isEqualTo(new URL("https://example.com/issuer1")); assertThat(providerConfiguration.getAuthorizationEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/authorize")); assertThat(providerConfiguration.getTokenEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/token")); - assertThat(providerConfiguration.getJwkSetUri()).isEqualTo(new URL("https://example.com/issuer1/oauth2/jwks")); + assertThat(providerConfiguration.getJwkSetUrl()).isEqualTo(new URL("https://example.com/issuer1/oauth2/jwks")); assertThat(providerConfiguration.getScopes()).containsExactly("openid"); assertThat(providerConfiguration.getResponseTypes()).containsExactly("code"); assertThat(providerConfiguration.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "client_credentials"); @@ -152,7 +153,7 @@ public class OidcProviderConfigurationHttpMessageConverterTests { .issuer("https://example.com/issuer1") .authorizationEndpoint("https://example.com/issuer1/oauth2/authorize") .tokenEndpoint("https://example.com/issuer1/oauth2/token") - .jwkSetUri("https://example.com/issuer1/oauth2/jwks") + .jwkSetUrl("https://example.com/issuer1/oauth2/jwks") .scope("openid") .responseType("code") .grantType("authorization_code") @@ -196,7 +197,7 @@ public class OidcProviderConfigurationHttpMessageConverterTests { .issuer("https://example.com/issuer1") .authorizationEndpoint("https://example.com/issuer1/oauth2/authorize") .tokenEndpoint("https://example.com/issuer1/oauth2/token") - .jwkSetUri("https://example.com/issuer1/oauth2/jwks") + .jwkSetUrl("https://example.com/issuer1/oauth2/jwks") .responseType("code") .subjectType("public") .idTokenSigningAlgorithm("RS256") diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/ProviderSettingsTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/ProviderSettingsTests.java index 3997f2cf..1c9ae660 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/ProviderSettingsTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/ProviderSettingsTests.java @@ -18,7 +18,7 @@ package org.springframework.security.oauth2.server.authorization.config; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Tests for {@link ProviderSettings}. @@ -78,48 +78,48 @@ public class ProviderSettingsTests { @Test public void issuerWhenNullThenThrowIllegalArgumentException() { ProviderSettings settings = new ProviderSettings(); - assertThatIllegalArgumentException() - .isThrownBy(() -> settings.issuer(null)) - .withMessage("value cannot be null"); + assertThatThrownBy(() -> settings.issuer(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("value cannot be null"); } @Test public void authorizationEndpointWhenNullThenThrowIllegalArgumentException() { ProviderSettings settings = new ProviderSettings(); - assertThatIllegalArgumentException() - .isThrownBy(() -> settings.authorizationEndpoint(null)) - .withMessage("value cannot be null"); + assertThatThrownBy(() -> settings.authorizationEndpoint(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("value cannot be null"); } @Test public void tokenEndpointWhenNullThenThrowIllegalArgumentException() { ProviderSettings settings = new ProviderSettings(); - assertThatIllegalArgumentException() - .isThrownBy(() -> settings.tokenEndpoint(null)) - .withMessage("value cannot be null"); + assertThatThrownBy(() -> settings.tokenEndpoint(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("value cannot be null"); } @Test public void tokenRevocationEndpointWhenNullThenThrowIllegalArgumentException() { ProviderSettings settings = new ProviderSettings(); - assertThatIllegalArgumentException() - .isThrownBy(() -> settings.tokenRevocationEndpoint(null)) - .withMessage("value cannot be null"); + assertThatThrownBy(() -> settings.tokenRevocationEndpoint(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("value cannot be null"); } @Test public void tokenIntrospectionEndpointWhenNullThenThrowIllegalArgumentException() { ProviderSettings settings = new ProviderSettings(); - assertThatIllegalArgumentException() - .isThrownBy(() -> settings.tokenIntrospectionEndpoint(null)) - .withMessage("value cannot be null"); + assertThatThrownBy(() -> settings.tokenIntrospectionEndpoint(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("value cannot be null"); } @Test public void jwksEndpointWhenNullThenThrowIllegalArgumentException() { ProviderSettings settings = new ProviderSettings(); - assertThatIllegalArgumentException() - .isThrownBy(() -> settings.jwkSetEndpoint(null)) - .withMessage("value cannot be null"); + assertThatThrownBy(() -> settings.jwkSetEndpoint(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("value cannot be null"); } } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilterTests.java index f27e8006..60d3172a 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilterTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerConfigurationEndpointFilterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java similarity index 53% rename from oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerConfigurationEndpointFilterTests.java rename to oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java index 32122791..1ef04f88 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerConfigurationEndpointFilterTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,17 @@ package org.springframework.security.oauth2.server.authorization.web; +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + import org.junit.Test; + import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.security.oauth2.server.authorization.config.ProviderSettings; -import javax.servlet.FilterChain; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.any; @@ -34,23 +35,23 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; /** - * Tests for {@link OAuth2AuthorizationServerConfigurationEndpointFilter}. + * Tests for {@link OAuth2AuthorizationServerMetadataEndpointFilter}. * * @author Daniel Garnier-Moiroux */ -public class OAuth2AuthorizationServerConfigurationEndpointFilterTests { +public class OAuth2AuthorizationServerMetadataEndpointFilterTests { @Test public void constructorWhenProviderSettingsNullThenThrowIllegalArgumentException() { assertThatIllegalArgumentException() - .isThrownBy(() -> new OAuth2AuthorizationServerConfigurationEndpointFilter(null)) + .isThrownBy(() -> new OAuth2AuthorizationServerMetadataEndpointFilter(null)) .withMessage("providerSettings cannot be null"); } @Test - public void doFilterWhenNotAuthorizationServerConfigurationRequestThenNotProcessed() throws Exception { - OAuth2AuthorizationServerConfigurationEndpointFilter filter = - new OAuth2AuthorizationServerConfigurationEndpointFilter(new ProviderSettings().issuer("https://example.com")); + public void doFilterWhenNotAuthorizationServerMetadataRequestThenNotProcessed() throws Exception { + OAuth2AuthorizationServerMetadataEndpointFilter filter = + new OAuth2AuthorizationServerMetadataEndpointFilter(new ProviderSettings().issuer("https://example.com")); String requestUri = "/path"; MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); @@ -64,11 +65,11 @@ public class OAuth2AuthorizationServerConfigurationEndpointFilterTests { } @Test - public void doFilterWhenAuthorizationServerConfigurationRequestPostThenNotProcessed() throws Exception { - OAuth2AuthorizationServerConfigurationEndpointFilter filter = - new OAuth2AuthorizationServerConfigurationEndpointFilter(new ProviderSettings().issuer("https://example.com")); + public void doFilterWhenAuthorizationServerMetadataRequestPostThenNotProcessed() throws Exception { + OAuth2AuthorizationServerMetadataEndpointFilter filter = + new OAuth2AuthorizationServerMetadataEndpointFilter(new ProviderSettings().issuer("https://example.com")); - String requestUri = OAuth2AuthorizationServerConfigurationEndpointFilter.DEFAULT_OAUTH2_AUTHORIZATION_SERVER_CONFIGURATION_ENDPOINT_URI; + String requestUri = OAuth2AuthorizationServerMetadataEndpointFilter.DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI; MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri); request.setServletPath(requestUri); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -80,22 +81,24 @@ public class OAuth2AuthorizationServerConfigurationEndpointFilterTests { } @Test - public void doFilterWhenAuthorizationServerConfigurationRequestThenAuthorizationServerConfigurationResponse() throws Exception { + public void doFilterWhenAuthorizationServerMetadataRequestThenMetadataResponse() throws Exception { String authorizationEndpoint = "/oauth2/v1/authorize"; String tokenEndpoint = "/oauth2/v1/token"; - String tokenRevocationEndpoint = "/oauth2/v1/revoke"; String jwkSetEndpoint = "/oauth2/v1/jwks"; + String tokenRevocationEndpoint = "/oauth2/v1/revoke"; + String tokenIntrospectionEndpoint = "/oauth2/v1/introspect"; ProviderSettings providerSettings = new ProviderSettings() .issuer("https://example.com/issuer1") .authorizationEndpoint(authorizationEndpoint) .tokenEndpoint(tokenEndpoint) + .jwkSetEndpoint(jwkSetEndpoint) .tokenRevocationEndpoint(tokenRevocationEndpoint) - .jwkSetEndpoint(jwkSetEndpoint); - OAuth2AuthorizationServerConfigurationEndpointFilter filter = - new OAuth2AuthorizationServerConfigurationEndpointFilter(providerSettings); + .tokenIntrospectionEndpoint(tokenIntrospectionEndpoint); + OAuth2AuthorizationServerMetadataEndpointFilter filter = + new OAuth2AuthorizationServerMetadataEndpointFilter(providerSettings); - String requestUri = OAuth2AuthorizationServerConfigurationEndpointFilter.DEFAULT_OAUTH2_AUTHORIZATION_SERVER_CONFIGURATION_ENDPOINT_URI; + String requestUri = OAuth2AuthorizationServerMetadataEndpointFilter.DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI; MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); request.setServletPath(requestUri); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -106,28 +109,29 @@ public class OAuth2AuthorizationServerConfigurationEndpointFilterTests { verifyNoInteractions(filterChain); assertThat(response.getContentType()).isEqualTo(MediaType.APPLICATION_JSON_VALUE); - String serverConfigurationResponse = response.getContentAsString(); - assertThat(serverConfigurationResponse).contains("\"issuer\":\"https://example.com/issuer1\""); - assertThat(serverConfigurationResponse).contains("\"authorization_endpoint\":\"https://example.com/issuer1/oauth2/v1/authorize\""); - assertThat(serverConfigurationResponse).contains("\"token_endpoint\":\"https://example.com/issuer1/oauth2/v1/token\""); - assertThat(serverConfigurationResponse).contains("\"revocation_endpoint\":\"https://example.com/issuer1/oauth2/v1/revoke\""); - assertThat(serverConfigurationResponse).contains("\"jwks_uri\":\"https://example.com/issuer1/oauth2/v1/jwks\""); - assertThat(serverConfigurationResponse).contains("\"scopes_supported\":[\"openid\"]"); - assertThat(serverConfigurationResponse).contains("\"response_types_supported\":[\"code\"]"); - assertThat(serverConfigurationResponse).contains("\"grant_types_supported\":[\"authorization_code\",\"client_credentials\",\"refresh_token\"]"); - assertThat(serverConfigurationResponse).contains("\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\"]"); - assertThat(serverConfigurationResponse).contains("\"revocation_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\"]"); - assertThat(serverConfigurationResponse).contains("\"code_challenge_methods_supported\":[\"plain\",\"S256\"]"); + String authorizationServerMetadataResponse = response.getContentAsString(); + assertThat(authorizationServerMetadataResponse).contains("\"issuer\":\"https://example.com/issuer1\""); + assertThat(authorizationServerMetadataResponse).contains("\"authorization_endpoint\":\"https://example.com/issuer1/oauth2/v1/authorize\""); + assertThat(authorizationServerMetadataResponse).contains("\"token_endpoint\":\"https://example.com/issuer1/oauth2/v1/token\""); + assertThat(authorizationServerMetadataResponse).contains("\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\"]"); + assertThat(authorizationServerMetadataResponse).contains("\"jwks_uri\":\"https://example.com/issuer1/oauth2/v1/jwks\""); + assertThat(authorizationServerMetadataResponse).contains("\"response_types_supported\":[\"code\"]"); + assertThat(authorizationServerMetadataResponse).contains("\"grant_types_supported\":[\"authorization_code\",\"client_credentials\",\"refresh_token\"]"); + assertThat(authorizationServerMetadataResponse).contains("\"revocation_endpoint\":\"https://example.com/issuer1/oauth2/v1/revoke\""); + assertThat(authorizationServerMetadataResponse).contains("\"revocation_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\"]"); + assertThat(authorizationServerMetadataResponse).contains("\"introspection_endpoint\":\"https://example.com/issuer1/oauth2/v1/introspect\""); + assertThat(authorizationServerMetadataResponse).contains("\"introspection_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\"]"); + assertThat(authorizationServerMetadataResponse).contains("\"code_challenge_methods_supported\":[\"plain\",\"S256\"]"); } @Test public void doFilterWhenProviderSettingsWithInvalidIssuerThenThrowIllegalArgumentException() { ProviderSettings providerSettings = new ProviderSettings() .issuer("https://this is an invalid URL"); - OAuth2AuthorizationServerConfigurationEndpointFilter filter = - new OAuth2AuthorizationServerConfigurationEndpointFilter(providerSettings); + OAuth2AuthorizationServerMetadataEndpointFilter filter = + new OAuth2AuthorizationServerMetadataEndpointFilter(providerSettings); - String requestUri = OAuth2AuthorizationServerConfigurationEndpointFilter.DEFAULT_OAUTH2_AUTHORIZATION_SERVER_CONFIGURATION_ENDPOINT_URI; + String requestUri = OAuth2AuthorizationServerMetadataEndpointFilter.DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI; MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); request.setServletPath(requestUri); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -138,4 +142,5 @@ public class OAuth2AuthorizationServerConfigurationEndpointFilterTests { .isThrownBy(() -> filter.doFilter(request, response, filterChain)) .withMessage("issuer must be a valid URL"); } + }