16
16
17
17
package org .springframework .security .oauth2 .client .registration ;
18
18
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
+
19
26
import com .nimbusds .oauth2 .sdk .GrantType ;
20
27
import com .nimbusds .oauth2 .sdk .ParseException ;
21
28
import com .nimbusds .oauth2 .sdk .Scope ;
22
29
import com .nimbusds .oauth2 .sdk .as .AuthorizationServerMetadata ;
23
30
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 ;
24
35
import org .springframework .security .oauth2 .core .AuthorizationGrantType ;
25
36
import org .springframework .security .oauth2 .core .ClientAuthenticationMethod ;
26
37
import org .springframework .security .oauth2 .core .oidc .IdTokenClaimNames ;
27
38
import org .springframework .security .oauth2 .core .oidc .OidcScopes ;
28
39
import org .springframework .util .Assert ;
40
+ import org .springframework .web .client .HttpClientErrorException ;
29
41
import org .springframework .web .client .RestTemplate ;
30
42
import org .springframework .web .util .UriComponentsBuilder ;
31
43
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
-
39
44
/**
40
45
* Allows creating a {@link ClientRegistration.Builder} from an
41
46
* <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig">OpenID Provider Configuration</a>
49
54
*/
50
55
public final class ClientRegistrations {
51
56
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 >>() {};
57
61
58
62
/**
59
63
* Creates a {@link ClientRegistration.Builder} using the provided
@@ -63,12 +67,6 @@ enum ProviderType {
63
67
* <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse">OpenID
64
68
* Provider Configuration Response</a> to initialize the {@link ClientRegistration.Builder}.
65
69
*
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
- *
72
70
* <p>
73
71
* For example, if the issuer provided is "https://example.com", then an "OpenID Provider Configuration Request" will
74
72
* be made to "https://example.com/.well-known/openid-configuration". The result is expected to be an "OpenID
@@ -88,42 +86,41 @@ enum ProviderType {
88
86
* @return a {@link ClientRegistration.Builder} that was initialized by the OpenID Provider Configuration.
89
87
*/
90
88
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 );
93
92
return withProviderConfiguration (metadata , issuer )
94
93
.userInfoUri (metadata .getUserInfoEndpointURI ().toASCIIString ());
95
94
}
96
95
97
96
/**
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}:
101
105
*
102
106
* <ol>
103
107
* <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>.
107
110
* </li>
108
111
* <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>.
112
115
* </li>
113
116
* <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>.
117
119
* </li>
118
120
* </ol>
119
121
*
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)}.
127
124
*
128
125
* <p>
129
126
* Example usage:
@@ -136,24 +133,65 @@ public static ClientRegistration.Builder fromOidcIssuerLocation(String issuer) {
136
133
* </pre>
137
134
*
138
135
* @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
140
137
*/
141
138
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
+ }
143
147
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 );
154
188
}
155
189
}
156
190
191
+ private interface ThrowingFunction <S , T , E extends Throwable > {
192
+ T apply (S src ) throws E ;
193
+ }
194
+
157
195
private static ClientRegistration .Builder withProviderConfiguration (AuthorizationServerMetadata metadata , String issuer ) {
158
196
String metadataIssuer = metadata .getIssuer ().getValue ();
159
197
if (!issuer .equals (metadataIssuer )) {
@@ -185,112 +223,6 @@ private static ClientRegistration.Builder withProviderConfiguration(Authorizatio
185
223
.clientName (issuer );
186
224
}
187
225
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
-
294
226
private static ClientAuthenticationMethod getClientAuthenticationMethod (String issuer ,
295
227
List <com .nimbusds .oauth2 .sdk .auth .ClientAuthenticationMethod > metadataAuthMethods ) {
296
228
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) {
317
249
}
318
250
}
319
251
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
-
332
252
private ClientRegistrations () {}
333
253
334
254
}
0 commit comments