Skip to content

Commit 4a2ee31

Browse files
committed
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, ...)
1 parent 43fbd9d commit 4a2ee31

File tree

14 files changed

+2140
-5
lines changed

14 files changed

+2140
-5
lines changed

oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,13 @@
3434
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationProvider;
3535
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationProvider;
3636
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
37+
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
3738
import org.springframework.security.oauth2.server.authorization.web.JwkSetEndpointFilter;
3839
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
3940
import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientAuthenticationFilter;
4041
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
4142
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenRevocationEndpointFilter;
43+
import org.springframework.security.oauth2.server.authorization.web.OidcProviderConfigurationEndpointFilter;
4244
import org.springframework.security.web.AuthenticationEntryPoint;
4345
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
4446
import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint;
@@ -52,6 +54,9 @@
5254
import org.springframework.util.Assert;
5355
import org.springframework.util.StringUtils;
5456

57+
import java.net.MalformedURLException;
58+
import java.net.URI;
59+
import java.net.URISyntaxException;
5560
import java.util.Arrays;
5661
import java.util.LinkedHashMap;
5762
import java.util.List;
@@ -85,6 +90,8 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
8590
OAuth2TokenRevocationEndpointFilter.DEFAULT_TOKEN_REVOCATION_ENDPOINT_URI, HttpMethod.POST.name());
8691
private final RequestMatcher jwkSetEndpointMatcher = new AntPathRequestMatcher(
8792
JwkSetEndpointFilter.DEFAULT_JWK_SET_ENDPOINT_URI, HttpMethod.GET.name());
93+
private final RequestMatcher oidcProviderConfigurationEndpointMatcher = new AntPathRequestMatcher(
94+
OidcProviderConfigurationEndpointFilter.DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI, HttpMethod.GET.name());
8895

8996
/**
9097
* Sets the repository of registered clients.
@@ -122,18 +129,33 @@ public OAuth2AuthorizationServerConfigurer<B> keySource(CryptoKeySource keySourc
122129
return this;
123130
}
124131

132+
/**
133+
* Sets the provider settings.
134+
*
135+
* @param providerSettings the provider settings
136+
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
137+
*/
138+
public OAuth2AuthorizationServerConfigurer<B> providerSettings(ProviderSettings providerSettings) {
139+
Assert.notNull(providerSettings, "providerSettings cannot be null");
140+
this.getBuilder().setSharedObject(ProviderSettings.class, providerSettings);
141+
return this;
142+
}
143+
125144
/**
126145
* Returns a {@code List} of {@link RequestMatcher}'s for the authorization server endpoints.
127146
*
128147
* @return a {@code List} of {@link RequestMatcher}'s for the authorization server endpoints
129148
*/
130149
public List<RequestMatcher> getEndpointMatchers() {
150+
// TODO: use ProviderSettings instead
131151
return Arrays.asList(this.authorizationEndpointMatcher, this.tokenEndpointMatcher,
132-
this.tokenRevocationEndpointMatcher, this.jwkSetEndpointMatcher);
152+
this.tokenRevocationEndpointMatcher, this.jwkSetEndpointMatcher, this.oidcProviderConfigurationEndpointMatcher);
133153
}
134154

135155
@Override
136156
public void init(B builder) {
157+
ProviderSettings providerSettings = getProviderSettings(builder);
158+
validateProviderSettings(providerSettings);
137159
OAuth2ClientAuthenticationProvider clientAuthenticationProvider =
138160
new OAuth2ClientAuthenticationProvider(
139161
getRegisteredClientRepository(builder),
@@ -186,7 +208,14 @@ public void init(B builder) {
186208

187209
@Override
188210
public void configure(B builder) {
189-
JwkSetEndpointFilter jwkSetEndpointFilter = new JwkSetEndpointFilter(getKeySource(builder));
211+
if (getProviderSettings(builder).issuer() != null) {
212+
OidcProviderConfigurationEndpointFilter oidcProviderConfigurationEndpointFilter = new OidcProviderConfigurationEndpointFilter(getProviderSettings(builder));
213+
builder.addFilterBefore(postProcess(oidcProviderConfigurationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
214+
}
215+
216+
JwkSetEndpointFilter jwkSetEndpointFilter = new JwkSetEndpointFilter(
217+
getKeySource(builder),
218+
getProviderSettings(builder).jwkSetEndpoint());
190219
builder.addFilterBefore(postProcess(jwkSetEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
191220

192221
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
@@ -200,18 +229,21 @@ public void configure(B builder) {
200229
OAuth2AuthorizationEndpointFilter authorizationEndpointFilter =
201230
new OAuth2AuthorizationEndpointFilter(
202231
getRegisteredClientRepository(builder),
203-
getAuthorizationService(builder));
232+
getAuthorizationService(builder),
233+
getProviderSettings(builder).authorizationEndpoint());
204234
builder.addFilterBefore(postProcess(authorizationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
205235

206236
OAuth2TokenEndpointFilter tokenEndpointFilter =
207237
new OAuth2TokenEndpointFilter(
208238
authenticationManager,
209-
getAuthorizationService(builder));
239+
getAuthorizationService(builder),
240+
getProviderSettings(builder).tokenEndpoint());
210241
builder.addFilterAfter(postProcess(tokenEndpointFilter), FilterSecurityInterceptor.class);
211242

212243
OAuth2TokenRevocationEndpointFilter tokenRevocationEndpointFilter =
213244
new OAuth2TokenRevocationEndpointFilter(
214-
authenticationManager);
245+
authenticationManager,
246+
getProviderSettings(builder).tokenRevocationEndpoint());
215247
builder.addFilterAfter(postProcess(tokenRevocationEndpointFilter), OAuth2TokenEndpointFilter.class);
216248
}
217249

@@ -263,4 +295,37 @@ private static <B extends HttpSecurityBuilder<B>> CryptoKeySource getKeySource(B
263295
private static <B extends HttpSecurityBuilder<B>> CryptoKeySource getKeySourceBean(B builder) {
264296
return builder.getSharedObject(ApplicationContext.class).getBean(CryptoKeySource.class);
265297
}
298+
299+
private static <B extends HttpSecurityBuilder<B>> ProviderSettings getProviderSettings(B builder) {
300+
ProviderSettings providerSettings = builder.getSharedObject(ProviderSettings.class);
301+
if (providerSettings == null) {
302+
providerSettings = getProviderSettingsBean(builder);
303+
if (providerSettings == null) {
304+
providerSettings = new ProviderSettings();
305+
}
306+
builder.setSharedObject(ProviderSettings.class, providerSettings);
307+
}
308+
return providerSettings;
309+
}
310+
311+
private static <B extends HttpSecurityBuilder<B>> ProviderSettings getProviderSettingsBean(B builder) {
312+
Map<String, ProviderSettings> providerSettingsMap = BeanFactoryUtils.beansOfTypeIncludingAncestors(
313+
builder.getSharedObject(ApplicationContext.class), ProviderSettings.class);
314+
if (providerSettingsMap.size() > 1) {
315+
throw new NoUniqueBeanDefinitionException(ProviderSettings.class, providerSettingsMap.size(),
316+
"Expected single matching bean of type '" + ProviderSettings.class.getName() + "' but found " +
317+
providerSettingsMap.size() + ": " + StringUtils.collectionToCommaDelimitedString(providerSettingsMap.keySet()));
318+
}
319+
return (!providerSettingsMap.isEmpty() ? providerSettingsMap.values().iterator().next() : null);
320+
}
321+
322+
private void validateProviderSettings(ProviderSettings providerSettings) {
323+
if (providerSettings.issuer() != null) {
324+
try {
325+
new URI(providerSettings.issuer()).toURL();
326+
} catch (MalformedURLException | URISyntaxException e) {
327+
throw new IllegalArgumentException("issuer must be a valid URL");
328+
}
329+
}
330+
}
266331
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright 2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.security.oauth2.core.converter;
17+
18+
import org.springframework.core.convert.TypeDescriptor;
19+
import org.springframework.core.convert.converter.ConditionalGenericConverter;
20+
import org.springframework.core.convert.converter.GenericConverter;
21+
import org.springframework.util.ClassUtils;
22+
23+
import java.util.Collection;
24+
import java.util.Collections;
25+
import java.util.LinkedHashSet;
26+
import java.util.Set;
27+
28+
/**
29+
* TODO
30+
* This class is temporary and will be removed after upgrading to Spring Security 5.5.0 GA.
31+
*
32+
* @author Daniel Garnier-Moiroux
33+
* @since 0.1.0
34+
* @see <a target="_blank" href="https://github.com/spring-projects/spring-security/pull/9146">Issue gh-9146</a>
35+
*/
36+
final public class ObjectToSetStringConverter2 implements ConditionalGenericConverter {
37+
38+
@Override
39+
public Set<GenericConverter.ConvertiblePair> getConvertibleTypes() {
40+
Set<GenericConverter.ConvertiblePair> convertibleTypes = new LinkedHashSet<>();
41+
convertibleTypes.add(new GenericConverter.ConvertiblePair(Object.class, Set.class));
42+
return convertibleTypes;
43+
}
44+
45+
@Override
46+
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
47+
if (targetType.getElementTypeDescriptor() == null
48+
|| targetType.getElementTypeDescriptor().getType().equals(String.class) || sourceType == null
49+
|| ClassUtils.isAssignable(sourceType.getType(), targetType.getElementTypeDescriptor().getType())) {
50+
return true;
51+
}
52+
return false;
53+
}
54+
55+
@Override
56+
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
57+
if (source == null) {
58+
return null;
59+
}
60+
if (source instanceof Set) {
61+
Set<?> sourceList = (Set<?>) source;
62+
for (Object entry: sourceList) {
63+
if (entry instanceof String) {
64+
return source;
65+
}
66+
}
67+
}
68+
if (source instanceof Collection) {
69+
Collection<String> results = new LinkedHashSet<>();
70+
for (Object object : ((Collection<?>) source)) {
71+
if (object != null) {
72+
results.add(object.toString());
73+
}
74+
}
75+
return results;
76+
}
77+
return Collections.singleton(source.toString());
78+
}
79+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/*
2+
* Copyright 2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.security.oauth2.core.http.converter;
17+
18+
import org.springframework.core.ParameterizedTypeReference;
19+
import org.springframework.core.convert.TypeDescriptor;
20+
import org.springframework.core.convert.converter.Converter;
21+
import org.springframework.http.HttpInputMessage;
22+
import org.springframework.http.HttpOutputMessage;
23+
import org.springframework.http.MediaType;
24+
import org.springframework.http.converter.AbstractHttpMessageConverter;
25+
import org.springframework.http.converter.GenericHttpMessageConverter;
26+
import org.springframework.http.converter.HttpMessageConverter;
27+
import org.springframework.http.converter.HttpMessageNotReadableException;
28+
import org.springframework.http.converter.HttpMessageNotWritableException;
29+
import org.springframework.security.oauth2.core.converter.ClaimConversionService;
30+
import org.springframework.security.oauth2.core.converter.ClaimTypeConverter;
31+
import org.springframework.security.oauth2.core.converter.ObjectToSetStringConverter2;
32+
import org.springframework.security.oauth2.core.oidc.OidcProviderConfiguration;
33+
import org.springframework.security.oauth2.core.oidc.OidcProviderMetadataClaimNames;
34+
import org.springframework.util.Assert;
35+
36+
import java.io.IOException;
37+
import java.net.URL;
38+
import java.util.HashMap;
39+
import java.util.Map;
40+
import java.util.Set;
41+
42+
43+
/**
44+
* A {@link HttpMessageConverter} for an {@link OidcProviderConfiguration OpenID Provider Configuration Metadata}.
45+
*
46+
* @author Daniel Garnier-Moiroux
47+
* @since 0.1.0
48+
* @see AbstractHttpMessageConverter
49+
* @see OidcProviderConfiguration
50+
*/
51+
public class OidcProviderConfigurationHttpMessageConverter
52+
extends AbstractHttpMessageConverter<OidcProviderConfiguration> {
53+
private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP =
54+
new ParameterizedTypeReference<Map<String, Object>>() {
55+
};
56+
57+
private final GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter();
58+
59+
private Converter<Map<String, Object>, OidcProviderConfiguration> providerConfigurationConverter = new OidcProviderConfigurationConverter();
60+
private Converter<OidcProviderConfiguration, Map<String, Object>> providerConfigurationParametersConverter = OidcProviderConfiguration::getClaims;
61+
62+
public OidcProviderConfigurationHttpMessageConverter() {
63+
super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
64+
}
65+
66+
@Override
67+
protected boolean supports(Class<?> clazz) {
68+
return OidcProviderConfiguration.class.isAssignableFrom(clazz);
69+
}
70+
71+
@Override
72+
@SuppressWarnings("unchecked")
73+
protected OidcProviderConfiguration readInternal(Class<? extends OidcProviderConfiguration> clazz, HttpInputMessage inputMessage)
74+
throws HttpMessageNotReadableException {
75+
try {
76+
Map<String, Object> providerConfigurationParameters = (Map<String, Object>) this.jsonMessageConverter.read(STRING_OBJECT_MAP.getType(), null, inputMessage);
77+
return this.providerConfigurationConverter.convert(providerConfigurationParameters);
78+
} catch (Exception ex) {
79+
throw new HttpMessageNotReadableException(
80+
"An error occurred reading the OpenID Provider Configuration: " + ex.getMessage(), ex, inputMessage);
81+
}
82+
}
83+
84+
@Override
85+
protected void writeInternal(OidcProviderConfiguration providerConfiguration, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
86+
try {
87+
Map<String, Object> providerConfigurationResponseParameters =
88+
this.providerConfigurationParametersConverter.convert(providerConfiguration);
89+
this.jsonMessageConverter.write(
90+
providerConfigurationResponseParameters,
91+
STRING_OBJECT_MAP.getType(),
92+
MediaType.APPLICATION_JSON,
93+
outputMessage
94+
);
95+
} catch (Exception ex) {
96+
throw new HttpMessageNotWritableException(
97+
"An error occurred writing the OpenID Provider Configuration: " + ex.getMessage(), ex);
98+
}
99+
}
100+
101+
/**
102+
* Sets the {@link Converter} used for converting the {@link OidcProviderConfiguration} to a
103+
* {@code Map} representation of the OpenID Provider Configuration.
104+
*
105+
* @param providerConfigurationParametersConverter the {@link Converter} used for converting to a
106+
* {@code Map} representation of the OpenID Provider Configuration
107+
*/
108+
public final void setProviderConfigurationParametersConverter(
109+
Converter<OidcProviderConfiguration, Map<String, Object>> providerConfigurationParametersConverter) {
110+
Assert.notNull(providerConfigurationParametersConverter, "providerConfigurationParametersConverter cannot be null");
111+
this.providerConfigurationParametersConverter = providerConfigurationParametersConverter;
112+
}
113+
114+
/**
115+
* Sets the {@link Converter} used for converting the OpenID Provider Configuration parameters
116+
* to an {@link OidcProviderConfiguration}.
117+
*
118+
* @param providerConfigurationConverter the {@link Converter} used for converting to an
119+
* {@link OidcProviderConfiguration}
120+
*/
121+
public final void setProviderConfigurationConverter(Converter<Map<String, Object>, OidcProviderConfiguration> providerConfigurationConverter) {
122+
Assert.notNull(providerConfigurationConverter, "providerConfigurationConverter cannot be null");
123+
this.providerConfigurationConverter = providerConfigurationConverter;
124+
}
125+
126+
private static final class OidcProviderConfigurationConverter implements Converter<Map<String, Object>, OidcProviderConfiguration> {
127+
private static final ClaimConversionService CLAIM_CONVERSION_SERVICE = ClaimConversionService.getSharedInstance();
128+
private static final TypeDescriptor OBJECT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Object.class);
129+
private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
130+
private static final TypeDescriptor URL_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(URL.class);
131+
private final ClaimTypeConverter claimTypeConverter;
132+
133+
OidcProviderConfigurationConverter() {
134+
CLAIM_CONVERSION_SERVICE.addConverter(new ObjectToSetStringConverter2());
135+
Map<String, Converter<Object, ?>> claimNameToConverter = new HashMap<>();
136+
Converter<Object, ?> setStringConverter = getConverter(TypeDescriptor.collection(Set.class, STRING_TYPE_DESCRIPTOR));
137+
Converter<Object, ?> urlConverter = getConverter(URL_TYPE_DESCRIPTOR);
138+
139+
claimNameToConverter.put(OidcProviderMetadataClaimNames.ISSUER, urlConverter);
140+
claimNameToConverter.put(OidcProviderMetadataClaimNames.AUTHORIZATION_ENDPOINT, urlConverter);
141+
claimNameToConverter.put(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT, urlConverter);
142+
claimNameToConverter.put(OidcProviderMetadataClaimNames.JWKS_URI, urlConverter);
143+
claimNameToConverter.put(OidcProviderMetadataClaimNames.GRANT_TYPES_SUPPORTED, setStringConverter);
144+
claimNameToConverter.put(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, setStringConverter);
145+
claimNameToConverter.put(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED, setStringConverter);
146+
claimNameToConverter.put(OidcProviderMetadataClaimNames.SCOPES_SUPPORTED, setStringConverter);
147+
claimNameToConverter.put(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, setStringConverter);
148+
this.claimTypeConverter = new ClaimTypeConverter(claimNameToConverter);
149+
}
150+
151+
@Override
152+
public OidcProviderConfiguration convert(Map<String, Object> source) {
153+
Map<String, Object> parsedClaims = this.claimTypeConverter.convert(source);
154+
return OidcProviderConfiguration.withClaims(parsedClaims).build();
155+
}
156+
157+
private static Converter<Object, ?> getConverter(TypeDescriptor targetDescriptor) {
158+
return (source) -> CLAIM_CONVERSION_SERVICE.convert(source, OBJECT_TYPE_DESCRIPTOR, targetDescriptor);
159+
}
160+
}
161+
}

0 commit comments

Comments
 (0)