Skip to content

Commit 7bd990c

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 c1e9c1d commit 7bd990c

File tree

16 files changed

+1892
-8
lines changed

16 files changed

+1892
-8
lines changed

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

Lines changed: 55 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.access.intercept.FilterSecurityInterceptor;
4345
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
4446
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
@@ -80,6 +82,8 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
8082
OAuth2TokenRevocationEndpointFilter.DEFAULT_TOKEN_REVOCATION_ENDPOINT_URI, HttpMethod.POST.name());
8183
private final RequestMatcher jwkSetEndpointMatcher = new AntPathRequestMatcher(
8284
JwkSetEndpointFilter.DEFAULT_JWK_SET_ENDPOINT_URI, HttpMethod.GET.name());
85+
private final RequestMatcher oidcProviderMetadataEndpointMatcher = new AntPathRequestMatcher(
86+
OidcProviderConfigurationEndpointFilter.DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI, HttpMethod.GET.name());
8387

8488
/**
8589
* Sets the repository of registered clients.
@@ -117,14 +121,27 @@ public OAuth2AuthorizationServerConfigurer<B> keySource(CryptoKeySource keySourc
117121
return this;
118122
}
119123

124+
/**
125+
* Sets the provider settings.
126+
*
127+
* @param providerSettings the provider settings
128+
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
129+
*/
130+
public OAuth2AuthorizationServerConfigurer<B> providerSettings(ProviderSettings providerSettings) {
131+
Assert.notNull(providerSettings, "providerSettings cannot be null");
132+
this.getBuilder().setSharedObject(ProviderSettings.class, providerSettings);
133+
return this;
134+
}
135+
120136
/**
121137
* Returns a {@code List} of {@link RequestMatcher}'s for the authorization server endpoints.
122138
*
123139
* @return a {@code List} of {@link RequestMatcher}'s for the authorization server endpoints
124140
*/
125141
public List<RequestMatcher> getEndpointMatchers() {
142+
// TODO: use ProviderSettings instead
126143
return Arrays.asList(this.authorizationEndpointMatcher, this.tokenEndpointMatcher,
127-
this.tokenRevocationEndpointMatcher, this.jwkSetEndpointMatcher);
144+
this.tokenRevocationEndpointMatcher, this.jwkSetEndpointMatcher, this.oidcProviderMetadataEndpointMatcher);
128145
}
129146

130147
@Override
@@ -172,7 +189,14 @@ public void init(B builder) {
172189

173190
@Override
174191
public void configure(B builder) {
175-
JwkSetEndpointFilter jwkSetEndpointFilter = new JwkSetEndpointFilter(getKeySource(builder));
192+
if (getProviderSettings(builder).issuer() != null) {
193+
OidcProviderConfigurationEndpointFilter oidcProviderConfigurationEndpointFilter = new OidcProviderConfigurationEndpointFilter(getProviderSettings(builder));
194+
builder.addFilterBefore(postProcess(oidcProviderConfigurationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
195+
}
196+
197+
JwkSetEndpointFilter jwkSetEndpointFilter = new JwkSetEndpointFilter(
198+
getKeySource(builder),
199+
getProviderSettings(builder).jwkSetEndpoint());
176200
builder.addFilterBefore(postProcess(jwkSetEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
177201

178202
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
@@ -186,18 +210,21 @@ public void configure(B builder) {
186210
OAuth2AuthorizationEndpointFilter authorizationEndpointFilter =
187211
new OAuth2AuthorizationEndpointFilter(
188212
getRegisteredClientRepository(builder),
189-
getAuthorizationService(builder));
213+
getAuthorizationService(builder),
214+
getProviderSettings(builder).authorizationEndpoint());
190215
builder.addFilterBefore(postProcess(authorizationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
191216

192217
OAuth2TokenEndpointFilter tokenEndpointFilter =
193218
new OAuth2TokenEndpointFilter(
194219
authenticationManager,
195-
getAuthorizationService(builder));
220+
getAuthorizationService(builder),
221+
getProviderSettings(builder).tokenEndpoint());
196222
builder.addFilterAfter(postProcess(tokenEndpointFilter), FilterSecurityInterceptor.class);
197223

198224
OAuth2TokenRevocationEndpointFilter tokenRevocationEndpointFilter =
199225
new OAuth2TokenRevocationEndpointFilter(
200-
authenticationManager);
226+
authenticationManager,
227+
getProviderSettings(builder).tokenRevocationEndpoint());
201228
builder.addFilterAfter(postProcess(tokenRevocationEndpointFilter), OAuth2TokenEndpointFilter.class);
202229
}
203230

@@ -249,4 +276,27 @@ private static <B extends HttpSecurityBuilder<B>> CryptoKeySource getKeySource(B
249276
private static <B extends HttpSecurityBuilder<B>> CryptoKeySource getKeySourceBean(B builder) {
250277
return builder.getSharedObject(ApplicationContext.class).getBean(CryptoKeySource.class);
251278
}
279+
280+
private static <B extends HttpSecurityBuilder<B>> ProviderSettings getProviderSettings(B builder) {
281+
ProviderSettings providerSettings = builder.getSharedObject(ProviderSettings.class);
282+
if (providerSettings == null) {
283+
providerSettings = getProviderSettingsBean(builder);
284+
if (providerSettings == null) {
285+
providerSettings = new ProviderSettings();
286+
}
287+
builder.setSharedObject(ProviderSettings.class, providerSettings);
288+
}
289+
return providerSettings;
290+
}
291+
292+
private static <B extends HttpSecurityBuilder<B>> ProviderSettings getProviderSettingsBean(B builder) {
293+
Map<String, ProviderSettings> providerSettingsMap = BeanFactoryUtils.beansOfTypeIncludingAncestors(
294+
builder.getSharedObject(ApplicationContext.class), ProviderSettings.class);
295+
if (providerSettingsMap.size() > 1) {
296+
throw new NoUniqueBeanDefinitionException(ProviderSettings.class, providerSettingsMap.size(),
297+
"Expected single matching bean of type '" + ProviderSettings.class.getName() + "' but found " +
298+
providerSettingsMap.size() + ": " + StringUtils.collectionToCommaDelimitedString(providerSettingsMap.keySet()));
299+
}
300+
return (!providerSettingsMap.isEmpty() ? providerSettingsMap.values().iterator().next() : null);
301+
}
252302
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
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.oidc.OidcProviderMetadataClaimNames;
32+
import org.springframework.security.oauth2.core.oidc.OidcProviderConfiguration;
33+
import org.springframework.util.Assert;
34+
35+
import java.io.IOException;
36+
import java.net.URL;
37+
import java.util.Collection;
38+
import java.util.HashMap;
39+
import java.util.Map;
40+
41+
42+
/**
43+
* A {@link HttpMessageConverter} for an {@link OidcProviderConfiguration OpenID Provider Configuration Metadata}.
44+
*
45+
* @author Daniel Garnier-Moiroux
46+
* @since 0.1.0
47+
* @see AbstractHttpMessageConverter
48+
* @see OidcProviderConfiguration
49+
*/
50+
public class OidcProviderConfigurationHttpMessageConverter
51+
extends AbstractHttpMessageConverter<OidcProviderConfiguration> {
52+
private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP =
53+
new ParameterizedTypeReference<Map<String, Object>>() {
54+
};
55+
56+
private final GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter();
57+
58+
private Converter<Map<String, Object>, OidcProviderConfiguration> providerConfigurationConverter = new OidcProviderConfigurationConverter();
59+
private Converter<OidcProviderConfiguration, Map<String, Object>> providerConfigurationParametersConverter = OidcProviderConfiguration::getClaims;
60+
61+
public OidcProviderConfigurationHttpMessageConverter() {
62+
super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
63+
}
64+
65+
@Override
66+
protected boolean supports(Class<?> clazz) {
67+
return OidcProviderConfiguration.class.isAssignableFrom(clazz);
68+
}
69+
70+
@Override
71+
@SuppressWarnings("unchecked")
72+
protected OidcProviderConfiguration readInternal(Class<? extends OidcProviderConfiguration> clazz, HttpInputMessage inputMessage)
73+
throws HttpMessageNotReadableException {
74+
try {
75+
Map<String, Object> providerConfigurationParameters = (Map<String, Object>) this.jsonMessageConverter.read(STRING_OBJECT_MAP.getType(), null, inputMessage);
76+
return this.providerConfigurationConverter.convert(providerConfigurationParameters);
77+
} catch (Exception ex) {
78+
throw new HttpMessageNotReadableException(
79+
"An error occurred reading the OpenID Provider Configuration: " + ex.getMessage(), ex, inputMessage);
80+
}
81+
}
82+
83+
@Override
84+
protected void writeInternal(OidcProviderConfiguration providerConfiguration, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
85+
try {
86+
Map<String, Object> providerConfigurationResponseParameters =
87+
this.providerConfigurationParametersConverter.convert(providerConfiguration);
88+
this.jsonMessageConverter.write(
89+
providerConfigurationResponseParameters,
90+
STRING_OBJECT_MAP.getType(),
91+
MediaType.APPLICATION_JSON,
92+
outputMessage
93+
);
94+
} catch (Exception ex) {
95+
throw new HttpMessageNotWritableException(
96+
"An error occurred writing the OpenID Provider Configuration: " + ex.getMessage(), ex);
97+
}
98+
}
99+
100+
/**
101+
* Sets the {@link Converter} used for converting the {@link OidcProviderConfiguration} to a
102+
* {@code Map} representation of the OpenID Provider Configuration.
103+
*
104+
* @param providerConfigurationParametersConverter the {@link Converter} used for converting to a
105+
* {@code Map} representation of the OpenID Provider Configuration
106+
*/
107+
public final void setProviderConfigurationParametersConverter(
108+
Converter<OidcProviderConfiguration, Map<String, Object>> providerConfigurationParametersConverter) {
109+
Assert.notNull(providerConfigurationParametersConverter, "providerConfigurationParametersConverter cannot be null");
110+
this.providerConfigurationParametersConverter = providerConfigurationParametersConverter;
111+
}
112+
113+
/**
114+
* Sets the {@link Converter} used for converting the OpenID Provider Configuration parameters
115+
* to an {@link OidcProviderConfiguration}.
116+
*
117+
* @param providerConfigurationConverter the {@link Converter} used for converting to an
118+
* {@link OidcProviderConfiguration}
119+
*/
120+
public final void setProviderConfigurationConverter(Converter<Map<String, Object>, OidcProviderConfiguration> providerConfigurationConverter) {
121+
Assert.notNull(providerConfigurationConverter, "providerConfigurationConverter cannot be null");
122+
this.providerConfigurationConverter = providerConfigurationConverter;
123+
}
124+
125+
private static final class OidcProviderConfigurationConverter implements Converter<Map<String, Object>, OidcProviderConfiguration> {
126+
private static final ClaimConversionService CLAIM_CONVERSION_SERVICE = ClaimConversionService.getSharedInstance();
127+
private static final TypeDescriptor OBJECT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Object.class);
128+
private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
129+
private static final TypeDescriptor URL_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(URL.class);
130+
private final ClaimTypeConverter claimTypeConverter;
131+
132+
OidcProviderConfigurationConverter() {
133+
Map<String, Converter<Object, ?>> claimNameToConverter = new HashMap<>();
134+
Converter<Object, ?> collectionStringConverter = getConverter(
135+
TypeDescriptor.collection(Collection.class, STRING_TYPE_DESCRIPTOR));
136+
Converter<Object, ?> urlConverter = getConverter(URL_TYPE_DESCRIPTOR);
137+
138+
claimNameToConverter.put(OidcProviderMetadataClaimNames.ISSUER, urlConverter);
139+
claimNameToConverter.put(OidcProviderMetadataClaimNames.AUTHORIZATION_ENDPOINT, urlConverter);
140+
claimNameToConverter.put(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT, urlConverter);
141+
claimNameToConverter.put(OidcProviderMetadataClaimNames.JWKS_URI, urlConverter);
142+
claimNameToConverter.put(OidcProviderMetadataClaimNames.GRANT_TYPES_SUPPORTED, collectionStringConverter);
143+
claimNameToConverter.put(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, collectionStringConverter);
144+
claimNameToConverter.put(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED, collectionStringConverter);
145+
claimNameToConverter.put(OidcProviderMetadataClaimNames.SCOPES_SUPPORTED, collectionStringConverter);
146+
claimNameToConverter.put(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, collectionStringConverter);
147+
this.claimTypeConverter = new ClaimTypeConverter(claimNameToConverter);
148+
}
149+
150+
@Override
151+
public OidcProviderConfiguration convert(Map<String, Object> source) {
152+
Map<String, Object> parsedClaims = this.claimTypeConverter.convert(source);
153+
return OidcProviderConfiguration.withClaims(parsedClaims).build();
154+
}
155+
156+
private static Converter<Object, ?> getConverter(TypeDescriptor targetDescriptor) {
157+
return (source) -> CLAIM_CONVERSION_SERVICE.convert(source, OBJECT_TYPE_DESCRIPTOR, targetDescriptor);
158+
}
159+
}
160+
}

0 commit comments

Comments
 (0)