From 6a5e277a117fa8f6ea179691e7a7157a61829d4b Mon Sep 17 00:00:00 2001 From: Daniel Garnier-Moiroux Date: Fri, 16 Oct 2020 14:56:39 +0200 Subject: [PATCH] Implement OpenID Provider Configuration endpoint - See https://openid.net/specs/openid-connect-discovery-1_0.html sections 3 and 4. - We introduce here a "ProviderSettings" construct to configure the authorization server, starting with endpoint paths (e.g. token endpoint, jwk set endpont, ...) Closes gh-55 --- .../OAuth2AuthorizationServerConfigurer.java | 75 +++- .../ObjectToSetStringConverter2.java | 79 ++++ ...iderConfigurationHttpMessageConverter.java | 161 +++++++ .../core/oidc/OidcProviderConfiguration.java | 331 +++++++++++++++ .../OidcProviderMetadataClaimAccessor.java | 118 ++++++ .../oidc/OidcProviderMetadataClaimNames.java | 73 ++++ .../config/ProviderSettings.java | 155 +++++++ ...dcProviderConfigurationEndpointFilter.java | 94 +++++ .../server/authorization/OidcTests.java | 139 ++++++ .../ObjectToSetStringConverter2Test.java | 76 ++++ ...onfigurationHttpMessageConverterTests.java | 208 +++++++++ .../oidc/OidcProviderConfigurationTests.java | 398 ++++++++++++++++++ .../config/ProviderSettingsTests.java | 126 ++++++ ...viderConfigurationEndpointFilterTests.java | 112 +++++ 14 files changed, 2140 insertions(+), 5 deletions(-) create mode 100644 oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/converter/ObjectToSetStringConverter2.java create mode 100644 oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/http/converter/OidcProviderConfigurationHttpMessageConverter.java create mode 100644 oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderConfiguration.java create mode 100644 oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderMetadataClaimAccessor.java create mode 100644 oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderMetadataClaimNames.java create mode 100644 oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ProviderSettings.java create mode 100644 oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OidcProviderConfigurationEndpointFilter.java create mode 100644 oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcTests.java create mode 100644 oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/converter/ObjectToSetStringConverter2Test.java create mode 100644 oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/http/converter/OidcProviderConfigurationHttpMessageConverterTests.java create mode 100644 oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/OidcProviderConfigurationTests.java create mode 100644 oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/ProviderSettingsTests.java create mode 100644 oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OidcProviderConfigurationEndpointFilterTests.java 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 23dbef9..a9db7b7 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 @@ -34,11 +34,13 @@ import org.springframework.security.oauth2.server.authorization.authentication.O import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationProvider; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationProvider; 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.JwkSetEndpointFilter; import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter; 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.OAuth2TokenRevocationEndpointFilter; +import org.springframework.security.oauth2.server.authorization.web.OidcProviderConfigurationEndpointFilter; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint; @@ -52,6 +54,9 @@ import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; import org.springframework.util.StringUtils; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; @@ -85,6 +90,8 @@ public final class OAuth2AuthorizationServerConfigurer providerSettings(ProviderSettings providerSettings) { + Assert.notNull(providerSettings, "providerSettings cannot be null"); + this.getBuilder().setSharedObject(ProviderSettings.class, providerSettings); + return this; + } + /** * Returns a {@code List} of {@link RequestMatcher}'s for the authorization server endpoints. * * @return a {@code List} of {@link RequestMatcher}'s for the authorization server endpoints */ public List getEndpointMatchers() { + // TODO: use ProviderSettings instead return Arrays.asList(this.authorizationEndpointMatcher, this.tokenEndpointMatcher, - this.tokenRevocationEndpointMatcher, this.jwkSetEndpointMatcher); + this.tokenRevocationEndpointMatcher, this.jwkSetEndpointMatcher, this.oidcProviderConfigurationEndpointMatcher); } @Override public void init(B builder) { + ProviderSettings providerSettings = getProviderSettings(builder); + validateProviderSettings(providerSettings); OAuth2ClientAuthenticationProvider clientAuthenticationProvider = new OAuth2ClientAuthenticationProvider( getRegisteredClientRepository(builder), @@ -186,7 +208,14 @@ public final class OAuth2AuthorizationServerConfigurer> CryptoKeySource getKeySourceBean(B builder) { return builder.getSharedObject(ApplicationContext.class).getBean(CryptoKeySource.class); } + + private static > ProviderSettings getProviderSettings(B builder) { + ProviderSettings providerSettings = builder.getSharedObject(ProviderSettings.class); + if (providerSettings == null) { + providerSettings = getProviderSettingsBean(builder); + if (providerSettings == null) { + providerSettings = new ProviderSettings(); + } + builder.setSharedObject(ProviderSettings.class, providerSettings); + } + return providerSettings; + } + + private static > ProviderSettings getProviderSettingsBean(B builder) { + Map providerSettingsMap = BeanFactoryUtils.beansOfTypeIncludingAncestors( + builder.getSharedObject(ApplicationContext.class), ProviderSettings.class); + if (providerSettingsMap.size() > 1) { + throw new NoUniqueBeanDefinitionException(ProviderSettings.class, providerSettingsMap.size(), + "Expected single matching bean of type '" + ProviderSettings.class.getName() + "' but found " + + providerSettingsMap.size() + ": " + StringUtils.collectionToCommaDelimitedString(providerSettingsMap.keySet())); + } + return (!providerSettingsMap.isEmpty() ? providerSettingsMap.values().iterator().next() : null); + } + + private void validateProviderSettings(ProviderSettings providerSettings) { + if (providerSettings.issuer() != null) { + try { + new URI(providerSettings.issuer()).toURL(); + } catch (MalformedURLException | URISyntaxException e) { + throw new IllegalArgumentException("issuer must be a valid URL"); + } + } + } } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/converter/ObjectToSetStringConverter2.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/converter/ObjectToSetStringConverter2.java new file mode 100644 index 0000000..0d8347f --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/converter/ObjectToSetStringConverter2.java @@ -0,0 +1,79 @@ +/* + * 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.converter; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.core.convert.converter.GenericConverter; +import org.springframework.util.ClassUtils; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * TODO + * This class is temporary and will be removed after upgrading to Spring Security 5.5.0 GA. + * + * @author Daniel Garnier-Moiroux + * @since 0.1.0 + * @see Issue gh-9146 + */ +final public class ObjectToSetStringConverter2 implements ConditionalGenericConverter { + + @Override + public Set getConvertibleTypes() { + Set convertibleTypes = new LinkedHashSet<>(); + convertibleTypes.add(new GenericConverter.ConvertiblePair(Object.class, Set.class)); + return convertibleTypes; + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + if (targetType.getElementTypeDescriptor() == null + || targetType.getElementTypeDescriptor().getType().equals(String.class) || sourceType == null + || ClassUtils.isAssignable(sourceType.getType(), targetType.getElementTypeDescriptor().getType())) { + return true; + } + return false; + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + if (source instanceof Set) { + Set sourceList = (Set) source; + for (Object entry: sourceList) { + if (entry instanceof String) { + return source; + } + } + } + if (source instanceof Collection) { + Collection results = new LinkedHashSet<>(); + for (Object object : ((Collection) source)) { + if (object != null) { + results.add(object.toString()); + } + } + return results; + } + return Collections.singleton(source.toString()); + } +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/http/converter/OidcProviderConfigurationHttpMessageConverter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/http/converter/OidcProviderConfigurationHttpMessageConverter.java new file mode 100644 index 0000000..20a7ea8 --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/http/converter/OidcProviderConfigurationHttpMessageConverter.java @@ -0,0 +1,161 @@ +/* + * 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.converter.ClaimConversionService; +import org.springframework.security.oauth2.core.converter.ClaimTypeConverter; +import org.springframework.security.oauth2.core.converter.ObjectToSetStringConverter2; +import org.springframework.security.oauth2.core.oidc.OidcProviderConfiguration; +import org.springframework.security.oauth2.core.oidc.OidcProviderMetadataClaimNames; +import org.springframework.util.Assert; + +import java.io.IOException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + + +/** + * A {@link HttpMessageConverter} for an {@link OidcProviderConfiguration OpenID Provider Configuration Metadata}. + * + * @author Daniel Garnier-Moiroux + * @since 0.1.0 + * @see AbstractHttpMessageConverter + * @see OidcProviderConfiguration + */ +public class OidcProviderConfigurationHttpMessageConverter + extends AbstractHttpMessageConverter { + private static final ParameterizedTypeReference> STRING_OBJECT_MAP = + new ParameterizedTypeReference>() { + }; + + private final GenericHttpMessageConverter jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter(); + + private Converter, OidcProviderConfiguration> providerConfigurationConverter = new OidcProviderConfigurationConverter(); + private Converter> providerConfigurationParametersConverter = OidcProviderConfiguration::getClaims; + + public OidcProviderConfigurationHttpMessageConverter() { + super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json")); + } + + @Override + protected boolean supports(Class clazz) { + return OidcProviderConfiguration.class.isAssignableFrom(clazz); + } + + @Override + @SuppressWarnings("unchecked") + protected OidcProviderConfiguration readInternal(Class clazz, HttpInputMessage inputMessage) + throws HttpMessageNotReadableException { + try { + Map providerConfigurationParameters = (Map) this.jsonMessageConverter.read(STRING_OBJECT_MAP.getType(), null, inputMessage); + return this.providerConfigurationConverter.convert(providerConfigurationParameters); + } catch (Exception ex) { + throw new HttpMessageNotReadableException( + "An error occurred reading the OpenID Provider Configuration: " + ex.getMessage(), ex, inputMessage); + } + } + + @Override + protected void writeInternal(OidcProviderConfiguration providerConfiguration, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { + try { + Map providerConfigurationResponseParameters = + this.providerConfigurationParametersConverter.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 OpenID Provider Configuration: " + ex.getMessage(), ex); + } + } + + /** + * Sets the {@link Converter} used for converting the {@link OidcProviderConfiguration} to a + * {@code Map} representation of the OpenID Provider Configuration. + * + * @param providerConfigurationParametersConverter the {@link Converter} used for converting to a + * {@code Map} representation of the OpenID Provider Configuration + */ + public final void setProviderConfigurationParametersConverter( + Converter> providerConfigurationParametersConverter) { + Assert.notNull(providerConfigurationParametersConverter, "providerConfigurationParametersConverter cannot be null"); + this.providerConfigurationParametersConverter = providerConfigurationParametersConverter; + } + + /** + * Sets the {@link Converter} used for converting the OpenID Provider Configuration parameters + * to an {@link OidcProviderConfiguration}. + * + * @param providerConfigurationConverter the {@link Converter} used for converting to an + * {@link OidcProviderConfiguration} + */ + public final void setProviderConfigurationConverter(Converter, OidcProviderConfiguration> providerConfigurationConverter) { + Assert.notNull(providerConfigurationConverter, "providerConfigurationConverter cannot be null"); + this.providerConfigurationConverter = providerConfigurationConverter; + } + + private static final class OidcProviderConfigurationConverter implements Converter, OidcProviderConfiguration> { + 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; + + OidcProviderConfigurationConverter() { + CLAIM_CONVERSION_SERVICE.addConverter(new ObjectToSetStringConverter2()); + Map> claimNameToConverter = new HashMap<>(); + Converter setStringConverter = getConverter(TypeDescriptor.collection(Set.class, STRING_TYPE_DESCRIPTOR)); + Converter urlConverter = getConverter(URL_TYPE_DESCRIPTOR); + + claimNameToConverter.put(OidcProviderMetadataClaimNames.ISSUER, urlConverter); + claimNameToConverter.put(OidcProviderMetadataClaimNames.AUTHORIZATION_ENDPOINT, urlConverter); + claimNameToConverter.put(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT, urlConverter); + claimNameToConverter.put(OidcProviderMetadataClaimNames.JWKS_URI, urlConverter); + claimNameToConverter.put(OidcProviderMetadataClaimNames.GRANT_TYPES_SUPPORTED, setStringConverter); + claimNameToConverter.put(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, setStringConverter); + claimNameToConverter.put(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED, setStringConverter); + claimNameToConverter.put(OidcProviderMetadataClaimNames.SCOPES_SUPPORTED, setStringConverter); + claimNameToConverter.put(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, setStringConverter); + this.claimTypeConverter = new ClaimTypeConverter(claimNameToConverter); + } + + @Override + public OidcProviderConfiguration convert(Map source) { + Map parsedClaims = this.claimTypeConverter.convert(source); + return OidcProviderConfiguration.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 new file mode 100644 index 0000000..28de8b1 --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderConfiguration.java @@ -0,0 +1,331 @@ +/* + * 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.oidc; + +import org.springframework.security.oauth2.server.authorization.Version; +import org.springframework.util.Assert; + +import java.io.Serializable; +import java.net.URI; +import java.net.URL; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; + +/** + * A representation of an OpenID Provider Configuration Response, + * which is returned from an Issuer's Discovery Endpoint, + * and contains a set of claims about the OpenID Provider's configuration. + * The claims are defined by the OpenID Connect Discovery 1.0 specification. + * + * @author Daniel Garnier-Moiroux + * @since 0.1.0 + * @see OidcProviderMetadataClaimAccessor + * @see 4.2. OpenID Provider Configuration Response + */ +public class OidcProviderConfiguration implements OidcProviderMetadataClaimAccessor, Serializable { + private static final long serialVersionUID = Version.SERIAL_VERSION_UID; + + private final Map claims; + + private OidcProviderConfiguration(Map claims) { + Assert.notEmpty(claims, "claims cannot be empty"); + this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims)); + } + + /** + * Returns the OpenID Provider Configuration metadata. + * + * @return a {@code Map} of the metadata values + */ + @Override + public Map getClaims() { + return this.claims; + } + + /** + * Constructs a new empty {@link Builder}. + * + * @return the {@link Builder} + */ + public static Builder withClaims() { + return new Builder(); + } + + + /** + * Constructs a new {@link Builder} with the provided claims. + * + * @param claims the claims to initialize the builder + */ + public static Builder withClaims(Map claims) { + Assert.notEmpty(claims, "claims cannot be empty"); + return new Builder() + .claims(c -> c.putAll(claims)); + } + + /** + * Helps configure an {@link OidcProviderConfiguration} + * + * @author Daniel Garnier-Moiroux + * @since 0.1.0 + * @see OpenID Connect Discovery 1.0 + * for required claims + */ + public static final class Builder { + private final Map claims = new LinkedHashMap<>(); + + private Builder() { + } + + /** + * Use this {@code issuer} in the resulting {@link OidcProviderConfiguration}, REQUIRED. + * + * @param issuer the issuer URI + * @return the {@link Builder} for further configuration + */ + public Builder issuer(String issuer) { + return claim(OidcProviderMetadataClaimNames.ISSUER, issuer); + } + + /** + * Use this {@code authorization_endpoint} in the resulting {@link OidcProviderConfiguration}, REQUIRED. + * + * @param authorizationEndpoint the URL of the OpenID Provider's OAuth 2.0 Authorization Endpoint + * @return the {@link Builder} for further configuration + */ + public Builder authorizationEndpoint(String authorizationEndpoint) { + return claim(OidcProviderMetadataClaimNames.AUTHORIZATION_ENDPOINT, authorizationEndpoint); + } + + /** + * Use this {@code token_endpoint} in the resulting {@link OidcProviderConfiguration}, REQUIRED. + * + * @param tokenEndpoint the URL of the OpenID Provider's OAuth 2.0 Token Endpoint + * @return the {@link Builder} for further configuration + */ + public Builder tokenEndpoint(String tokenEndpoint) { + return claim(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT, tokenEndpoint); + } + + /** + * Use this {@code jwks_uri} in the resulting {@link OidcProviderConfiguration}, REQUIRED. + * + * @param jwksUri the URL of the OpenID Provider's JSON Web Key Set document + * @return the {@link Builder} for further configuration + */ + public Builder jwksUri(String jwksUri) { + return claim(OidcProviderMetadataClaimNames.JWKS_URI, jwksUri); + } + + /** + * Add this Response Type to the collection of {@code response_types_supported} in the resulting + * {@link OidcProviderConfiguration}, REQUIRED. + * + * @param responseType the OAuth 2.0 {@code response_type} values that the OpenID Provider supports + * @return the {@link Builder} for further configuration + */ + public Builder responseType(String responseType) { + addClaimToClaimSet(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, responseType); + return this; + } + + /** + * 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 Builder} for further configuration + */ + public Builder responseTypes(Consumer> responseTypesConsumer) { + applyToClaim(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, responseTypesConsumer); + return this; + } + + /** + * Add this Subject Type to the collection of {@code subject_types_supported} in the resulting + * {@link OidcProviderConfiguration}, REQUIRED. + * + * @param subjectType the Subject Identifiers that the OpenID Provider supports + * @return the {@link Builder} for further configuration + */ + public Builder subjectType(String subjectType) { + addClaimToClaimSet(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED, subjectType); + return this; + } + + /** + * A {@code Consumer} of the Subject Types(s) allowing the ability to add, replace, or remove. + * + * @param subjectTypesConsumer a {@code Consumer} of the Subject Types(s) + * @return the {@link Builder} for further configuration + */ + public Builder subjectTypes(Consumer> subjectTypesConsumer) { + applyToClaim(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED, subjectTypesConsumer); + return this; + } + + /** + * Add this Scope to the collection of {@code scopes_supported} in the resulting + * {@link OidcProviderConfiguration}, RECOMMENDED. + * + * @param scope the OAuth 2.0 {@code scopes} values that the OpenID Provider supports + * @return the {@link Builder} for further configuration + */ + public Builder scope(String scope) { + addClaimToClaimSet(OidcProviderMetadataClaimNames.SCOPES_SUPPORTED, scope); + return this; + } + + /** + * 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 Builder} for further configuration + */ + public Builder scopes(Consumer> scopesConsumer) { + applyToClaim(OidcProviderMetadataClaimNames.SCOPES_SUPPORTED, scopesConsumer); + return this; + } + + /** + * Add this Grant Type to the collection of {@code grant_types_supported} in the resulting + * {@link OidcProviderConfiguration}, OPTIONAL. + * + * @param grantType the OAuth 2.0 {@code grant_type} values that the OpenID Provider supports + * @return the {@link Builder} for further configuration + */ + public Builder grantType(String grantType) { + addClaimToClaimSet(OidcProviderMetadataClaimNames.GRANT_TYPES_SUPPORTED, grantType); + return this; + } + + /** + * 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 Builder} for further configuration + */ + public Builder grantTypes(Consumer> grantTypesConsumer) { + applyToClaim(OidcProviderMetadataClaimNames.GRANT_TYPES_SUPPORTED, grantTypesConsumer); + return this; + } + + /** + * Add this Authentication Method to the collection of {@code token_endpoint_auth_methods_supported} + * in the resulting {@link OidcProviderConfiguration}, OPTIONAL. + * + * @param authenticationMethod the OAuth 2.0 Authentication Method supported by the Token endpoint + * @return the {@link Builder} for further configuration + */ + public Builder tokenEndpointAuthenticationMethod(String authenticationMethod) { + addClaimToClaimSet(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, authenticationMethod); + return this; + } + + /** + * 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 Builder} for further configuration + */ + public Builder tokenEndpointAuthenticationMethods(Consumer> authenticationMethodsConsumer) { + applyToClaim(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, authenticationMethodsConsumer); + 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 the consumer + * @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}. The following claims are REQUIRED: + * - issuer + * - authorization_endpoint + * - token_endpoint + * - jwks_uri + * - response_types_supported + * - subject_types_supported + * + * @return The constructed {@link OidcProviderConfiguration} + */ + public OidcProviderConfiguration build() { + validateClaims(); + return new OidcProviderConfiguration(this.claims); + } + + private void validateClaims() { + Assert.notNull(this.claims.get(OidcProviderMetadataClaimNames.ISSUER), "issuer cannot be null"); + validateURL(this.claims.get(OidcProviderMetadataClaimNames.ISSUER), "issuer must be a valid URL"); + Assert.notNull(this.claims.get(OidcProviderMetadataClaimNames.AUTHORIZATION_ENDPOINT), "authorizationEndpoint cannot be null"); + validateURL(this.claims.get(OidcProviderMetadataClaimNames.AUTHORIZATION_ENDPOINT), "authorizationEndpoint must be a valid URL"); + Assert.notNull(this.claims.get(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT), "tokenEndpoint cannot be null"); + validateURL(this.claims.get(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT), "tokenEndpoint must be a valid URL"); + Assert.notNull(this.claims.get(OidcProviderMetadataClaimNames.JWKS_URI), "jwkSetUri cannot be null"); + validateURL(this.claims.get(OidcProviderMetadataClaimNames.JWKS_URI), "jwkSetUri must be a valid URL"); + Assert.notEmpty((Set) this.claims.get(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED), "subjectTypes cannot be empty"); + Assert.notEmpty((Set) this.claims.get(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED), "responseTypes cannot be empty"); + } + + private void validateURL(Object url, String errorMessage) { + if (url.getClass().isAssignableFrom(URL.class)) return; + + try { + new URI(url.toString()).toURL(); + } catch (Exception e) { + throw new IllegalArgumentException(errorMessage); + } + + } + + @SuppressWarnings("unchecked") + private void addClaimToClaimSet(String name, String value) { + this.claims.putIfAbsent(name, new LinkedHashSet()); + ((Set) this.claims.get(name)).add(value); + } + + @SuppressWarnings("unchecked") + private void applyToClaim(String name, Consumer> consumer) { + this.claims.putIfAbsent(name, new LinkedHashSet()); + Set values = (Set) this.claims.get(name); + consumer.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 new file mode 100644 index 0000000..9287bab --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderMetadataClaimAccessor.java @@ -0,0 +1,118 @@ +/* + * 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.oidc; + + +import org.springframework.security.oauth2.core.ClaimAccessor; + +import java.net.URL; +import java.util.List; + +/** + * A {@link ClaimAccessor} for the "claims" that can be returned + * in the OpenID Provider Configuration Response. + * + * @author Daniel Garnier-Moiroux + * @since 0.1.0 + * @see ClaimAccessor + * @see OidcProviderMetadataClaimNames + * @see OidcProviderConfiguration + * @see 3. OpenID Provider Metadata + */ +public interface OidcProviderMetadataClaimAccessor extends ClaimAccessor { + + /** + * Returns the URL the OpenID Provider asserts as its Issuer Identifier {@code (issuer)}. + * + * @return the URL the OpenID Provider asserts as its Issuer Identifier + */ + default URL getIssuer() { + return this.getClaimAsURL(OidcProviderMetadataClaimNames.ISSUER); + } + + /** + * Returns the URL of the OAuth 2.0 Authorization Endpoint {@code (authorization_endpoint)}. + * + * @return the URL of the OAuth 2.0 Authorization Endpoint + */ + default URL getAuthorizationEndpoint() { + return this.getClaimAsURL(OidcProviderMetadataClaimNames.AUTHORIZATION_ENDPOINT); + } + + /** + * Returns the URL of the OAuth 2.0 Token Endpoint {@code (token_endpoint)}. + * + * @return the URL of the OAuth 2.0 Token Endpoint + */ + default URL getTokenEndpoint() { + return this.getClaimAsURL(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT); + } + + /** + * Returns the client authentication methods supported by the OAuth 2.0 Token Endpoint {@code (token_endpoint_auth_methods_supported)}. + * + * @return the client authentication methods supported by the OAuth 2.0 Token Endpoint + */ + default List getTokenEndpointAuthenticationMethods() { + return this.getClaimAsStringList(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED); + } + + /** + * Returns the URL of the JSON Web Key Set {@code (jwks_uri)}. + * + * @return the URL of the JSON Web Key Set + */ + default URL getJwksUri() { + return this.getClaimAsURL(OidcProviderMetadataClaimNames.JWKS_URI); + } + + /** + * Returns the OAuth 2.0 {@code response_type} values supported {@code (response_types_supported)}. + * + * @return the OAuth 2.0 {@code response_type} values supported + */ + default List getResponseTypes() { + return this.getClaimAsStringList(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED); + } + + /** + * Returns the OAuth 2.0 {@code grant_type} values supported {@code (grant_types_supported)}. + * + * @return the OAuth 2.0 {@code grant_type} values supported + */ + default List getGrantTypes() { + return this.getClaimAsStringList(OidcProviderMetadataClaimNames.GRANT_TYPES_SUPPORTED); + } + + /** + * Returns the Subject Identifier types supported {@code (subject_types_supported)}. + * + * @return the Subject Identifier types supported + */ + default List getSubjectTypes() { + return this.getClaimAsStringList(OidcProviderMetadataClaimNames.SUBJECT_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(OidcProviderMetadataClaimNames.SCOPES_SUPPORTED); + } + +} 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 new file mode 100644 index 0000000..03b6545 --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderMetadataClaimNames.java @@ -0,0 +1,73 @@ +/* + * 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.oidc; + +/** + * The names of the "claims" defined by the OpenID Connect Discovery 1.0 that can be returned + * in the OpenID Provider Configuration Response. + * + * @author Daniel Garnier-Moiroux + * @since 0.1.0 + * @see 3. OpenID Provider Metadata + */ +public interface OidcProviderMetadataClaimNames { + + /** + * {@code issuer} - the URL the OpenID Provider asserts as its Issuer Identifier + */ + String ISSUER = "issuer"; + + /** + * {@code authorization_endpoint} - the URL of the OAuth 2.0 Authorization Endpoint + */ + String AUTHORIZATION_ENDPOINT = "authorization_endpoint"; + + /** + * {@code token_endpoint} - the URL of the OAuth 2.0 Token Endpoint + */ + String TOKEN_ENDPOINT = "token_endpoint"; + + /** + * {@code token_endpoint_auth_methods_supported} - the client authentication methods supported by the OAuth 2.0 Token Endpoint + */ + String TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED = "token_endpoint_auth_methods_supported"; + + /** + * {@code jwks_uri} - the URL of the JSON Web Key Set + */ + String JWKS_URI = "jwks_uri"; + + /** + * {@code response_types_supported} - the OAuth 2.0 {@code response_type} values supported + */ + String RESPONSE_TYPES_SUPPORTED = "response_types_supported"; + + /** + * {@code grant_types_supported} - the OAuth 2.0 {@code grant_type} values supported + */ + String GRANT_TYPES_SUPPORTED = "grant_types_supported"; + + /** + * {@code subject_types_supported} - the Subject Identifier types supported + */ + String SUBJECT_TYPES_SUPPORTED = "subject_types_supported"; + + /** + * {@code scopes_supported} - the OAuth 2.0 {@code scope} values supported + */ + String SCOPES_SUPPORTED = "scopes_supported"; + +} 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 new file mode 100644 index 0000000..7bf2e76 --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ProviderSettings.java @@ -0,0 +1,155 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.server.authorization.config; + + +import org.springframework.util.Assert; + +import java.util.HashMap; +import java.util.Map; + +/** + * A facility for OpenID Connect Provider Configuration settings. + * + * @author Daniel Garnier-Moiroux + * @since 0.1.0 + * @see Settings + * @see OpenID Connect Discovery 1.0 + */ +public class ProviderSettings extends Settings { + private static final String PROVIDER_SETTING_BASE = "setting.provider."; + public static final String ISSUER = PROVIDER_SETTING_BASE.concat("issuer"); + public static final String AUTHORIZATION_ENDPOINT = PROVIDER_SETTING_BASE.concat("authorization-endpoint"); + public static final String TOKEN_ENDPOINT = PROVIDER_SETTING_BASE.concat("token-endpoint"); + public static final String JWK_SET_ENDPOINT = PROVIDER_SETTING_BASE.concat("jwk-set-endpoint"); + public static final String TOKEN_REVOCATION_ENDPOINT = PROVIDER_SETTING_BASE.concat("token-revocation-endpoint"); + + /** + * Constructs a {@code ProviderSettings}. + */ + public ProviderSettings() { + super(defaultSettings()); + } + + /** + * Returns the URL for the OpenID Issuer. + * + * @return the URL for the OpenID Issuer + */ + public String issuer() { + return setting(ISSUER); + } + + /** + * Sets the URL the Provider uses as its Issuer Identity. + * + * @param issuer the URL the Provider uses as its Issuer Identity. + * @return the {@link ProviderSettings} for further configuration + */ + public ProviderSettings issuer(String issuer) { + Assert.notNull(issuer, "issuer cannot be null"); + return setting(ISSUER, issuer); + } + + /** + * Returns the provider's OAuth 2.0 Authorization endpoint. The default is {@code /oauth2/authorize}. + * + * @return the Authorization endpoint + */ + public String authorizationEndpoint() { + return setting(AUTHORIZATION_ENDPOINT); + } + + /** + * Sets the provider's OAuth 2.0 Authorization endpoint. + * + * @param authorizationEndpoint the Authorization endpoint + * @return the {@link ProviderSettings} for further configuration + */ + public ProviderSettings authorizationEndpoint(String authorizationEndpoint) { + Assert.hasText(authorizationEndpoint, "authorizationEndpoint cannot be empty"); + return setting(AUTHORIZATION_ENDPOINT, authorizationEndpoint); + } + + /** + * Returns the provider's OAuth 2.0 Token endpoint. The default is {@code /oauth2/token}. + * + * @return the Token endpoint + */ + public String tokenEndpoint() { + return setting(TOKEN_ENDPOINT); + } + + /** + * Sets the provider's OAuth 2.0 Token endpoint. + * + * @param tokenEndpoint the Token endpoint + * @return the {@link ProviderSettings} for further configuration + */ + public ProviderSettings tokenEndpoint(String tokenEndpoint) { + Assert.hasText(tokenEndpoint, "tokenEndpoint cannot be empty"); + return setting(TOKEN_ENDPOINT, tokenEndpoint); + } + + /** + * Returns the provider's JWK Set endpoint. The default is {@code /oauth2/jwks}. + * + * @return the JWK Set endpoint + */ + public String jwkSetEndpoint() { + return setting(JWK_SET_ENDPOINT); + } + + /** + * Sets the provider's OAuth 2.0 JWK Set endpoint. + * + * @param jwkSetEndpoint the JWK Set endpoint + * @return the {@link ProviderSettings} for further configuration + */ + public ProviderSettings jwkSetEndpoint(String jwkSetEndpoint) { + Assert.hasText(jwkSetEndpoint, "jwkSetEndpoint cannot be empty"); + return setting(JWK_SET_ENDPOINT, jwkSetEndpoint); + } + + /** + * Returns the provider's Token Revocation endpoint. The default is {@code /oauth2/revoke}. + * + * @return the Token Revocation endpoint + */ + public String tokenRevocationEndpoint() { + return setting(TOKEN_REVOCATION_ENDPOINT); + } + + /** + * Sets the provider's OAuth 2.0 Token Revocation endpoint. + * + * @param tokenRevocationEndpoint the Token Revocation endpoint + * @return the {@link ProviderSettings} for further configuration + */ + public ProviderSettings tokenRevocationEndpoint(String tokenRevocationEndpoint) { + Assert.hasText(tokenRevocationEndpoint, "tokenRevocationEndpoint cannot be empty"); + return setting(TOKEN_REVOCATION_ENDPOINT, tokenRevocationEndpoint); + } + + protected static Map defaultSettings() { + Map settings = new HashMap<>(); + settings.put(AUTHORIZATION_ENDPOINT, "/oauth2/authorize"); + settings.put(TOKEN_ENDPOINT, "/oauth2/token"); + settings.put(JWK_SET_ENDPOINT, "/oauth2/jwks"); + settings.put(TOKEN_REVOCATION_ENDPOINT, "/oauth2/revoke"); + return settings; + } +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OidcProviderConfigurationEndpointFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OidcProviderConfigurationEndpointFilter.java new file mode 100644 index 0000000..f9d6bd4 --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OidcProviderConfigurationEndpointFilter.java @@ -0,0 +1,94 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.server.authorization.web; + +import org.springframework.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.endpoint.OAuth2AuthorizationResponseType; +import org.springframework.security.oauth2.core.http.converter.OidcProviderConfigurationHttpMessageConverter; +import org.springframework.security.oauth2.core.oidc.OidcProviderConfiguration; +import org.springframework.security.oauth2.core.oidc.OidcScopes; +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; +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 OpenID Provider Configuration Request. + * + * @author Daniel Garnier-Moiroux + * @since 0.1.0 + * @see ProviderSettings + * @see OpenID Connect Discovery 1.0 + */ +public class OidcProviderConfigurationEndpointFilter extends OncePerRequestFilter { + /** + * The default endpoint {@code URI} for OpenID Provider Configuration requests. + */ + public static final String DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI = "/.well-known/openid-configuration"; + + private final RequestMatcher requestMatcher; + private final ProviderSettings providerSettings; + private final OidcProviderConfigurationHttpMessageConverter providerConfigurationHttpMessageConverter = new OidcProviderConfigurationHttpMessageConverter(); + + public OidcProviderConfigurationEndpointFilter(ProviderSettings providerSettings) { + Assert.notNull(providerSettings, "providerSettings cannot be null"); + this.requestMatcher = new AntPathRequestMatcher( + DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI, + HttpMethod.GET.name() + ); + this.providerSettings = providerSettings; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + if (!this.requestMatcher.matches(request)) { + filterChain.doFilter(request, response); + return; + } + + OidcProviderConfiguration providerConfiguration = OidcProviderConfiguration.withClaims() + .issuer(this.providerSettings.issuer()) + .authorizationEndpoint(asUrl(this.providerSettings.issuer(), this.providerSettings.authorizationEndpoint())) + .tokenEndpoint(asUrl(this.providerSettings.issuer(), this.providerSettings.tokenEndpoint())) + .jwksUri(asUrl(this.providerSettings.issuer(), this.providerSettings.jwkSetEndpoint())) + .subjectType("public") + .responseType(OAuth2AuthorizationResponseType.CODE.getValue()) + .scope(OidcScopes.OPENID) + .grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()) + .grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) + .tokenEndpointAuthenticationMethod("client_secret_basic") // TODO: move this ClientAuthenticationMethod + .build(); + + ServletServerHttpResponse resp = new ServletServerHttpResponse(response); + this.providerConfigurationHttpMessageConverter.write(providerConfiguration, MediaType.APPLICATION_JSON, resp); + } + + private String asUrl(String issuer, String endpoint) { + return UriComponentsBuilder.fromUriString(issuer).path(endpoint).build().toUriString(); + } +} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcTests.java new file mode 100644 index 0000000..db800a7 --- /dev/null +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcTests.java @@ -0,0 +1,139 @@ +/* + * 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.config.annotation.web.configurers.oauth2.server.authorization; + +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; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.crypto.key.CryptoKeySource; +import org.springframework.security.crypto.key.StaticKeyGeneratingCryptoKeySource; +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.OidcProviderConfigurationEndpointFilter; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Integration tests for the OpenID Connect. + * + * @author Daniel Garnier-Moiroux + */ +public class OidcTests { + private static final String issuerUrl = "https://example.com/issuer1"; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Autowired + private MockMvc mvc; + + @Test + public void requestWhenIssuerSetAndOpenIDProviderConfigurationRequestThenReturnProviderConfigurationResponse() throws Exception { + this.spring.register(AuthorizationServerConfigurationWithIssuer.class).autowire(); + + this.mvc.perform(MockMvcRequestBuilders.get(OidcProviderConfigurationEndpointFilter.DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI)) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("issuer").value(issuerUrl)) + .andReturn(); + } + + @Test + public void requestWhenIssuerNotSetAndOpenIDProviderConfigurationRequestThenRedirectsToLogin() throws Exception { + this.spring.register(AuthorizationServerConfiguration.class).autowire(); + + MvcResult mvcResult = this.mvc.perform(get(OidcProviderConfigurationEndpointFilter.DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI)) + .andExpect(status().is3xxRedirection()) + .andReturn(); + assertThat(mvcResult.getResponse().getRedirectedUrl()).endsWith("/login"); + } + + @Test + public void requestWhenIssuerNotValidUrlThenThrowException() { + assertThatThrownBy( + () -> this.spring.register(AuthorizationServerConfigurationWithInvalidUrlIssuer.class).autowire() + ); + } + + @Test + public void requestWhenIssuerNotValidUriThenThrowException() { + assertThatThrownBy( + () -> this.spring.register(AuthorizationServerConfigurationWithInvalidUriIssuer.class).autowire() + ); + } + + @EnableWebSecurity + @Import(OAuth2AuthorizationServerConfiguration.class) + static class AuthorizationServerConfiguration { + + @Bean + RegisteredClientRepository registeredClientRepository() { + return mock(RegisteredClientRepository.class); + } + + @Bean + CryptoKeySource keySource() { + return new StaticKeyGeneratingCryptoKeySource(); + } + + @Bean + ProviderSettings providerSettings() { + return new ProviderSettings(); + } + } + + @EnableWebSecurity + @Import(OAuth2AuthorizationServerConfiguration.class) + static class AuthorizationServerConfigurationWithIssuer extends AuthorizationServerConfiguration { + @Bean + @Override + ProviderSettings providerSettings() { + return new ProviderSettings().issuer(issuerUrl); + } + } + + @EnableWebSecurity + @Import(OAuth2AuthorizationServerConfiguration.class) + static class AuthorizationServerConfigurationWithInvalidUrlIssuer extends AuthorizationServerConfiguration { + @Bean + @Override + ProviderSettings providerSettings() { + return new ProviderSettings().issuer("urn:example"); + } + } + + @EnableWebSecurity + @Import(OAuth2AuthorizationServerConfiguration.class) + static class AuthorizationServerConfigurationWithInvalidUriIssuer extends AuthorizationServerConfiguration { + @Bean + @Override + ProviderSettings providerSettings() { + return new ProviderSettings().issuer("https://not a valid uri"); + } + } +} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/converter/ObjectToSetStringConverter2Test.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/converter/ObjectToSetStringConverter2Test.java new file mode 100644 index 0000000..ce10dc4 --- /dev/null +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/converter/ObjectToSetStringConverter2Test.java @@ -0,0 +1,76 @@ +/* + * 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.converter; + +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +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. + * These tests will probably be folded into tests for {@link ClaimConversionService}. + * + * Tests for {@link ObjectToSetStringConverter2}. + * + * @author Daniel Garnier-Moiroux + */ +public class ObjectToSetStringConverter2Test { + @Test + @SuppressWarnings("unchecked") + public void convertFromNullThenReturnNull() { + ObjectToSetStringConverter2 converter = new ObjectToSetStringConverter2(); + Set result = (Set) converter.convert(null, null, null); + assertThat(result).isNull(); + } + + @Test + @SuppressWarnings("unchecked") + public void convertFromStringThenReturnSet() { + ObjectToSetStringConverter2 converter = new ObjectToSetStringConverter2(); + Set result = (Set) converter.convert("Hello", null, null); + assertThat(result).containsExactly("Hello"); + } + + @Test + @SuppressWarnings("unchecked") + public void convertFromSetThenReturnSet() { + ObjectToSetStringConverter2 converter = new ObjectToSetStringConverter2(); + Set result = (Set) converter.convert(new HashSet<>(Arrays.asList("Hello", "world")), null, null); + assertThat(result).containsExactlyInAnyOrder("Hello", "world"); + } + + @Test + @SuppressWarnings("unchecked") + public void convertFromCollectionThenReturnSet() { + ObjectToSetStringConverter2 converter = new ObjectToSetStringConverter2(); + Set result = (Set) converter.convert(Arrays.asList("Hello", "world"), null, null); + assertThat(result).containsExactlyInAnyOrder("Hello", "world"); + } + + @Test + @SuppressWarnings("unchecked") + public void convertFromEmptyCollectionThenReturnEmptySet() { + ObjectToSetStringConverter2 converter = new ObjectToSetStringConverter2(); + Set result = (Set) converter.convert(Collections.emptyList(), null, null); + assertThat(result).isEmpty(); + } +} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/http/converter/OidcProviderConfigurationHttpMessageConverterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/http/converter/OidcProviderConfigurationHttpMessageConverterTests.java new file mode 100644 index 0000000..df081ba --- /dev/null +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/http/converter/OidcProviderConfigurationHttpMessageConverterTests.java @@ -0,0 +1,208 @@ +/* + * 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.oidc.OidcProviderConfiguration; + +import java.net.MalformedURLException; +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; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link OidcProviderConfigurationHttpMessageConverter} + * + * @author Daniel Garnier-Moiroux + */ +public class OidcProviderConfigurationHttpMessageConverterTests { + private final OidcProviderConfigurationHttpMessageConverter messageConverter = new OidcProviderConfigurationHttpMessageConverter(); + + @Test + public void supportsWhenOidcProviderConfigurationThenTrue() { + assertThat(this.messageConverter.supports(OidcProviderConfiguration.class)).isTrue(); + } + + @Test + public void setProviderConfigurationParametersConverterWhenConverterIsNullThenThrowIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.messageConverter.setProviderConfigurationParametersConverter(null)); + } + + @Test + public void setProviderConfigurationConverterWhenConverterIsNullThenThrowIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.messageConverter.setProviderConfigurationConverter(null)); + } + + @Test + public void readInternalWhenSuccessfulProviderConfigurationOnlyRequiredParametersThenReadOidcProviderConfiguration() throws Exception { + // @formatter:off + String providerConfigurationResponse = "{\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" + + " \"subject_types_supported\": [\"public\"]\n" + + "}\n"; + // @formatter:on + MockClientHttpResponse response = new MockClientHttpResponse(providerConfigurationResponse.getBytes(), HttpStatus.OK); + OidcProviderConfiguration providerConfiguration = this.messageConverter + .readInternal(OidcProviderConfiguration.class, response); + + 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.getJwksUri()).isEqualTo(new URL("https://example.com/issuer1/oauth2/jwks")); + assertThat(providerConfiguration.getResponseTypes()).containsExactly("code"); + assertThat(providerConfiguration.getSubjectTypes()).containsExactly("public"); + assertThat(providerConfiguration.getScopes()).isNull(); + assertThat(providerConfiguration.getGrantTypes()).isNull(); + assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).isNull(); + } + + @Test + public void readInternalWhenSuccessfulProviderConfigurationThenReadOidcProviderConfiguration() throws Exception { + // @formatter:off + String providerConfigurationResponse = "{\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" + + " \"scopes_supported\": [\"openid\"],\n" + + " \"response_types_supported\": [\"code\"],\n" + + " \"grant_types_supported\": [\"authorization_code\", \"client_credentials\"],\n" + + " \"subject_types_supported\": [\"public\"],\n" + + " \"token_endpoint_auth_methods_supported\": [\"basic\"],\n" + + " \"custom_claim\": \"value\",\n" + + " \"custom_collection_claim\": [\"value1\", \"value2\"]\n" + + "}\n"; + // @formatter:on + MockClientHttpResponse response = new MockClientHttpResponse(providerConfigurationResponse.getBytes(), HttpStatus.OK); + OidcProviderConfiguration providerConfiguration = this.messageConverter + .readInternal(OidcProviderConfiguration.class, response); + + 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.getJwksUri()).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"); + assertThat(providerConfiguration.getSubjectTypes()).containsExactly("public"); + assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).containsExactly("basic"); + assertThat(providerConfiguration.getClaimAsString("custom_claim")).isEqualTo("value"); + assertThat(providerConfiguration.getClaimAsStringList("custom_collection_claim")).containsExactlyInAnyOrder("value1", "value2"); + } + + @Test + public void readInternalWhenFailingConverterThenThrowException() { + String errorMessage = "this is not a valid converter"; + this.messageConverter.setProviderConfigurationConverter(source -> { + throw new RuntimeException(errorMessage); + }); + MockClientHttpResponse response = new MockClientHttpResponse("{}".getBytes(), HttpStatus.OK); + + assertThatExceptionOfType(HttpMessageNotReadableException.class) + .isThrownBy(() -> this.messageConverter.readInternal(OidcProviderConfiguration.class, response)) + .withMessageContaining("An error occurred reading the OpenID Provider Configuration") + .withMessageContaining(errorMessage); + } + + @Test + public void readInternalWhenInvalidProviderConfigurationThenThrowException() { + String providerConfigurationResponse = "{ \"issuer\": null }"; + MockClientHttpResponse response = new MockClientHttpResponse(providerConfigurationResponse.getBytes(), HttpStatus.OK); + + assertThatExceptionOfType(HttpMessageNotReadableException.class) + .isThrownBy(() -> this.messageConverter.readInternal(OidcProviderConfiguration.class, response)) + .withMessageContaining("An error occurred reading the OpenID Provider Configuration") + .withMessageContaining("issuer cannot be null"); + } + + @Test + public void writeInternalWhenOidcProviderConfigurationThenWriteTokenResponse() throws Exception { + OidcProviderConfiguration providerConfiguration = + OidcProviderConfiguration.withClaims() + .issuer("https://example.com/issuer1") + .authorizationEndpoint("https://example.com/issuer1/oauth2/authorize") + .tokenEndpoint("https://example.com/issuer1/oauth2/token") + .jwksUri("https://example.com/issuer1/oauth2/jwks") + .scope("openid") + .responseType("code") + .grantType("authorization_code") + .grantType("client_credentials") + .subjectType("public") + .tokenEndpointAuthenticationMethod("basic") + .claim("custom_claim", "value") + .claim("custom_collection_claim", Arrays.asList("value1", "value2")) + .build(); + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + + this.messageConverter.writeInternal(providerConfiguration, outputMessage); + + String providerConfigurationResponse = outputMessage.getBodyAsString(); + assertThat(providerConfigurationResponse).contains("\"issuer\":\"https://example.com/issuer1\""); + assertThat(providerConfigurationResponse).contains("\"authorization_endpoint\":\"https://example.com/issuer1/oauth2/authorize\""); + assertThat(providerConfigurationResponse).contains("\"token_endpoint\":\"https://example.com/issuer1/oauth2/token\""); + assertThat(providerConfigurationResponse).contains("\"jwks_uri\":\"https://example.com/issuer1/oauth2/jwks\""); + assertThat(providerConfigurationResponse).contains("\"scopes_supported\":[\"openid\"]"); + assertThat(providerConfigurationResponse).contains("\"response_types_supported\":[\"code\"]"); + assertThat(providerConfigurationResponse).contains("\"grant_types_supported\":[\"authorization_code\",\"client_credentials\"]"); + assertThat(providerConfigurationResponse).contains("\"subject_types_supported\":[\"public\"]"); + assertThat(providerConfigurationResponse).contains("\"token_endpoint_auth_methods_supported\":[\"basic\"]"); + assertThat(providerConfigurationResponse).contains("\"custom_claim\":\"value\""); + assertThat(providerConfigurationResponse).contains("\"custom_collection_claim\":[\"value1\",\"value2\"]"); + } + + @Test + @SuppressWarnings("unchecked") + public void writeInternalWhenWriteFailsThenThrowsException() throws MalformedURLException { + String errorMessage = "this is not a valid converter"; + Converter> failingConverter = + source -> { + throw new RuntimeException(errorMessage); + }; + this.messageConverter.setProviderConfigurationParametersConverter(failingConverter); + + OidcProviderConfiguration providerConfiguration = + OidcProviderConfiguration.withClaims() + .issuer("https://example.com") + .authorizationEndpoint("https://example.com") + .tokenEndpoint("https://example.com") + .jwksUri("https://example.com") + .responseType("code") + .subjectType("public") + .build(); + + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + + assertThatThrownBy(() -> this.messageConverter.writeInternal(providerConfiguration, outputMessage)) + .isInstanceOf(HttpMessageNotWritableException.class) + .hasMessageContaining("An error occurred writing the OpenID Provider Configuration") + .hasMessageContaining(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 new file mode 100644 index 0000000..caadaaa --- /dev/null +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/OidcProviderConfigurationTests.java @@ -0,0 +1,398 @@ +/* + * 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.oidc; + +import org.junit.Test; + +import java.net.MalformedURLException; +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.assertThatThrownBy; + +/** + * Tests for {@link OidcProviderConfiguration}. + * + * @author Daniel Garnier-Moiroux + */ +public class OidcProviderConfigurationTests { + private final OidcProviderConfiguration.Builder minimalConfigurationBuilder = + OidcProviderConfiguration.withClaims() + .issuer("https://example.com/issuer1") + .authorizationEndpoint("https://example.com/issuer1/oauth2/authorize") + .tokenEndpoint("https://example.com/issuer1/oauth2/token") + .jwksUri("https://example.com/issuer1/oauth2/jwks") + .scope("openid") + .responseType("code") + .subjectType("public"); + + @Test + public void buildWhenAllRequiredClaimsAndAdditionalClaimsThenCreated() { + OidcProviderConfiguration providerConfiguration = OidcProviderConfiguration.withClaims() + .issuer("https://example.com/issuer1") + .authorizationEndpoint("https://example.com/issuer1/oauth2/authorize") + .tokenEndpoint("https://example.com/issuer1/oauth2/token") + .jwksUri("https://example.com/issuer1/oauth2/jwks") + .scope("openid") + .responseType("code") + .grantType("authorization_code") + .grantType("client_credentials") + .subjectType("public") + .tokenEndpointAuthenticationMethod("basic") + .claim("a-claim", "a-value") + .build(); + + 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.getJwksUri()).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"); + assertThat(providerConfiguration.getSubjectTypes()).containsExactly("public"); + assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).containsExactly("basic"); + assertThat(providerConfiguration.getClaimAsString("a-claim")).isEqualTo("a-value"); + } + + @Test + public void buildWhenOnlyRequiredClaimsThenCreated() { + OidcProviderConfiguration providerConfiguration = OidcProviderConfiguration.withClaims() + .issuer("https://example.com/issuer1") + .authorizationEndpoint("https://example.com/issuer1/oauth2/authorize") + .tokenEndpoint("https://example.com/issuer1/oauth2/token") + .jwksUri("https://example.com/issuer1/oauth2/jwks") + .scope("openid") + .responseType("code") + .subjectType("public") + .build(); + + 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.getJwksUri()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks")); + assertThat(providerConfiguration.getScopes()).containsExactly("openid"); + assertThat(providerConfiguration.getResponseTypes()).containsExactly("code"); + assertThat(providerConfiguration.getGrantTypes()).isNull(); + assertThat(providerConfiguration.getSubjectTypes()).containsExactly("public"); + assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).isNull(); + } + + @Test + public void buildFromClaimsThenCreated() { + HashMap claims = new HashMap<>(); + claims.put(OidcProviderMetadataClaimNames.ISSUER, "https://example.com/issuer1"); + claims.put(OidcProviderMetadataClaimNames.AUTHORIZATION_ENDPOINT, "https://example.com/issuer1/oauth2/authorize"); + claims.put(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT, "https://example.com/issuer1/oauth2/token"); + claims.put(OidcProviderMetadataClaimNames.JWKS_URI, "https://example.com/issuer1/oauth2/jwks"); + claims.put(OidcProviderMetadataClaimNames.SCOPES_SUPPORTED, Collections.singleton("openid")); + claims.put(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, Collections.singleton("code")); + claims.put(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED, Collections.singleton("public")); + claims.put("some-claim", "some-value"); + + OidcProviderConfiguration providerConfiguration = OidcProviderConfiguration.withClaims(claims).build(); + + 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.getJwksUri()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks")); + assertThat(providerConfiguration.getScopes()).containsExactly("openid"); + assertThat(providerConfiguration.getResponseTypes()).containsExactly("code"); + assertThat(providerConfiguration.getGrantTypes()).isNull(); + assertThat(providerConfiguration.getSubjectTypes()).containsExactly("public"); + assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).isNull(); + assertThat(providerConfiguration.getClaimAsString("some-claim")).isEqualTo("some-value"); + } + + @Test + public void buildFromClaimsWhenUsingUrlsThenCreated() { + HashMap claims = new HashMap<>(); + claims.put(OidcProviderMetadataClaimNames.ISSUER, url("https://example.com/issuer1")); + claims.put(OidcProviderMetadataClaimNames.AUTHORIZATION_ENDPOINT, url("https://example.com/issuer1/oauth2/authorize")); + claims.put(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT, url("https://example.com/issuer1/oauth2/token")); + claims.put(OidcProviderMetadataClaimNames.JWKS_URI, url("https://example.com/issuer1/oauth2/jwks")); + claims.put(OidcProviderMetadataClaimNames.SCOPES_SUPPORTED, Collections.singleton("openid")); + claims.put(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, Collections.singleton("code")); + claims.put(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED, Collections.singleton("public")); + claims.put("some-claim", "some-value"); + + OidcProviderConfiguration providerConfiguration = OidcProviderConfiguration.withClaims(claims).build(); + + 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.getJwksUri()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks")); + assertThat(providerConfiguration.getScopes()).containsExactly("openid"); + assertThat(providerConfiguration.getResponseTypes()).containsExactly("code"); + assertThat(providerConfiguration.getGrantTypes()).isNull(); + assertThat(providerConfiguration.getSubjectTypes()).containsExactly("public"); + assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).isNull(); + assertThat(providerConfiguration.getClaimAsString("some-claim")).isEqualTo("some-value"); + } + + @Test + public void withClaimsWhenNullThenThrowsException() { + assertThatThrownBy(() -> OidcProviderConfiguration.withClaims(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void withClaimsWhenMissingRequiredClaimsThenThrowsException() { + assertThatThrownBy(() -> OidcProviderConfiguration.withClaims(Collections.emptyMap())) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void buildWhenCalledTwiceThenGeneratesTwoConfigurations() { + OidcProviderConfiguration first = minimalConfigurationBuilder + .grantType("client_credentials") + .build(); + + OidcProviderConfiguration second = minimalConfigurationBuilder + .claims((claims) -> + { + LinkedHashSet newGrantTypes = new LinkedHashSet<>(); + newGrantTypes.add("authorization_code"); + newGrantTypes.add("implicit"); + claims.put(OidcProviderMetadataClaimNames.GRANT_TYPES_SUPPORTED, newGrantTypes); + } + ) + .build(); + + assertThat(first.getGrantTypes()).containsExactly("client_credentials"); + assertThat(second.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "implicit"); + } + + @Test + public void buildWhenMissingIssuerThenThrowsException() { + OidcProviderConfiguration.Builder builder = minimalConfigurationBuilder + .claims((claims) -> claims.remove(OidcProviderMetadataClaimNames.ISSUER)); + + assertThatThrownBy(builder::build) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("issuer cannot be null"); + } + + @Test + public void buildWhenIssuerIsNotAnUrlThenThrowsException() { + OidcProviderConfiguration.Builder builder = minimalConfigurationBuilder + .claims((claims) -> claims.put(OidcProviderMetadataClaimNames.ISSUER, "not an url")); + + assertThatThrownBy(builder::build) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageStartingWith("issuer must be a valid URL"); + } + + @Test + public void buildWhenMissingAuthorizationEndpointThenThrowsException() { + OidcProviderConfiguration.Builder builder = minimalConfigurationBuilder + .claims((claims) -> claims.remove(OidcProviderMetadataClaimNames.AUTHORIZATION_ENDPOINT)); + + assertThatThrownBy(builder::build) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("authorizationEndpoint cannot be null"); + } + + @Test + public void buildWhenAuthorizationEndpointIsNotAnUrlThenThrowsException() { + OidcProviderConfiguration.Builder builder = minimalConfigurationBuilder + .claims((claims) -> claims.put(OidcProviderMetadataClaimNames.AUTHORIZATION_ENDPOINT, "not an url")); + + assertThatThrownBy(builder::build) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageStartingWith("authorizationEndpoint must be a valid URL"); + } + + @Test + public void buildWhenMissingTokenEndpointThenThrowsException() { + OidcProviderConfiguration.Builder builder = minimalConfigurationBuilder + .claims((claims) -> claims.remove(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT)); + + assertThatThrownBy(builder::build) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("tokenEndpoint cannot be null"); + } + + @Test + public void buildWhenTokenEndpointIsNotAnUrlThenThrowsException() { + OidcProviderConfiguration.Builder builder = minimalConfigurationBuilder + .claims((claims) -> claims.put(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT, "not an url")); + + assertThatThrownBy(builder::build) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageStartingWith("tokenEndpoint must be a valid URL"); + } + + @Test + public void buildWhenMissingJwksUriThenThrowsException() { + OidcProviderConfiguration.Builder builder = minimalConfigurationBuilder + .claims((claims) -> claims.remove(OidcProviderMetadataClaimNames.JWKS_URI)); + + assertThatThrownBy(builder::build) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("jwkSetUri cannot be null"); + } + + @Test + public void buildWheJwksUriIsNotAnUrlThenThrowsException() { + OidcProviderConfiguration.Builder builder = minimalConfigurationBuilder + .claims((claims) -> claims.put(OidcProviderMetadataClaimNames.JWKS_URI, "not an url")); + + assertThatThrownBy(builder::build) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageStartingWith("jwkSetUri must be a valid URL"); + } + + @Test + public void buildWhenMissingResponseTypesThenThrowsException() { + OidcProviderConfiguration.Builder builder = minimalConfigurationBuilder + .claims((claims) -> claims.remove(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED)); + + assertThatThrownBy(builder::build) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("responseTypes cannot be empty"); + } + + @Test + public void buildWhenMissingSubjectTypesThenThrowsException() { + OidcProviderConfiguration.Builder builder = minimalConfigurationBuilder + .claims((claims) -> claims.remove(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED)); + + assertThatThrownBy(builder::build) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("subjectTypes cannot be empty"); + } + + @Test + public void responseTypesWhenAddingOrRemovingThenCorrectValues() { + OidcProviderConfiguration configuration = 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() { + OidcProviderConfiguration configuration = minimalConfigurationBuilder + .claims(claims -> claims.remove(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED)) + .responseTypes(responseTypes -> responseTypes.add("some-response-type")) + .build(); + + assertThat(configuration.getResponseTypes()).containsExactly("some-response-type"); + } + + @Test + public void subjectTypesWhenAddingOrRemovingThenCorrectValues() { + OidcProviderConfiguration configuration = minimalConfigurationBuilder + .subjectType("should-be-removed") + .subjectTypes(subjectTypes -> { + subjectTypes.clear(); + subjectTypes.add("some-subject-type"); + }) + .build(); + + assertThat(configuration.getSubjectTypes()).containsExactly("some-subject-type"); + } + + @Test + public void scopesWhenAddingOrRemovingThenCorrectValues() { + OidcProviderConfiguration configuration = minimalConfigurationBuilder + .scope("should-be-removed") + .scopes(scopes -> { + scopes.clear(); + scopes.add("some-scope"); + }) + .build(); + + assertThat(configuration.getScopes()).containsExactly("some-scope"); + } + + @Test + public void grantTypesWhenAddingOrRemovingThenCorrectValues() { + OidcProviderConfiguration configuration = 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() { + OidcProviderConfiguration configuration = 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 claimWhenNameIsNullThenThrowIllegalArgumentException() { + OidcProviderConfiguration.Builder builder = OidcProviderConfiguration.withClaims(); + assertThatThrownBy(() -> builder.claim(null, "value")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("name cannot be empty"); + } + + @Test + public void claimWhenValueIsNullThenThrowIllegalArgumentException() { + OidcProviderConfiguration.Builder builder = OidcProviderConfiguration.withClaims(); + assertThatThrownBy(() -> builder.claim("claim-name", null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("value cannot be null"); + } + + @Test + public void claimsWhenRemovingAClaimThenIsNotPresent() { + OidcProviderConfiguration configuration = + minimalConfigurationBuilder + .grantType("some-grant-type") + .claims((claims) -> claims.remove(OidcProviderMetadataClaimNames.GRANT_TYPES_SUPPORTED)) + .build(); + assertThat(configuration.getGrantTypes()).isNull(); + } + + @Test + public void claimsWhenAddingAClaimThenIsPresent() { + OidcProviderConfiguration configuration = + minimalConfigurationBuilder + .claims((claims) -> claims.put(OidcProviderMetadataClaimNames.GRANT_TYPES_SUPPORTED, "authorization_code")) + .build(); + assertThat(configuration.getGrantTypes()).containsExactly("authorization_code"); + } + + private static URL url(String urlString) { + try { + return new URL(urlString); + } catch (MalformedURLException e) { + 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/server/authorization/config/ProviderSettingsTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/ProviderSettingsTests.java new file mode 100644 index 0000000..040d298 --- /dev/null +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/ProviderSettingsTests.java @@ -0,0 +1,126 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.server.authorization.config; + +import org.junit.Test; + +import java.net.MalformedURLException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link ProviderSettings}. + * + * @author Daniel Garnier-Moiroux + */ +public class ProviderSettingsTests { + @Test + public void constructorWhenDefaultThenDefaultsAreSetAndIssuerIsNotSet() { + ProviderSettings providerSettings = new ProviderSettings(); + + assertThat(providerSettings.issuer()).isNull(); + assertThat(providerSettings.authorizationEndpoint()).isEqualTo("/oauth2/authorize"); + assertThat(providerSettings.tokenEndpoint()).isEqualTo("/oauth2/token"); + assertThat(providerSettings.jwkSetEndpoint()).isEqualTo("/oauth2/jwks"); + assertThat(providerSettings.tokenRevocationEndpoint()).isEqualTo("/oauth2/revoke"); + } + + @Test + public void settingsWhenProvidedThenSet() throws MalformedURLException { + String authorizationEndpoint = "/my-endpoints/authorize"; + String tokenEndpoint = "/my-endpoints/token"; + String jwksEndpoint = "/my-endpoints/jwks"; + String tokenRevocationEndpoint = "/my-endpoints/revoke"; + String issuer = "https://example.com/9000"; + + ProviderSettings providerSettings = new ProviderSettings() + .issuer(issuer) + .authorizationEndpoint(authorizationEndpoint) + .tokenEndpoint(tokenEndpoint) + .jwkSetEndpoint(jwksEndpoint) + .tokenRevocationEndpoint(tokenRevocationEndpoint); + + assertThat(providerSettings.issuer()).isEqualTo(issuer); + assertThat(providerSettings.authorizationEndpoint()).isEqualTo(authorizationEndpoint); + assertThat(providerSettings.tokenEndpoint()).isEqualTo(tokenEndpoint); + assertThat(providerSettings.jwkSetEndpoint()).isEqualTo(jwksEndpoint); + assertThat(providerSettings.tokenRevocationEndpoint()).isEqualTo(tokenRevocationEndpoint); + } + + @Test + public void settingWhenCalledThenReturnTokenSettings() { + ProviderSettings providerSettings = new ProviderSettings() + .setting("name1", "value1") + .settings(settings -> settings.put("name2", "value2")); + + assertThat(providerSettings.settings()).hasSize(6); + assertThat(providerSettings.setting("name1")).isEqualTo("value1"); + assertThat(providerSettings.setting("name2")).isEqualTo("value2"); + } + + @Test + public void issuerWhenNullThenThrowsIllegalArgumentException() { + ProviderSettings settings = new ProviderSettings(); + assertThatThrownBy(() -> settings.issuer(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("issuer cannot be null"); + } + + @Test + public void authorizationEndpointWhenNullThenThrowsIllegalArgumentException() { + ProviderSettings settings = new ProviderSettings(); + assertThatThrownBy(() -> settings.authorizationEndpoint(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("authorizationEndpoint cannot be empty"); + assertThatThrownBy(() -> settings.authorizationEndpoint("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("authorizationEndpoint cannot be empty"); + } + + @Test + public void tokenEndpointWhenNullThenThrowsIllegalArgumentException() { + ProviderSettings settings = new ProviderSettings(); + assertThatThrownBy(() -> settings.tokenEndpoint(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("tokenEndpoint cannot be empty"); + assertThatThrownBy(() -> settings.tokenEndpoint("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("tokenEndpoint cannot be empty"); + } + + @Test + public void tokenRevocationEndpointWhenNullThenThrowsIllegalArgumentException() { + ProviderSettings settings = new ProviderSettings(); + assertThatThrownBy(() -> settings.tokenRevocationEndpoint(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("tokenRevocationEndpoint cannot be empty"); + assertThatThrownBy(() -> settings.tokenRevocationEndpoint("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("tokenRevocationEndpoint cannot be empty"); + } + + @Test + public void jwkSetEndpointWhenNullThenThrowsIllegalArgumentException() { + ProviderSettings settings = new ProviderSettings(); + assertThatThrownBy(() -> settings.jwkSetEndpoint(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("jwkSetEndpoint cannot be empty"); + assertThatThrownBy(() -> settings.jwkSetEndpoint("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("jwkSetEndpoint cannot be empty"); + } +} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OidcProviderConfigurationEndpointFilterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OidcProviderConfigurationEndpointFilterTests.java new file mode 100644 index 0000000..d6da8ef --- /dev/null +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OidcProviderConfigurationEndpointFilterTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.server.authorization.web; + +import org.junit.Test; +import org.springframework.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.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +/** + * Tests for {@link OidcProviderConfigurationEndpointFilter}. + * + * @author Daniel Garnier-Moiroux + */ +public class OidcProviderConfigurationEndpointFilterTests { + @Test + public void constructorWhenProviderSettingsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new OidcProviderConfigurationEndpointFilter(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("providerSettings cannot be null"); + } + + @Test + public void doFilterWhenRequestDoesNotMatchThenNotProcessed() throws Exception { + OidcProviderConfigurationEndpointFilter filter = new OidcProviderConfigurationEndpointFilter(new ProviderSettings()); + String requestUri = "/path"; + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + filter.doFilter(request, response, filterChain); + + verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); + } + + @Test + public void doFilterWhenSuccessThenConfigurationResponse() throws Exception { + String authorizationEndpoint = "/my-endpoints/authorize"; + String tokenEndpoint = "/my-endpoints/token"; + String jwksEndpoint = "/my-endpoints/jwks"; + + ProviderSettings providerSettings = new ProviderSettings() + .issuer("https://example.com/issuer1") + .authorizationEndpoint(authorizationEndpoint) + .tokenEndpoint(tokenEndpoint) + .jwkSetEndpoint(jwksEndpoint); + OidcProviderConfigurationEndpointFilter filter = new OidcProviderConfigurationEndpointFilter(providerSettings); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", org.springframework.security.oauth2.server.authorization.web.OidcProviderConfigurationEndpointFilter.DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI); + request.setServletPath(org.springframework.security.oauth2.server.authorization.web.OidcProviderConfigurationEndpointFilter.DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + filter.doFilter(request, response, filterChain); + + verifyNoInteractions(filterChain); + + assertThat(response.getContentType()).isEqualTo(MediaType.APPLICATION_JSON_VALUE); + String providerConfigurationResponse = response.getContentAsString(); + assertThat(providerConfigurationResponse).contains("\"issuer\":\"https://example.com/issuer1\""); + assertThat(providerConfigurationResponse).contains("\"authorization_endpoint\":\"https://example.com/issuer1/my-endpoints/authorize\""); + assertThat(providerConfigurationResponse).contains("\"token_endpoint\":\"https://example.com/issuer1/my-endpoints/token\""); + assertThat(providerConfigurationResponse).contains("\"jwks_uri\":\"https://example.com/issuer1/my-endpoints/jwks\""); + assertThat(providerConfigurationResponse).contains("\"scopes_supported\":[\"openid\"]"); + assertThat(providerConfigurationResponse).contains("\"response_types_supported\":[\"code\"]"); + assertThat(providerConfigurationResponse).contains("\"grant_types_supported\":[\"authorization_code\",\"client_credentials\"]"); + assertThat(providerConfigurationResponse).contains("\"subject_types_supported\":[\"public\"]"); + assertThat(providerConfigurationResponse).contains("\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\"]"); + } + + + @Test + public void doFilterWhenProviderSettingsWithInvalidIssuerThenThrowIllegalArgumentException() { + ProviderSettings providerSettings = new ProviderSettings() + .issuer("https://this is an invalid URL"); + OidcProviderConfigurationEndpointFilter filter = new OidcProviderConfigurationEndpointFilter(providerSettings); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", org.springframework.security.oauth2.server.authorization.web.OidcProviderConfigurationEndpointFilter.DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI); + request.setServletPath(org.springframework.security.oauth2.server.authorization.web.OidcProviderConfigurationEndpointFilter.DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + assertThatThrownBy(() -> filter.doFilter(request, response, filterChain)) + .isInstanceOf(IllegalArgumentException.class); + } +}