Skip to content

Commit 1739ef8

Browse files
committed
Polish ClientRegistrations, (Reactive)JwtDecoders
Simplifed some of the branching logic in the implementations. Updated the JavaDocs. Simplified some of the test support. Issue: gh-6500
1 parent f5b7706 commit 1739ef8

File tree

6 files changed

+535
-460
lines changed

6 files changed

+535
-460
lines changed

oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java

Lines changed: 89 additions & 169 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,31 @@
1616

1717
package org.springframework.security.oauth2.client.registration;
1818

19+
import java.net.URI;
20+
import java.util.Collections;
21+
import java.util.LinkedHashMap;
22+
import java.util.List;
23+
import java.util.Map;
24+
import java.util.Optional;
25+
1926
import com.nimbusds.oauth2.sdk.GrantType;
2027
import com.nimbusds.oauth2.sdk.ParseException;
2128
import com.nimbusds.oauth2.sdk.Scope;
2229
import com.nimbusds.oauth2.sdk.as.AuthorizationServerMetadata;
2330
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
31+
import net.minidev.json.JSONObject;
32+
33+
import org.springframework.core.ParameterizedTypeReference;
34+
import org.springframework.http.RequestEntity;
2435
import org.springframework.security.oauth2.core.AuthorizationGrantType;
2536
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
2637
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
2738
import org.springframework.security.oauth2.core.oidc.OidcScopes;
2839
import org.springframework.util.Assert;
40+
import org.springframework.web.client.HttpClientErrorException;
2941
import org.springframework.web.client.RestTemplate;
3042
import org.springframework.web.util.UriComponentsBuilder;
3143

32-
import java.net.URI;
33-
import java.util.Collections;
34-
import java.util.HashMap;
35-
import java.util.LinkedHashMap;
36-
import java.util.List;
37-
import java.util.Map;
38-
3944
/**
4045
* Allows creating a {@link ClientRegistration.Builder} from an
4146
* <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig">OpenID Provider Configuration</a>
@@ -49,11 +54,10 @@
4954
*/
5055
public final class ClientRegistrations {
5156
private static final String OIDC_METADATA_PATH = "/.well-known/openid-configuration";
52-
private static final String OAUTH2_METADATA_PATH = "/.well-known/oauth-authorization-server";
53-
54-
enum ProviderType {
55-
OIDCV1, OIDC, OAUTH2;
56-
}
57+
private static final String OAUTH_METADATA_PATH = "/.well-known/oauth-authorization-server";
58+
private static final RestTemplate rest = new RestTemplate();
59+
private static final ParameterizedTypeReference<Map<String, Object>> typeReference =
60+
new ParameterizedTypeReference<Map<String, Object>>() {};
5761

5862
/**
5963
* Creates a {@link ClientRegistration.Builder} using the provided
@@ -63,12 +67,6 @@ enum ProviderType {
6367
* <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse">OpenID
6468
* Provider Configuration Response</a> to initialize the {@link ClientRegistration.Builder}.
6569
*
66-
* When deployed in legacy environments using OpenID Connect Discovery 1.0 and if the provided issuer has
67-
* a path i.e. /issuer1 then as per <a href="https://tools.ietf.org/html/rfc8414#section-5">Compatibility Notes</a>
68-
* first make an <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest">OpenID Provider
69-
* Configuration Request</a> using path /.well-known/openid-configuration/issuer1 and only if the retrieval
70-
* fail then a subsequent request to path /issuer1/.well-known/openid-configuration should be made.
71-
*
7270
* <p>
7371
* For example, if the issuer provided is "https://example.com", then an "OpenID Provider Configuration Request" will
7472
* be made to "https://example.com/.well-known/openid-configuration". The result is expected to be an "OpenID
@@ -88,42 +86,41 @@ enum ProviderType {
8886
* @return a {@link ClientRegistration.Builder} that was initialized by the OpenID Provider Configuration.
8987
*/
9088
public static ClientRegistration.Builder fromOidcIssuerLocation(String issuer) {
91-
Map<ProviderType, String> configuration = getIssuerConfiguration(issuer, OIDC_METADATA_PATH);
92-
OIDCProviderMetadata metadata = parse(configuration.get(ProviderType.OIDCV1), OIDCProviderMetadata::parse);
89+
Assert.hasText(issuer, "issuer cannot be empty");
90+
Map<String, Object> configuration = getConfiguration(issuer, oidc(URI.create(issuer)));
91+
OIDCProviderMetadata metadata = parse(configuration, OIDCProviderMetadata::parse);
9392
return withProviderConfiguration(metadata, issuer)
9493
.userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString());
9594
}
9695

9796
/**
98-
* Unlike <strong>fromOidcIssuerLocation</strong> the <strong>fromIssuerLocation</strong> queries three different endpoints and uses the
99-
* returned response from whichever that returns successfully. When <strong>fromIssuerLocation</strong> is invoked with an issuer
100-
* the following sequence of actions take place
97+
* Creates a {@link ClientRegistration.Builder} using the provided
98+
* <a href="https://openid.net/specs/openid-connect-core-1_0.html#IssuerIdentifier">Issuer</a> by querying
99+
* three different discovery endpoints serially, using the values in the first successful response to
100+
* initialize. If an endpoint returns anything other than a 200 or a 4xx, the method will exit without
101+
* attempting subsequent endpoints.
102+
*
103+
* The three endpoints are computed as follows, given that the {@code issuer} is composed of a {@code host}
104+
* and a {@code path}:
101105
*
102106
* <ol>
103107
* <li>
104-
* The first request is made against <i>{host}/.well-known/openid-configuration/issuer1</i> where issuer is equal to
105-
* <strong>issuer1</strong>. See <a href="https://tools.ietf.org/html/rfc8414#section-5">Compatibility Notes</a> of RFC 8414
106-
* specification for more details.
108+
* {@code host/.well-known/openid-configuration/path}, as defined in
109+
* <a href="https://tools.ietf.org/html/rfc8414#section-5">RFC 8414's Compatibility Notes</a>.
107110
* </li>
108111
* <li>
109-
* If the first attempt request returned non-Success (i.e. 200 status code) response then based on <strong>Compatibility Notes</strong> of
110-
* <strong>RFC 8414</strong> a fallback <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest">
111-
* OpenID Provider Configuration Request</a> is made to <i>{host}/issuer1/.well-known/openid-configuration</i>
112+
* {@code issuer/.well-known/openid-configuration}, as defined in
113+
* <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest">
114+
* OpenID Provider Configuration</a>.
112115
* </li>
113116
* <li>
114-
* If the second attempted request returns a non-Success (i.e. 200 status code) response then based a final
115-
* <a href="https://tools.ietf.org/html/rfc8414#section-3.1">Authorization Server Metadata Request</a> is being made to
116-
* <i>{host}/.well-known/oauth-authorization-server/issuer1</i>.
117+
* {@code host/.well-known/oauth-authorization-server/path}, as defined in
118+
* <a href="https://tools.ietf.org/html/rfc8414#section-3.1">Authorization Server Metadata Request</a>.
117119
* </li>
118120
* </ol>
119121
*
120-
*
121-
* As explained above, <strong>fromIssuerLocation</strong> would behave the exact same way as <strong>fromOidcIssuerLocation</strong> and that is
122-
* because <strong>fromIssuerLocation</strong> does the exact same processing as <strong>fromOidcIssuerLocation</strong> behind the scene. Use of
123-
* <strong>fromIssuerLocation</strong> is encouraged due to the fact that it is well-aligned with RFC 8414 specification and more specifically
124-
* it queries latest OIDC metadata endpoint with a fallback to legacy OIDC v1 discovery endpoint.
125-
*
126-
* The <strong>fromIssuerLocation</strong> is based on <a href="https://tools.ietf.org/html/rfc8414">RFC 8414</a> specification.
122+
* Note that the second endpoint is the equivalent of calling
123+
* {@link ClientRegistrations#fromOidcIssuerLocation(String)}.
127124
*
128125
* <p>
129126
* Example usage:
@@ -136,24 +133,65 @@ public static ClientRegistration.Builder fromOidcIssuerLocation(String issuer) {
136133
* </pre>
137134
*
138135
* @param issuer
139-
* @return a {@link ClientRegistration.Builder} that was initialized by the Authorization Sever Metadata Provider
136+
* @return a {@link ClientRegistration.Builder} that was initialized by one of the described endpoints
140137
*/
141138
public static ClientRegistration.Builder fromIssuerLocation(String issuer) {
142-
Map<ProviderType, String> configuration = getIssuerConfiguration(issuer, OIDC_METADATA_PATH, OAUTH2_METADATA_PATH);
139+
Assert.hasText(issuer, "issuer cannot be empty");
140+
URI uri = URI.create(issuer);
141+
Map<String, Object> configuration = getConfiguration(issuer, oidc(uri), oidcRfc8414(uri), oauth(uri));
142+
AuthorizationServerMetadata metadata = parse(configuration, AuthorizationServerMetadata::parse);
143+
ClientRegistration.Builder builder = withProviderConfiguration(metadata, issuer);
144+
return Optional.ofNullable((String) configuration.get("userinfo_endpoint"))
145+
.map(builder::userInfoUri).orElse(builder);
146+
}
143147

144-
if (configuration.containsKey(ProviderType.OAUTH2)) {
145-
AuthorizationServerMetadata metadata = parse(configuration.get(ProviderType.OAUTH2), AuthorizationServerMetadata::parse);
146-
ClientRegistration.Builder builder = withProviderConfiguration(metadata, issuer);
147-
return builder;
148-
} else {
149-
String response = configuration.getOrDefault(ProviderType.OIDC, configuration.get(ProviderType.OIDCV1));
150-
OIDCProviderMetadata metadata = parse(response, OIDCProviderMetadata::parse);
151-
ClientRegistration.Builder builder = withProviderConfiguration(metadata, issuer)
152-
.userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString());
153-
return builder;
148+
private static URI oidc(URI issuer) {
149+
return UriComponentsBuilder.fromUri(issuer)
150+
.replacePath(issuer.getPath() + OIDC_METADATA_PATH).build(Collections.emptyMap());
151+
}
152+
153+
private static URI oidcRfc8414(URI issuer) {
154+
return UriComponentsBuilder.fromUri(issuer)
155+
.replacePath(OIDC_METADATA_PATH + issuer.getPath()).build(Collections.emptyMap());
156+
}
157+
158+
private static URI oauth(URI issuer) {
159+
return UriComponentsBuilder.fromUri(issuer)
160+
.replacePath(OAUTH_METADATA_PATH + issuer.getPath()).build(Collections.emptyMap());
161+
}
162+
163+
private static Map<String, Object> getConfiguration(String issuer, URI... uris) {
164+
String errorMessage = "Unable to resolve Configuration with the provided Issuer of \"" + issuer + "\"";
165+
for (URI uri : uris) {
166+
try {
167+
RequestEntity<Void> request = RequestEntity.get(uri).build();
168+
return rest.exchange(request, typeReference).getBody();
169+
} catch (HttpClientErrorException e) {
170+
if (!e.getStatusCode().is4xxClientError()) {
171+
throw e;
172+
}
173+
// else try another endpoint
174+
} catch (RuntimeException e) {
175+
throw new IllegalArgumentException(errorMessage, e);
176+
}
177+
}
178+
throw new IllegalArgumentException(errorMessage);
179+
}
180+
181+
private static <T> T parse(Map<String, Object> body,
182+
ThrowingFunction<JSONObject, T, ParseException> parser) {
183+
184+
try {
185+
return parser.apply(new JSONObject(body));
186+
} catch (ParseException e) {
187+
throw new RuntimeException(e);
154188
}
155189
}
156190

191+
private interface ThrowingFunction<S, T, E extends Throwable> {
192+
T apply(S src) throws E;
193+
}
194+
157195
private static ClientRegistration.Builder withProviderConfiguration(AuthorizationServerMetadata metadata, String issuer) {
158196
String metadataIssuer = metadata.getIssuer().getValue();
159197
if (!issuer.equals(metadataIssuer)) {
@@ -185,112 +223,6 @@ private static ClientRegistration.Builder withProviderConfiguration(Authorizatio
185223
.clientName(issuer);
186224
}
187225

188-
/**
189-
* When the length of paths is equal to one (1) then it's a request for OpenId v1 discovery endpoint
190-
* hence the request is made to <strong>{host}/issuer1/.well-known/openid-configuration</strong>.
191-
* Otherwise, all three (3) metadata endpoints are queried one after another.
192-
*
193-
* @param issuer
194-
* @param paths
195-
* @throws IllegalArgumentException if the paths is null or empty or if none of the providers
196-
* responded to given issuer and paths requests
197-
* @return Map<String, Object> - Configuration Metadata from the given issuer
198-
*/
199-
private static Map<ProviderType, String> getIssuerConfiguration(String issuer, String... paths) {
200-
Assert.notEmpty(paths, "paths cannot be empty or null.");
201-
202-
Map<ProviderType, String> providersUrl = buildIssuerConfigurationUrls(issuer, paths);
203-
Map<ProviderType, String> providerResponse = new HashMap<>();
204-
205-
if (providersUrl.containsKey(ProviderType.OIDC)) {
206-
providerResponse = mapResponse(providersUrl, ProviderType.OIDC);
207-
}
208-
209-
// Fallback to OpenId v1 Discovery Endpoint based on RFC 8414 Compatibility Notes
210-
if (providerResponse.isEmpty() && providersUrl.containsKey(ProviderType.OIDCV1)) {
211-
providerResponse = mapResponse(providersUrl, ProviderType.OIDCV1);
212-
}
213-
214-
if (providerResponse.isEmpty() && providersUrl.containsKey(ProviderType.OAUTH2)) {
215-
providerResponse = mapResponse(providersUrl, ProviderType.OAUTH2);
216-
}
217-
218-
if (providerResponse.isEmpty()) {
219-
throw new IllegalArgumentException("Unable to resolve Configuration with the provided Issuer of \"" + issuer + "\"");
220-
}
221-
return providerResponse;
222-
}
223-
224-
private static Map<ProviderType, String> mapResponse(Map<ProviderType, String> providersUrl, ProviderType providerType) {
225-
Map<ProviderType, String> providerResponse = new HashMap<>();
226-
String response = makeIssuerRequest(providersUrl.get(providerType));
227-
if (response != null) {
228-
providerResponse.put(providerType, response);
229-
}
230-
return providerResponse;
231-
}
232-
233-
private static String makeIssuerRequest(String uri) {
234-
RestTemplate rest = new RestTemplate();
235-
try {
236-
return rest.getForObject(uri, String.class);
237-
} catch(RuntimeException ex) {
238-
return null;
239-
}
240-
}
241-
242-
/**
243-
* When invoked with a path then make a
244-
* <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest">
245-
* OpenID Provider Configuration Request</a> by querying the OpenId Connection Discovery 1.0 endpoint
246-
* and the url would look as follow <strong>{host}/issuer1/.well-known/openid-configuration</strong>
247-
*
248-
* <p>
249-
* When more than one path is provided then query all the three (3) endpoints for metadata configuration
250-
* as per <a href="https://tools.ietf.org/html/rfc8414#section-5">Section 5</a> of RF 8414 specification
251-
* and the URLs would look as follow
252-
* </p>
253-
*
254-
* <ol>
255-
* <li>
256-
* <strong>{host}/.well-known/openid-configuration/issuer1</strong> - OpenID as per RFC 8414
257-
* </li>
258-
* <li>
259-
* <strong>{host}/issuer1/.well-known/openid-configuration</strong> - OpenID Connect 1.0 Discovery Compatibility as per RFC 8414
260-
* </li>
261-
* <li>
262-
* <strong>/.well-known/oauth-authorization-server/issuer1</strong> - OAuth2 Authorization Server Metadata as per RFC 8414
263-
* </li>
264-
* </ol>
265-
*
266-
* @param issuer
267-
* @param paths
268-
* @throws IllegalArgumentException throws exception if paths length is not 1 or 3, 1 for <strong>fromOidcLocationIssuer</strong>
269-
* and 3 for the newly introduced <strong>fromIssuerLocation</strong> to support querying 3 different metadata provider endpoints
270-
* @return Map<ProviderType, String> key-value map of provider with its request url
271-
*/
272-
private static Map<ProviderType, String> buildIssuerConfigurationUrls(String issuer, String... paths) {
273-
Assert.isTrue(paths.length != 1 || paths.length != 3, "paths length can either be 1 or 3");
274-
275-
Map<ProviderType, String> providersUrl = new HashMap<>();
276-
277-
URI issuerURI = URI.create(issuer);
278-
279-
if (paths.length == 1) {
280-
providersUrl.put(ProviderType.OIDCV1,
281-
UriComponentsBuilder.fromUri(issuerURI).replacePath(issuerURI.getPath() + paths[0]).toUriString());
282-
} else {
283-
providersUrl.put(ProviderType.OIDC,
284-
UriComponentsBuilder.fromUri(issuerURI).replacePath(paths[0] + issuerURI.getPath()).toUriString());
285-
providersUrl.put(ProviderType.OIDCV1,
286-
UriComponentsBuilder.fromUri(issuerURI).replacePath(issuerURI.getPath() + paths[0]).toUriString());
287-
providersUrl.put(ProviderType.OAUTH2,
288-
UriComponentsBuilder.fromUri(issuerURI).replacePath(paths[1] + issuerURI.getPath()).toUriString());
289-
}
290-
291-
return providersUrl;
292-
}
293-
294226
private static ClientAuthenticationMethod getClientAuthenticationMethod(String issuer,
295227
List<com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod> metadataAuthMethods) {
296228
if (metadataAuthMethods == null || metadataAuthMethods.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.CLIENT_SECRET_BASIC)) {
@@ -317,18 +249,6 @@ private static List<String> getScopes(AuthorizationServerMetadata metadata) {
317249
}
318250
}
319251

320-
private static <T> T parse(String body, ThrowingFunction<String, T, ParseException> parser) {
321-
try {
322-
return parser.apply(body);
323-
} catch (ParseException e) {
324-
throw new RuntimeException(e);
325-
}
326-
}
327-
328-
private interface ThrowingFunction<S, T, E extends Throwable> {
329-
T apply(S src) throws E;
330-
}
331-
332252
private ClientRegistrations() {}
333253

334254
}

0 commit comments

Comments
 (0)