Skip to content

Commit 086fe63

Browse files
committed
Implement OAuth 2.0 Server Metadata (RFC 8414)
See See https://tools.ietf.org/html/rfc8414 Closes gh-54
1 parent f97b8b2 commit 086fe63

File tree

28 files changed

+2276
-445
lines changed

28 files changed

+2276
-445
lines changed

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
3838
import org.springframework.security.oauth2.server.authorization.web.JwkSetEndpointFilter;
3939
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
40+
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationServerMetadataEndpointFilter;
4041
import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientAuthenticationFilter;
4142
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
4243
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenRevocationEndpointFilter;
@@ -94,6 +95,8 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
9495
JwkSetEndpointFilter.DEFAULT_JWK_SET_ENDPOINT_URI, HttpMethod.GET.name());
9596
private final RequestMatcher oidcProviderConfigurationEndpointMatcher = new AntPathRequestMatcher(
9697
OidcProviderConfigurationEndpointFilter.DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI, HttpMethod.GET.name());
98+
private final RequestMatcher oauth2ServerMetadataEndpointMatcher = new AntPathRequestMatcher(
99+
OAuth2AuthorizationServerMetadataEndpointFilter.DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI, HttpMethod.GET.name());
97100

98101
/**
99102
* Sets the repository of registered clients.
@@ -150,9 +153,13 @@ public OAuth2AuthorizationServerConfigurer<B> providerSettings(ProviderSettings
150153
*/
151154
public List<RequestMatcher> getEndpointMatchers() {
152155
// TODO Initialize matchers using URI's from ProviderSettings
153-
return Arrays.asList(this.authorizationEndpointMatcher, this.tokenEndpointMatcher,
154-
this.tokenRevocationEndpointMatcher, this.jwkSetEndpointMatcher,
155-
this.oidcProviderConfigurationEndpointMatcher);
156+
return Arrays.asList(
157+
this.authorizationEndpointMatcher,
158+
this.tokenEndpointMatcher,
159+
this.tokenRevocationEndpointMatcher,
160+
this.jwkSetEndpointMatcher,
161+
this.oidcProviderConfigurationEndpointMatcher,
162+
this.oauth2ServerMetadataEndpointMatcher);
156163
}
157164

158165
@Override
@@ -217,6 +224,9 @@ public void configure(B builder) {
217224
OidcProviderConfigurationEndpointFilter oidcProviderConfigurationEndpointFilter =
218225
new OidcProviderConfigurationEndpointFilter(providerSettings);
219226
builder.addFilterBefore(postProcess(oidcProviderConfigurationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
227+
228+
OAuth2AuthorizationServerMetadataEndpointFilter authorizationServerMetadataFilter = new OAuth2AuthorizationServerMetadataEndpointFilter(providerSettings);
229+
builder.addFilterBefore(postProcess(authorizationServerMetadataFilter), AbstractPreAuthenticatedProcessingFilter.class);
220230
}
221231

222232
JwkSetEndpointFilter jwkSetEndpointFilter = new JwkSetEndpointFilter(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
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;
17+
18+
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
19+
import org.springframework.security.oauth2.core.oidc.OidcScopes;
20+
import org.springframework.security.oauth2.server.authorization.Version;
21+
import org.springframework.util.Assert;
22+
23+
import java.io.Serializable;
24+
import java.net.URI;
25+
import java.net.URL;
26+
import java.util.Arrays;
27+
import java.util.Collection;
28+
import java.util.Collections;
29+
import java.util.LinkedHashMap;
30+
import java.util.LinkedList;
31+
import java.util.List;
32+
import java.util.Map;
33+
import java.util.Set;
34+
import java.util.function.Consumer;
35+
import java.util.stream.Collectors;
36+
37+
38+
/**
39+
* A base representation of an authorization server Provider Configuration response,
40+
* returned by an endpoint defined either in OpenID Connect Discovery 1.0 or
41+
* OAuth 2.0 Authorization Server Metadata.
42+
* It contains a set of claims about the Provider's configuration.
43+
*
44+
* @author Daniel Garnier-Moiroux
45+
* @since 0.1.0
46+
* @see AuthorizationServerMetadataClaimAccessor
47+
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse">4.2. OpenID Provider Configuration Response</a>
48+
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc8414#section-3.2">3.2. Authorization Server Metadata Response</a>
49+
*/
50+
public abstract class AbstractAuthorizationServerMetadata implements AuthorizationServerMetadataClaimAccessor, Serializable {
51+
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
52+
53+
protected final Map<String, Object> claims;
54+
55+
protected AbstractAuthorizationServerMetadata(Map<String, Object> claims) {
56+
Assert.notEmpty(claims, "claims cannot be empty");
57+
this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims));
58+
}
59+
60+
/**
61+
* Returns the Authorization Server metadata.
62+
*
63+
* @return a {@code Map} of the metadata values
64+
*/
65+
@Override
66+
public Map<String, Object> getClaims() {
67+
return this.claims;
68+
}
69+
70+
protected static Map<String, Object> defaultClaims() {
71+
LinkedHashMap<String, Object> claims = new LinkedHashMap<>();
72+
claims.put(
73+
AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED,
74+
Arrays.asList(
75+
"client_secret_basic", // TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_BASIC in Spring Security 5.5.0
76+
"client_secret_post" // TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_POST in Spring Security 5.5.0
77+
)
78+
);
79+
claims.put(
80+
AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED,
81+
Arrays.asList(
82+
AuthorizationGrantType.AUTHORIZATION_CODE.getValue(),
83+
AuthorizationGrantType.CLIENT_CREDENTIALS.getValue(),
84+
AuthorizationGrantType.REFRESH_TOKEN.getValue()
85+
)
86+
);
87+
claims.put(
88+
AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED,
89+
Collections.singletonList(OAuth2AuthorizationResponseType.CODE.getValue())
90+
);
91+
claims.put(
92+
AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED,
93+
Collections.singletonList(OidcScopes.OPENID)
94+
);
95+
return claims;
96+
}
97+
98+
/**
99+
* An abstract builder for subclasses of {@link AbstractAuthorizationServerMetadata}.
100+
*/
101+
public static abstract class AbstractBuilder<T extends AbstractAuthorizationServerMetadata, B extends AbstractBuilder<T, B>> {
102+
103+
protected final Map<String, Object> claims = new LinkedHashMap<>();
104+
105+
protected AbstractBuilder() { }
106+
107+
protected abstract B getThis(); // avoid unchecked casts in builder when doing "return (T) this;"
108+
109+
/**
110+
* Use this {@code issuer} in the resulting {@link AbstractAuthorizationServerMetadata}, REQUIRED.
111+
*
112+
* @param issuer the URL of the Authorization Server's Issuer Identifier
113+
* @return the {@link AbstractBuilder} for further configuration
114+
*/
115+
public B issuer(String issuer) {
116+
return claim(AuthorizationServerMetadataClaimNames.ISSUER, issuer);
117+
}
118+
119+
/**
120+
* Use this {@code authorization_endpoint} in the resulting {@link AbstractAuthorizationServerMetadata}, REQUIRED.
121+
*
122+
* @param authorizationEndpoint the URL of the Authorization Server's OAuth 2.0 Authorization Endpoint
123+
* @return the {@link AbstractBuilder} for further configuration
124+
*/
125+
public B authorizationEndpoint(String authorizationEndpoint) {
126+
return claim(AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, authorizationEndpoint);
127+
}
128+
129+
/**
130+
* Use this {@code token_endpoint} in the resulting {@link AbstractAuthorizationServerMetadata}, REQUIRED.
131+
*
132+
* @param tokenEndpoint the URL of the Authorization Server's OAuth 2.0 Token Endpoint
133+
* @return the {@link AbstractBuilder} for further configuration
134+
*/
135+
public B tokenEndpoint(String tokenEndpoint) {
136+
return claim(AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, tokenEndpoint);
137+
}
138+
139+
/**
140+
* Add this Authentication Method to the collection of {@code token_endpoint_auth_methods_supported}
141+
* in the resulting {@link AbstractAuthorizationServerMetadata}, OPTIONAL.
142+
*
143+
* @param authenticationMethod the OAuth 2.0 Authentication Method supported by the Token Endpoint
144+
* @return the {@link AbstractBuilder} for further configuration
145+
*/
146+
public B tokenEndpointAuthenticationMethod(String authenticationMethod) {
147+
addClaimToClaimList(AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, authenticationMethod);
148+
return getThis();
149+
}
150+
151+
/**
152+
* A {@code Consumer} of the Token Endpoint Authentication Method(s) allowing the ability to add, replace, or remove.
153+
*
154+
* @param authenticationMethodsConsumer a {@code Consumer} of the Token Endpoint Authentication Method(s)
155+
* @return the {@link AbstractBuilder} for further configuration
156+
*/
157+
public B tokenEndpointAuthenticationMethods(Consumer<List<String>> authenticationMethodsConsumer) {
158+
acceptClaimValues(AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, authenticationMethodsConsumer);
159+
return getThis();
160+
}
161+
162+
/**
163+
* Use this {@code jwks_uri} in the resulting {@link AbstractAuthorizationServerMetadata}, REQUIRED.
164+
*
165+
* @param jwkSetUri the URL of the Authorization Server's JSON Web Key Set document
166+
* @return the {@link AbstractBuilder} for further configuration
167+
*/
168+
public B jwkSetUri(String jwkSetUri) {
169+
return claim(AuthorizationServerMetadataClaimNames.JWKS_URI, jwkSetUri);
170+
}
171+
172+
/**
173+
* Add this Response Type to the collection of {@code response_types_supported} in the resulting
174+
* {@link AbstractAuthorizationServerMetadata}.
175+
*
176+
* @param responseType the OAuth 2.0 {@code response_type} value that the Authorization Server supports
177+
* @return the {@link AbstractBuilder} for further configuration
178+
*/
179+
public B responseType(String responseType) {
180+
addClaimToClaimList(AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, responseType);
181+
return getThis();
182+
}
183+
184+
/**
185+
* A {@code Consumer} of the Response Type(s) allowing the ability to add, replace, or remove.
186+
*
187+
* @param responseTypesConsumer a {@code Consumer} of the Response Type(s)
188+
* @return the {@link AbstractBuilder} for further configuration
189+
*/
190+
public B responseTypes(Consumer<List<String>> responseTypesConsumer) {
191+
acceptClaimValues(AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, responseTypesConsumer);
192+
return getThis();
193+
}
194+
195+
/**
196+
* Add this Grant Type to the collection of {@code grant_types_supported} in the resulting
197+
* {@link AbstractAuthorizationServerMetadata}, OPTIONAL.
198+
*
199+
* @param grantType the OAuth 2.0 {@code grant_type} value that the Authorization Server supports
200+
* @return the {@link AbstractBuilder} for further configuration
201+
*/
202+
public B grantType(String grantType) {
203+
addClaimToClaimList(AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, grantType);
204+
return getThis();
205+
}
206+
207+
/**
208+
* A {@code Consumer} of the Grant Type(s) allowing the ability to add, replace, or remove.
209+
*
210+
* @param grantTypesConsumer a {@code Consumer} of the Grant Type(s)
211+
* @return the {@link AbstractBuilder} for further configuration
212+
*/
213+
public B grantTypes(Consumer<List<String>> grantTypesConsumer) {
214+
acceptClaimValues(AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, grantTypesConsumer);
215+
return getThis();
216+
}
217+
218+
/**
219+
* Add this Scope to the collection of {@code scopes_supported} in the resulting
220+
* {@link AbstractAuthorizationServerMetadata}, RECOMMENDED.
221+
*
222+
* @param scope the OAuth 2.0 {@code scope} value that the Authorization Server supports
223+
* @return the {@link AbstractBuilder} for further configuration
224+
*/
225+
public B scope(String scope) {
226+
addClaimToClaimList(AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, scope);
227+
return getThis();
228+
}
229+
230+
/**
231+
* A {@code Consumer} of the Scopes(s) allowing the ability to add, replace, or remove.
232+
*
233+
* @param scopesConsumer a {@code Consumer} of the Scopes(s)
234+
* @return the {@link AbstractBuilder} for further configuration
235+
*/
236+
public B scopes(Consumer<List<String>> scopesConsumer) {
237+
acceptClaimValues(AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, scopesConsumer);
238+
return getThis();
239+
}
240+
241+
/**
242+
* Use this claim in the resulting {@link AbstractAuthorizationServerMetadata}
243+
*
244+
* @param name the claim name
245+
* @param value the claim value
246+
* @return the {@link AbstractBuilder} for further configuration
247+
*/
248+
public B claim(String name, Object value) {
249+
Assert.hasText(name, "name cannot be empty");
250+
Assert.notNull(value, "value cannot be null");
251+
this.claims.put(name, value);
252+
return getThis();
253+
}
254+
255+
/**
256+
* Provides access to every {@link #claim(String, Object)} declared so far with
257+
* the possibility to add, replace, or remove.
258+
*
259+
* @param claimsConsumer a {@code Consumer} of the claims
260+
* @return the {@link AbstractBuilder} for further configurations
261+
*/
262+
public B claims(Consumer<Map<String, Object>> claimsConsumer) {
263+
claimsConsumer.accept(this.claims);
264+
return getThis();
265+
}
266+
267+
/**
268+
* Creates the {@link AbstractAuthorizationServerMetadata}.
269+
*
270+
* @return the {@link AbstractAuthorizationServerMetadata}
271+
*/
272+
public abstract T build();
273+
274+
protected void validateCommonClaims() {
275+
Assert.notNull(this.claims.get(AuthorizationServerMetadataClaimNames.ISSUER), "issuer cannot be null");
276+
validateURL(this.claims.get(AuthorizationServerMetadataClaimNames.ISSUER), "issuer must be a valid URL");
277+
Assert.notNull(this.claims.get(AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT), "authorizationEndpoint cannot be null");
278+
validateURL(this.claims.get(AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT), "authorizationEndpoint must be a valid URL");
279+
Assert.notNull(this.claims.get(AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT), "tokenEndpoint cannot be null");
280+
validateURL(this.claims.get(AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT), "tokenEndpoint must be a valid URL");
281+
Assert.notNull(this.claims.get(AuthorizationServerMetadataClaimNames.JWKS_URI), "jwksUri cannot be null");
282+
validateURL(this.claims.get(AuthorizationServerMetadataClaimNames.JWKS_URI), "jwksUri must be a valid URL");
283+
Assert.notNull(this.claims.get(AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED), "responseTypes cannot be null");
284+
Assert.isInstanceOf(List.class, this.claims.get(AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED), "responseTypes must be of type List");
285+
Assert.notEmpty((List<?>) this.claims.get(AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED), "responseTypes cannot be empty");
286+
}
287+
288+
/**
289+
* Remove claims of type Collection that have a size of zero.
290+
* <p>
291+
* Both <a target="_blank" href="https://tools.ietf.org/html/rfc8414#section-3.2">3.2. Authorization Server Metadata Response</a>
292+
* and <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse">4.2. OpenID Provider Configuration Response</a>
293+
* state "Claims with zero elements MUST be omitted from the response."
294+
*/
295+
protected void removeEmptyClaims() {
296+
Set<String> claimsToRemove = this.claims.entrySet()
297+
.stream()
298+
.filter(entry -> entry.getValue() != null)
299+
.filter(entry -> Collection.class.isAssignableFrom(entry.getValue().getClass()))
300+
.filter(entry -> ((Collection<?>) entry.getValue()).size() == 0)
301+
.map(Map.Entry::getKey)
302+
.collect(Collectors.toSet());
303+
304+
for (String claimToRemove : claimsToRemove) {
305+
this.claims.remove(claimToRemove);
306+
}
307+
}
308+
309+
protected static void validateURL(Object url, String errorMessage) {
310+
if (URL.class.isAssignableFrom(url.getClass())) {
311+
return;
312+
}
313+
314+
try {
315+
new URI(url.toString()).toURL();
316+
} catch (Exception ex) {
317+
throw new IllegalArgumentException(errorMessage, ex);
318+
}
319+
}
320+
321+
@SuppressWarnings("unchecked")
322+
protected void addClaimToClaimList(String name, String value) {
323+
Assert.hasText(name, "name cannot be empty");
324+
Assert.notNull(value, "value cannot be null");
325+
this.claims.computeIfAbsent(name, k -> new LinkedList<String>());
326+
((List<String>) this.claims.get(name)).add(value);
327+
}
328+
329+
@SuppressWarnings("unchecked")
330+
protected void acceptClaimValues(String name, Consumer<List<String>> valuesConsumer) {
331+
Assert.hasText(name, "name cannot be empty");
332+
Assert.notNull(valuesConsumer, "valuesConsumer cannot be null");
333+
this.claims.computeIfAbsent(name, k -> new LinkedList<String>());
334+
List<String> values = (List<String>) this.claims.get(name);
335+
valuesConsumer.accept(values);
336+
}
337+
}
338+
}

0 commit comments

Comments
 (0)