Skip to content

Commit 00bec01

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 df8793c commit 00bec01

File tree

16 files changed

+1570
-5
lines changed

16 files changed

+1570
-5
lines changed

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

Lines changed: 43 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.OidcProviderConfigurationFilter;
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+
"/.well-known/openid-configuration", HttpMethod.GET.name());
8387

8488
/**
8589
* Sets the repository of registered clients.
@@ -117,14 +121,27 @@ public OAuth2AuthorizationServerConfigurer<B> keyManager(KeyManager keyManager)
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, oidcProviderMetadataEndpointMatcher);
128145
}
129146

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

173190
@Override
174191
public void configure(B builder) {
175-
JwkSetEndpointFilter jwkSetEndpointFilter = new JwkSetEndpointFilter(getKeyManager(builder));
192+
OidcProviderConfigurationFilter oidcProviderConfigurationFilter = new OidcProviderConfigurationFilter(getProviderSettings(builder));
193+
builder.addFilterBefore(postProcess(oidcProviderConfigurationFilter), AbstractPreAuthenticatedProcessingFilter.class);
194+
195+
JwkSetEndpointFilter jwkSetEndpointFilter = new JwkSetEndpointFilter(
196+
getKeyManager(builder),
197+
getProviderSettings(builder).jwkSetEndpoint());
176198
builder.addFilterBefore(postProcess(jwkSetEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
177199

178200
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
@@ -186,18 +208,21 @@ public void configure(B builder) {
186208
OAuth2AuthorizationEndpointFilter authorizationEndpointFilter =
187209
new OAuth2AuthorizationEndpointFilter(
188210
getRegisteredClientRepository(builder),
189-
getAuthorizationService(builder));
211+
getAuthorizationService(builder),
212+
getProviderSettings(builder).authorizationEndpoint());
190213
builder.addFilterBefore(postProcess(authorizationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
191214

192215
OAuth2TokenEndpointFilter tokenEndpointFilter =
193216
new OAuth2TokenEndpointFilter(
194217
authenticationManager,
195-
getAuthorizationService(builder));
218+
getAuthorizationService(builder),
219+
getProviderSettings(builder).tokenEndpoint());
196220
builder.addFilterAfter(postProcess(tokenEndpointFilter), FilterSecurityInterceptor.class);
197221

198222
OAuth2TokenRevocationEndpointFilter tokenRevocationEndpointFilter =
199223
new OAuth2TokenRevocationEndpointFilter(
200-
authenticationManager);
224+
authenticationManager,
225+
getProviderSettings(builder).tokenRevocationEndpoint());
201226
builder.addFilterAfter(postProcess(tokenRevocationEndpointFilter), OAuth2TokenEndpointFilter.class);
202227
}
203228

@@ -249,4 +274,17 @@ private static <B extends HttpSecurityBuilder<B>> KeyManager getKeyManager(B bui
249274
private static <B extends HttpSecurityBuilder<B>> KeyManager getKeyManagerBean(B builder) {
250275
return builder.getSharedObject(ApplicationContext.class).getBean(KeyManager.class);
251276
}
277+
278+
private static <B extends HttpSecurityBuilder<B>> ProviderSettings getProviderSettings(B builder) {
279+
ProviderSettings providerSettings = builder.getSharedObject(ProviderSettings.class);
280+
if (providerSettings == null) {
281+
providerSettings = getProviderSettingsBean(builder);
282+
builder.setSharedObject(ProviderSettings.class, providerSettings);
283+
}
284+
return providerSettings;
285+
}
286+
287+
private static <B extends HttpSecurityBuilder<B>> ProviderSettings getProviderSettingsBean(B builder) {
288+
return builder.getSharedObject(ApplicationContext.class).getBean(ProviderSettings.class);
289+
}
252290
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
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.endpoint.OidcProviderConfiguration;
33+
import org.springframework.util.Assert;
34+
35+
import java.io.IOException;
36+
import java.net.URI;
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> oidcProviderConfigurationConverter = new ProviderConfigurationConverter();
59+
private Converter<OidcProviderConfiguration, Map<String, Object>> oidcProviderConfigurationParametersConverter = 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.oidcProviderConfigurationConverter.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.oidcProviderConfigurationParametersConverter.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.oidcProviderConfigurationParametersConverter = providerConfigurationParametersConverter;
111+
}
112+
113+
/**
114+
* Sets the {@link Converter} used for converting the OpenID Provider Configuration parameters
115+
* to a {@link OidcProviderConfiguration}.
116+
*
117+
* @param providerConfigurationConverter the {@link Converter} used for converting to a
118+
* {@link OidcProviderConfiguration}
119+
*/
120+
public void setProviderConfigurationConverter(Converter<Map<String, Object>, OidcProviderConfiguration> providerConfigurationConverter) {
121+
Assert.notNull(providerConfigurationConverter, "providerConfigurationConverter cannot be null");
122+
this.oidcProviderConfigurationConverter = providerConfigurationConverter;
123+
}
124+
125+
private static final class ProviderConfigurationConverter 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 final ClaimTypeConverter claimTypeConverter;
130+
131+
ProviderConfigurationConverter() {
132+
Map<String, Converter<Object, ?>> claimNameToConverter = new HashMap<>();
133+
Converter<Object, ?> collectionStringConverter = getConverter(
134+
TypeDescriptor.collection(Collection.class, STRING_TYPE_DESCRIPTOR));
135+
claimNameToConverter.put(OidcProviderMetadataClaimNames.ISSUER, ProviderConfigurationConverter::convertUri);
136+
claimNameToConverter.put(OidcProviderMetadataClaimNames.AUTHORIZATION_ENDPOINT, ProviderConfigurationConverter::convertUri);
137+
claimNameToConverter.put(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT, ProviderConfigurationConverter::convertUri);
138+
claimNameToConverter.put(OidcProviderMetadataClaimNames.JWKS_URI, ProviderConfigurationConverter::convertUri);
139+
claimNameToConverter.put(OidcProviderMetadataClaimNames.GRANT_TYPES_SUPPORTED, collectionStringConverter);
140+
claimNameToConverter.put(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, collectionStringConverter);
141+
claimNameToConverter.put(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED, collectionStringConverter);
142+
claimNameToConverter.put(OidcProviderMetadataClaimNames.SCOPES_SUPPORTED, collectionStringConverter);
143+
claimNameToConverter.put(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, collectionStringConverter);
144+
this.claimTypeConverter = new ClaimTypeConverter(claimNameToConverter);
145+
}
146+
147+
@Override
148+
public OidcProviderConfiguration convert(Map<String, Object> source) {
149+
Map<String, Object> parsedClaims = claimTypeConverter.convert(source);
150+
return new OidcProviderConfiguration.Builder(parsedClaims).build();
151+
}
152+
153+
private static URI convertUri(Object source) {
154+
if (source == null) {
155+
return null;
156+
}
157+
try {
158+
return new URI(source.toString());
159+
}
160+
catch (Exception ex) {
161+
throw new IllegalStateException("Could not coerce " + source + " into a URI String", ex);
162+
}
163+
}
164+
165+
private static Converter<Object, ?> getConverter(TypeDescriptor targetDescriptor) {
166+
return (source) -> CLAIM_CONVERSION_SERVICE.convert(source, OBJECT_TYPE_DESCRIPTOR, targetDescriptor);
167+
}
168+
}
169+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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.oidc;
17+
18+
19+
import org.springframework.security.oauth2.core.ClaimAccessor;
20+
import org.springframework.security.oauth2.core.oidc.endpoint.OidcProviderConfiguration;
21+
22+
import java.net.URL;
23+
import java.util.List;
24+
25+
/**
26+
* A {@link ClaimAccessor} for the "claims" that can be returned
27+
* in the OpenID Provider Configuration Response.
28+
*
29+
* @author Daniel Garnier-Moiroux
30+
* @since 0.1.0
31+
* @see ClaimAccessor
32+
* @see OidcProviderMetadataClaimNames
33+
* @see OidcProviderConfiguration
34+
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata">OpenID Connect Discovery 1.0</a>
35+
*/
36+
public interface OidcProviderMetadataClaimAccessor extends ClaimAccessor {
37+
38+
/**
39+
* Returns the Issuer identifier {@code (issuer)}.
40+
*
41+
* @return the Issuer identifier
42+
*/
43+
default URL getIssuer() {
44+
return this.getClaimAsURL(OidcProviderMetadataClaimNames.ISSUER);
45+
}
46+
47+
/**
48+
* Returns the URL of the OAuth 2.0 Authorization Endpoint {@code (authorization_endpoint)}.
49+
*
50+
* @return the URL of the OAuth 2.0 Authorization Endpoint
51+
*/
52+
default URL getAuthorizationEndpoint() {
53+
return this.getClaimAsURL(OidcProviderMetadataClaimNames.AUTHORIZATION_ENDPOINT);
54+
}
55+
56+
/**
57+
* Returns the URL of the OAuth 2.0 Token Endpoint {@code (token_endpoint)}.
58+
*
59+
* @return the URL of the OAuth 2.0 Token Endpoint
60+
*/
61+
default URL getTokenEndpoint() {
62+
return this.getClaimAsURL(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT);
63+
}
64+
65+
/**
66+
* Returns the client authentication methods supported by the OAuth 2.0 Token Endpoint {@code (token_endpoint_auth_methods_supported)}.
67+
*
68+
* @return the client authentication methods supported by the OAuth 2.0 Token Endpoint
69+
*/
70+
default List<String> getTokenEndpointAuthenticationMethods() {
71+
return this.getClaimAsStringList(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED);
72+
}
73+
74+
/**
75+
* Returns the URL of the JSON Web Key Set {@code (jwks_uri)}.
76+
*
77+
* @return the URL of the JSON Web Key Set
78+
*/
79+
default URL getJwksUri() {
80+
return this.getClaimAsURL(OidcProviderMetadataClaimNames.JWKS_URI);
81+
}
82+
83+
/**
84+
* Returns the OAuth 2.0 {@code response_type} values supported {@code (response_types_supported)}.
85+
*
86+
* @return the OAuth 2.0 {@code response_type} values supported
87+
*/
88+
default List<String> getResponseTypes() {
89+
return this.getClaimAsStringList(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED);
90+
}
91+
92+
/**
93+
* Returns the OAuth 2.0 {@code grant_type} values supported {@code (grant_types_supported)}.
94+
*
95+
* @return the OAuth 2.0 {@code grant_type} values supported
96+
*/
97+
default List<String> getGrantTypes() {
98+
return this.getClaimAsStringList(OidcProviderMetadataClaimNames.GRANT_TYPES_SUPPORTED);
99+
}
100+
101+
/**
102+
* Returns the Subject Identifier types supported {@code (subject_types_supported)}.
103+
*
104+
* @return the Subject Identifier types supported
105+
*/
106+
default List<String> getSubjectTypes() {
107+
return this.getClaimAsStringList(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED);
108+
}
109+
110+
/**
111+
* Returns the OAuth 2.0 {@code scope} values supported {@code (scopes_supported)}.
112+
*
113+
* @return the OAuth 2.0 {@code scope} values supported
114+
*/
115+
default List<String> getScopes() {
116+
return this.getClaimAsStringList(OidcProviderMetadataClaimNames.SCOPES_SUPPORTED);
117+
}
118+
119+
}

0 commit comments

Comments
 (0)