19
19
import com .nimbusds .oauth2 .sdk .GrantType ;
20
20
import com .nimbusds .oauth2 .sdk .ParseException ;
21
21
import com .nimbusds .oauth2 .sdk .Scope ;
22
+ import com .nimbusds .oauth2 .sdk .as .AuthorizationServerMetadata ;
22
23
import com .nimbusds .openid .connect .sdk .op .OIDCProviderMetadata ;
23
24
import org .springframework .security .oauth2 .core .AuthorizationGrantType ;
24
25
import org .springframework .security .oauth2 .core .ClientAuthenticationMethod ;
25
26
import org .springframework .security .oauth2 .core .oidc .IdTokenClaimNames ;
26
27
import org .springframework .security .oauth2 .core .oidc .OidcScopes ;
28
+ import org .springframework .util .Assert ;
27
29
import org .springframework .web .client .RestTemplate ;
30
+ import org .springframework .web .util .UriComponentsBuilder ;
28
31
29
32
import java .net .URI ;
30
33
import java .util .Collections ;
34
+ import java .util .HashMap ;
31
35
import java .util .LinkedHashMap ;
32
36
import java .util .List ;
33
37
import java .util .Map ;
34
38
35
39
/**
36
40
* Allows creating a {@link ClientRegistration.Builder} from an
37
- * <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig">OpenID Provider Configuration</a>.
41
+ * <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig">OpenID Provider Configuration</a>
42
+ * or <a href="https://tools.ietf.org/html/rfc8414#section-3">Authorization Server Metadata</a> based on
43
+ * provided issuer.
38
44
*
39
45
* @author Rob Winch
40
46
* @author Josh Cummings
47
+ * @author Rafiullah Hamedy
41
48
* @since 5.1
42
49
*/
43
50
public final class ClientRegistrations {
51
+ 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
+ }
44
57
45
58
/**
46
59
* Creates a {@link ClientRegistration.Builder} using the provided
@@ -50,6 +63,12 @@ public final class ClientRegistrations {
50
63
* <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse">OpenID
51
64
* Provider Configuration Response</a> to initialize the {@link ClientRegistration.Builder}.
52
65
*
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
+ *
53
72
* <p>
54
73
* For example, if the issuer provided is "https://example.com", then an "OpenID Provider Configuration Request" will
55
74
* be made to "https://example.com/.well-known/openid-configuration". The result is expected to be an "OpenID
@@ -69,19 +88,86 @@ public final class ClientRegistrations {
69
88
* @return a {@link ClientRegistration.Builder} that was initialized by the OpenID Provider Configuration.
70
89
*/
71
90
public static ClientRegistration .Builder fromOidcIssuerLocation (String issuer ) {
72
- String openidConfiguration = getOpenidConfiguration (issuer );
73
- OIDCProviderMetadata metadata = parse (openidConfiguration );
91
+ Map <ProviderType , String > configuration = getIssuerConfiguration (issuer , OIDC_METADATA_PATH );
92
+ OIDCProviderMetadata metadata = parse (configuration .get (ProviderType .OIDCV1 ), OIDCProviderMetadata ::parse );
93
+ return withProviderConfiguration (metadata , issuer )
94
+ .userInfoUri (metadata .getUserInfoEndpointURI ().toASCIIString ());
95
+ }
96
+
97
+ /**
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
101
+ *
102
+ * <ol>
103
+ * <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.
107
+ * </li>
108
+ * <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
+ * </li>
113
+ * <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
+ * </li>
118
+ * </ol>
119
+ *
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.
127
+ *
128
+ * <p>
129
+ * Example usage:
130
+ * </p>
131
+ * <pre>
132
+ * ClientRegistration registration = ClientRegistrations.fromIssuerLocation("https://example.com")
133
+ * .clientId("client-id")
134
+ * .clientSecret("client-secret")
135
+ * .build();
136
+ * </pre>
137
+ *
138
+ * @param issuer
139
+ * @return a {@link ClientRegistration.Builder} that was initialized by the Authorization Sever Metadata Provider
140
+ */
141
+ public static ClientRegistration .Builder fromIssuerLocation (String issuer ) {
142
+ Map <ProviderType , String > configuration = getIssuerConfiguration (issuer , OIDC_METADATA_PATH , OAUTH2_METADATA_PATH );
143
+
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 ;
154
+ }
155
+ }
156
+
157
+ private static ClientRegistration .Builder withProviderConfiguration (AuthorizationServerMetadata metadata , String issuer ) {
74
158
String metadataIssuer = metadata .getIssuer ().getValue ();
75
159
if (!issuer .equals (metadataIssuer )) {
76
- throw new IllegalStateException ("The Issuer \" " + metadataIssuer + "\" provided in the OpenID Configuration did not match the requested issuer \" " + issuer + "\" " );
160
+ throw new IllegalStateException ("The Issuer \" " + metadataIssuer + "\" provided in the configuration metadata did "
161
+ + "not match the requested issuer \" " + issuer + "\" " );
77
162
}
78
163
79
164
String name = URI .create (issuer ).getHost ();
80
165
ClientAuthenticationMethod method = getClientAuthenticationMethod (issuer , metadata .getTokenEndpointAuthMethods ());
81
166
List <GrantType > grantTypes = metadata .getGrantTypes ();
82
167
// If null, the default includes authorization_code
83
168
if (grantTypes != null && !grantTypes .contains (GrantType .AUTHORIZATION_CODE )) {
84
- throw new IllegalArgumentException ("Only AuthorizationGrantType.AUTHORIZATION_CODE is supported. The issuer \" " + issuer + "\" returned a configuration of " + grantTypes );
169
+ throw new IllegalArgumentException ("Only AuthorizationGrantType.AUTHORIZATION_CODE is supported. The issuer \" " + issuer +
170
+ "\" returned a configuration of " + grantTypes );
85
171
}
86
172
List <String > scopes = getScopes (metadata );
87
173
Map <String , Object > configurationMetadata = new LinkedHashMap <>(metadata .toJSONObject ());
@@ -95,21 +181,118 @@ public static ClientRegistration.Builder fromOidcIssuerLocation(String issuer) {
95
181
.authorizationUri (metadata .getAuthorizationEndpointURI ().toASCIIString ())
96
182
.jwkSetUri (metadata .getJWKSetURI ().toASCIIString ())
97
183
.providerConfigurationMetadata (configurationMetadata )
98
- .userInfoUri (metadata .getUserInfoEndpointURI ().toASCIIString ())
99
184
.tokenUri (metadata .getTokenEndpointURI ().toASCIIString ())
100
185
.clientName (issuer );
101
186
}
102
187
103
- private static String getOpenidConfiguration (String issuer ) {
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 ) {
104
234
RestTemplate rest = new RestTemplate ();
105
235
try {
106
- return rest .getForObject (issuer + "/.well-known/openid-configuration" , String .class );
107
- } catch (RuntimeException e ) {
108
- throw new IllegalArgumentException ("Unable to resolve the OpenID Configuration with the provided Issuer of \" " + issuer + "\" " , e );
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 ());
109
289
}
290
+
291
+ return providersUrl ;
110
292
}
111
293
112
- private static ClientAuthenticationMethod getClientAuthenticationMethod (String issuer , List <com .nimbusds .oauth2 .sdk .auth .ClientAuthenticationMethod > metadataAuthMethods ) {
294
+ private static ClientAuthenticationMethod getClientAuthenticationMethod (String issuer ,
295
+ List <com .nimbusds .oauth2 .sdk .auth .ClientAuthenticationMethod > metadataAuthMethods ) {
113
296
if (metadataAuthMethods == null || metadataAuthMethods .contains (com .nimbusds .oauth2 .sdk .auth .ClientAuthenticationMethod .CLIENT_SECRET_BASIC )) {
114
297
// If null, the default includes client_secret_basic
115
298
return ClientAuthenticationMethod .BASIC ;
@@ -120,10 +303,11 @@ private static ClientAuthenticationMethod getClientAuthenticationMethod(String i
120
303
if (metadataAuthMethods .contains (com .nimbusds .oauth2 .sdk .auth .ClientAuthenticationMethod .NONE )) {
121
304
return ClientAuthenticationMethod .NONE ;
122
305
}
123
- throw new IllegalArgumentException ("Only ClientAuthenticationMethod.BASIC, ClientAuthenticationMethod.POST and ClientAuthenticationMethod.NONE are supported. The issuer \" " + issuer + "\" returned a configuration of " + metadataAuthMethods );
306
+ throw new IllegalArgumentException ("Only ClientAuthenticationMethod.BASIC, ClientAuthenticationMethod.POST and "
307
+ + "ClientAuthenticationMethod.NONE are supported. The issuer \" " + issuer + "\" returned a configuration of " + metadataAuthMethods );
124
308
}
125
309
126
- private static List <String > getScopes (OIDCProviderMetadata metadata ) {
310
+ private static List <String > getScopes (AuthorizationServerMetadata metadata ) {
127
311
Scope scope = metadata .getScopes ();
128
312
if (scope == null ) {
129
313
// If null, default to "openid" which must be supported
@@ -133,15 +317,18 @@ private static List<String> getScopes(OIDCProviderMetadata metadata) {
133
317
}
134
318
}
135
319
136
- private static OIDCProviderMetadata parse (String body ) {
320
+ private static < T > T parse (String body , ThrowingFunction < String , T , ParseException > parser ) {
137
321
try {
138
- return OIDCProviderMetadata .parse (body );
139
- }
140
- catch (ParseException e ) {
322
+ return parser .apply (body );
323
+ } catch (ParseException e ) {
141
324
throw new RuntimeException (e );
142
325
}
143
326
}
144
327
328
+ private interface ThrowingFunction <S , T , E extends Throwable > {
329
+ T apply (S src ) throws E ;
330
+ }
331
+
145
332
private ClientRegistrations () {}
146
333
147
334
}
0 commit comments