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
This commit is contained in:
Daniel Garnier-Moiroux 2020-10-16 14:56:39 +02:00 committed by Joe Grandja
parent 43fbd9d345
commit 6a5e277a11
14 changed files with 2140 additions and 5 deletions

View File

@ -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<B extends HttpSecurityBui
OAuth2TokenRevocationEndpointFilter.DEFAULT_TOKEN_REVOCATION_ENDPOINT_URI, HttpMethod.POST.name());
private final RequestMatcher jwkSetEndpointMatcher = new AntPathRequestMatcher(
JwkSetEndpointFilter.DEFAULT_JWK_SET_ENDPOINT_URI, HttpMethod.GET.name());
private final RequestMatcher oidcProviderConfigurationEndpointMatcher = new AntPathRequestMatcher(
OidcProviderConfigurationEndpointFilter.DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI, HttpMethod.GET.name());
/**
* Sets the repository of registered clients.
@ -122,18 +129,33 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
return this;
}
/**
* Sets the provider settings.
*
* @param providerSettings the provider settings
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
*/
public OAuth2AuthorizationServerConfigurer<B> 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<RequestMatcher> 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<B extends HttpSecurityBui
@Override
public void configure(B builder) {
JwkSetEndpointFilter jwkSetEndpointFilter = new JwkSetEndpointFilter(getKeySource(builder));
if (getProviderSettings(builder).issuer() != null) {
OidcProviderConfigurationEndpointFilter oidcProviderConfigurationEndpointFilter = new OidcProviderConfigurationEndpointFilter(getProviderSettings(builder));
builder.addFilterBefore(postProcess(oidcProviderConfigurationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
}
JwkSetEndpointFilter jwkSetEndpointFilter = new JwkSetEndpointFilter(
getKeySource(builder),
getProviderSettings(builder).jwkSetEndpoint());
builder.addFilterBefore(postProcess(jwkSetEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
@ -200,18 +229,21 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
OAuth2AuthorizationEndpointFilter authorizationEndpointFilter =
new OAuth2AuthorizationEndpointFilter(
getRegisteredClientRepository(builder),
getAuthorizationService(builder));
getAuthorizationService(builder),
getProviderSettings(builder).authorizationEndpoint());
builder.addFilterBefore(postProcess(authorizationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
OAuth2TokenEndpointFilter tokenEndpointFilter =
new OAuth2TokenEndpointFilter(
authenticationManager,
getAuthorizationService(builder));
getAuthorizationService(builder),
getProviderSettings(builder).tokenEndpoint());
builder.addFilterAfter(postProcess(tokenEndpointFilter), FilterSecurityInterceptor.class);
OAuth2TokenRevocationEndpointFilter tokenRevocationEndpointFilter =
new OAuth2TokenRevocationEndpointFilter(
authenticationManager);
authenticationManager,
getProviderSettings(builder).tokenRevocationEndpoint());
builder.addFilterAfter(postProcess(tokenRevocationEndpointFilter), OAuth2TokenEndpointFilter.class);
}
@ -263,4 +295,37 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
private static <B extends HttpSecurityBuilder<B>> CryptoKeySource getKeySourceBean(B builder) {
return builder.getSharedObject(ApplicationContext.class).getBean(CryptoKeySource.class);
}
private static <B extends HttpSecurityBuilder<B>> 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 <B extends HttpSecurityBuilder<B>> ProviderSettings getProviderSettingsBean(B builder) {
Map<String, ProviderSettings> 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");
}
}
}
}

View File

@ -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 <a target="_blank" href="https://github.com/spring-projects/spring-security/pull/9146">Issue gh-9146</a>
*/
final public class ObjectToSetStringConverter2 implements ConditionalGenericConverter {
@Override
public Set<GenericConverter.ConvertiblePair> getConvertibleTypes() {
Set<GenericConverter.ConvertiblePair> 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<String> results = new LinkedHashSet<>();
for (Object object : ((Collection<?>) source)) {
if (object != null) {
results.add(object.toString());
}
}
return results;
}
return Collections.singleton(source.toString());
}
}

View File

@ -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<OidcProviderConfiguration> {
private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP =
new ParameterizedTypeReference<Map<String, Object>>() {
};
private final GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter();
private Converter<Map<String, Object>, OidcProviderConfiguration> providerConfigurationConverter = new OidcProviderConfigurationConverter();
private Converter<OidcProviderConfiguration, Map<String, Object>> 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<? extends OidcProviderConfiguration> clazz, HttpInputMessage inputMessage)
throws HttpMessageNotReadableException {
try {
Map<String, Object> providerConfigurationParameters = (Map<String, Object>) 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<String, Object> 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<OidcProviderConfiguration, Map<String, Object>> 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<Map<String, Object>, OidcProviderConfiguration> providerConfigurationConverter) {
Assert.notNull(providerConfigurationConverter, "providerConfigurationConverter cannot be null");
this.providerConfigurationConverter = providerConfigurationConverter;
}
private static final class OidcProviderConfigurationConverter implements Converter<Map<String, Object>, 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<String, Converter<Object, ?>> claimNameToConverter = new HashMap<>();
Converter<Object, ?> setStringConverter = getConverter(TypeDescriptor.collection(Set.class, STRING_TYPE_DESCRIPTOR));
Converter<Object, ?> 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<String, Object> source) {
Map<String, Object> parsedClaims = this.claimTypeConverter.convert(source);
return OidcProviderConfiguration.withClaims(parsedClaims).build();
}
private static Converter<Object, ?> getConverter(TypeDescriptor targetDescriptor) {
return (source) -> CLAIM_CONVERSION_SERVICE.convert(source, OBJECT_TYPE_DESCRIPTOR, targetDescriptor);
}
}
}

View File

@ -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 <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse">4.2. OpenID Provider Configuration Response</a>
*/
public class OidcProviderConfiguration implements OidcProviderMetadataClaimAccessor, Serializable {
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
private final Map<String, Object> claims;
private OidcProviderConfiguration(Map<String, Object> 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<String, Object> 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<String, Object> 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 <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata">OpenID Connect Discovery 1.0</a>
* for required claims
*/
public static final class Builder {
private final Map<String, Object> 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<Set<String>> 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<Set<String>> 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<Set<String>> 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<Set<String>> 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<Set<String>> 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<Map<String, Object>> 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<String>());
((Set<String>) this.claims.get(name)).add(value);
}
@SuppressWarnings("unchecked")
private void applyToClaim(String name, Consumer<Set<String>> consumer) {
this.claims.putIfAbsent(name, new LinkedHashSet<String>());
Set<String> values = (Set<String>) this.claims.get(name);
consumer.accept(values);
}
}
}

View File

@ -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 <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata">3. OpenID Provider Metadata</a>
*/
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<String> 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<String> 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<String> 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<String> 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<String> getScopes() {
return this.getClaimAsStringList(OidcProviderMetadataClaimNames.SCOPES_SUPPORTED);
}
}

View File

@ -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 <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata">3. OpenID Provider Metadata</a>
*/
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";
}

View File

@ -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 <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata">OpenID Connect Discovery 1.0</a>
*/
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<String, Object> defaultSettings() {
Map<String, Object> 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;
}
}

View File

@ -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 <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig">OpenID Connect Discovery 1.0</a>
*/
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();
}
}

View File

@ -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");
}
}
}

View File

@ -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<String> result = (Set<String>) converter.convert(null, null, null);
assertThat(result).isNull();
}
@Test
@SuppressWarnings("unchecked")
public void convertFromStringThenReturnSet() {
ObjectToSetStringConverter2 converter = new ObjectToSetStringConverter2();
Set<String> result = (Set<String>) converter.convert("Hello", null, null);
assertThat(result).containsExactly("Hello");
}
@Test
@SuppressWarnings("unchecked")
public void convertFromSetThenReturnSet() {
ObjectToSetStringConverter2 converter = new ObjectToSetStringConverter2();
Set<String> result = (Set<String>) 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<String> result = (Set<String>) converter.convert(Arrays.asList("Hello", "world"), null, null);
assertThat(result).containsExactlyInAnyOrder("Hello", "world");
}
@Test
@SuppressWarnings("unchecked")
public void convertFromEmptyCollectionThenReturnEmptySet() {
ObjectToSetStringConverter2 converter = new ObjectToSetStringConverter2();
Set<String> result = (Set<String>) converter.convert(Collections.emptyList(), null, null);
assertThat(result).isEmpty();
}
}

View File

@ -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<OidcProviderConfiguration, Map<String, Object>> 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);
}
}

View File

@ -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<String, Object> 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<String, Object> 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<String> 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");
}
}
}

View File

@ -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.<String>setting("name1")).isEqualTo("value1");
assertThat(providerSettings.<String>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");
}
}

View File

@ -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);
}
}