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 ;
24
+
25
+ import org .springframework .http .HttpStatus ;
23
26
import org .springframework .security .oauth2 .core .AuthorizationGrantType ;
24
27
import org .springframework .security .oauth2 .core .ClientAuthenticationMethod ;
25
28
import org .springframework .security .oauth2 .core .oidc .IdTokenClaimNames ;
26
29
import org .springframework .security .oauth2 .core .oidc .OidcScopes ;
30
+ import org .springframework .web .client .HttpClientErrorException ;
27
31
import org .springframework .web .client .RestTemplate ;
32
+ import org .springframework .web .util .UriComponentsBuilder ;
28
33
29
34
import java .net .URI ;
30
35
import java .util .Collections ;
34
39
35
40
/**
36
41
* 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>.
42
+ * <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig">OpenID Provider Configuration</a>
43
+ * and
44
+ * <a href="https://tools.ietf.org/html/rfc8414#section-3">Obtaining Authorization Server Metadata</a>.
38
45
*
39
46
* @author Rob Winch
40
47
* @author Josh Cummings
48
+ * @author Rafiullah Hamedy
41
49
* @since 5.1
42
50
*/
43
51
public final class ClientRegistrations {
44
52
53
+ private static final String WELL_KNOWN_PATH = "/.well-known/" ;
54
+ private static final String OIDC_METADATA_PATH = "openid-configuration" ;
55
+ private static final String OAUTH2_METADATA_PATH = "oauth-authorization-server" ;
56
+
45
57
/**
46
58
* Creates a {@link ClientRegistration.Builder} using the provided
47
59
* <a href="https://openid.net/specs/openid-connect-core-1_0.html#IssuerIdentifier">Issuer</a> by making an
@@ -50,6 +62,12 @@ public final class ClientRegistrations {
50
62
* <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse">OpenID
51
63
* Provider Configuration Response</a> to initialize the {@link ClientRegistration.Builder}.
52
64
*
65
+ * When deployed in legacy environments using OpenID Connect Discovery 1.0 and if the provided issuer has
66
+ * a path i.e. /issuer1 then as per <a href="https://tools.ietf.org/html/rfc8414#section-5">Compatibility Notes</a>
67
+ * first make an <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest">OpenID Provider
68
+ * Configuration Request</a> using path /.well-known/openid-configuration/issuer1 and only if the retrieval
69
+ * fail then a subsequent request to path /issuer1/.well-known/openid-configuration should be made.
70
+ *
53
71
* <p>
54
72
* For example, if the issuer provided is "https://example.com", then an "OpenID Provider Configuration Request" will
55
73
* be made to "https://example.com/.well-known/openid-configuration". The result is expected to be an "OpenID
@@ -69,19 +87,56 @@ public final class ClientRegistrations {
69
87
* @return a {@link ClientRegistration.Builder} that was initialized by the OpenID Provider Configuration.
70
88
*/
71
89
public static ClientRegistration .Builder fromOidcIssuerLocation (String issuer ) {
72
- String openidConfiguration = getOpenidConfiguration (issuer );
73
- OIDCProviderMetadata metadata = parse (openidConfiguration );
90
+ String configuration = getOpenIdConfiguration (issuer );
91
+ OIDCProviderMetadata metadata = parse (configuration , OIDCProviderMetadata ::parse );
92
+ return withProviderConfiguration (metadata , issuer )
93
+ .userInfoUri (metadata .getUserInfoEndpointURI ().toASCIIString ());
94
+ }
95
+
96
+ /**
97
+ * Creates a {@link ClientRegistration.Builder} using the provided issuer by making an
98
+ * <a href="https://tools.ietf.org/html/rfc8414#section-3.1">Authorization Server Metadata Request</a> and using the
99
+ * values in the <a href="https://tools.ietf.org/html/rfc8414#section-3.2">Authorization Server Metadata Response</a>
100
+ * to initialize the {@link ClientRegistration.Builder}.
101
+ *
102
+ * <p>
103
+ * For example, if the issuer provided is "https://example.com", then an "Authorization Server Metadata Request" will
104
+ * be made to "https://example.com/.well-known/oauth-authorization-server". The result is expected to be an "Authorization
105
+ * Server Metadata Response".
106
+ * </p>
107
+ *
108
+ * <p>
109
+ * Example usage:
110
+ * </p>
111
+ * <pre>
112
+ * ClientRegistration registration = ClientRegistrations.fromOAuth2IssuerLocation("https://example.com")
113
+ * .clientId("client-id")
114
+ * .clientSecret("client-secret")
115
+ * .build();
116
+ * </pre>
117
+ * @param issuer
118
+ * @return a {@link ClientRegistration.Builder} that was initialized by the Authorization Sever Metadata Provider
119
+ */
120
+ public static ClientRegistration .Builder fromOAuth2IssuerLocation (String issuer ) {
121
+ String configuration = getOAuth2Configuration (issuer );
122
+ AuthorizationServerMetadata metadata = parse (configuration , AuthorizationServerMetadata ::parse );
123
+ return withProviderConfiguration (metadata , issuer );
124
+ }
125
+
126
+ private static ClientRegistration .Builder withProviderConfiguration (AuthorizationServerMetadata metadata , String issuer ) {
74
127
String metadataIssuer = metadata .getIssuer ().getValue ();
75
128
if (!issuer .equals (metadataIssuer )) {
76
- throw new IllegalStateException ("The Issuer \" " + metadataIssuer + "\" provided in the OpenID Configuration did not match the requested issuer \" " + issuer + "\" " );
129
+ throw new IllegalStateException ("The Issuer \" " + metadataIssuer + "\" provided in the configuration metadata did "
130
+ + "not match the requested issuer \" " + issuer + "\" " );
77
131
}
78
132
79
133
String name = URI .create (issuer ).getHost ();
80
134
ClientAuthenticationMethod method = getClientAuthenticationMethod (issuer , metadata .getTokenEndpointAuthMethods ());
81
135
List <GrantType > grantTypes = metadata .getGrantTypes ();
82
136
// If null, the default includes authorization_code
83
137
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 );
138
+ throw new IllegalArgumentException ("Only AuthorizationGrantType.AUTHORIZATION_CODE is supported. The issuer \" " + issuer +
139
+ "\" returned a configuration of " + grantTypes );
85
140
}
86
141
List <String > scopes = getScopes (metadata );
87
142
Map <String , Object > configurationMetadata = new LinkedHashMap <>(metadata .toJSONObject ());
@@ -95,21 +150,62 @@ public static ClientRegistration.Builder fromOidcIssuerLocation(String issuer) {
95
150
.authorizationUri (metadata .getAuthorizationEndpointURI ().toASCIIString ())
96
151
.jwkSetUri (metadata .getJWKSetURI ().toASCIIString ())
97
152
.providerConfigurationMetadata (configurationMetadata )
98
- .userInfoUri (metadata .getUserInfoEndpointURI ().toASCIIString ())
99
153
.tokenUri (metadata .getTokenEndpointURI ().toASCIIString ())
100
154
.clientName (issuer );
101
155
}
102
156
103
- private static String getOpenidConfiguration (String issuer ) {
157
+ private static String getOpenIdConfiguration (String issuer ) {
158
+ final String wellKnownPath = WELL_KNOWN_PATH + OIDC_METADATA_PATH ;
159
+ final String invalidIssuerMessage = "Unable to resolve the OpenID Configuration with the provided Issuer of \" " + issuer + "\" " ;
160
+
104
161
RestTemplate rest = new RestTemplate ();
162
+
163
+ URI uri = URI .create (issuer );
105
164
try {
106
- return rest .getForObject (issuer + "/.well-known/openid-configuration" , String .class );
165
+ /**
166
+ * Results in /.well-known/openid-configuration/issuer1 assuming issuer is https://example.com/issuer1
167
+ */
168
+ String url = UriComponentsBuilder .fromUri (uri ).replacePath (wellKnownPath + uri .getPath ()).toUriString ();
169
+ return rest .getForObject (url , String .class );
170
+ } catch (HttpClientErrorException e ) {
171
+ if (e .getStatusCode () == HttpStatus .NOT_FOUND ) {
172
+ /**
173
+ * As per the <a href="https://tools.ietf.org/html/rfc8414#section-5">Section 5</a> when the first attempt for
174
+ * https://example.com/.well-known/openid-configuration/issuer1 failed then for backward compatibility
175
+ * check https://example.com/issuer1/.well-known/openid-configuration for Open ID only.
176
+ *
177
+ * Results in /issuer1/.well-known/openid-configuration where issuer is https://example.com/issuer1
178
+ */
179
+ String url = UriComponentsBuilder .fromUri (uri ).replacePath (uri .getPath () + wellKnownPath ).toUriString ();
180
+ return rest .getForObject (url , String .class );
181
+ } else {
182
+ throw new IllegalArgumentException (invalidIssuerMessage , e );
183
+ }
107
184
} catch (RuntimeException e ) {
108
- throw new IllegalArgumentException ("Unable to resolve the OpenID Configuration with the provided Issuer of \" " + issuer + " \" " , e );
185
+ throw new IllegalArgumentException (invalidIssuerMessage , e );
109
186
}
110
187
}
111
188
112
- private static ClientAuthenticationMethod getClientAuthenticationMethod (String issuer , List <com .nimbusds .oauth2 .sdk .auth .ClientAuthenticationMethod > metadataAuthMethods ) {
189
+ private static String getOAuth2Configuration (String issuer ) {
190
+ final String wellKnownPath = WELL_KNOWN_PATH + OAUTH2_METADATA_PATH ;
191
+
192
+ RestTemplate rest = new RestTemplate ();
193
+
194
+ URI uri = URI .create (issuer );
195
+ try {
196
+ /**
197
+ * Results in /.well-known/oauth-authorization-server/issuer1 where issuer is https://example.com/issuer1
198
+ */
199
+ String url = UriComponentsBuilder .fromUri (uri ).replacePath (wellKnownPath + uri .getPath ()).toUriString ();
200
+ return rest .getForObject (url , String .class );
201
+ } catch (RuntimeException e ) {
202
+ throw new IllegalArgumentException ("Unable to resolve the Authorization Server Metadata with the provided "
203
+ + "Issuer of \" " + issuer + "\" " , e );
204
+ }
205
+ }
206
+
207
+ private static ClientAuthenticationMethod getClientAuthenticationMethod (String issuer ,
208
+ List <com .nimbusds .oauth2 .sdk .auth .ClientAuthenticationMethod > metadataAuthMethods ) {
113
209
if (metadataAuthMethods == null || metadataAuthMethods .contains (com .nimbusds .oauth2 .sdk .auth .ClientAuthenticationMethod .CLIENT_SECRET_BASIC )) {
114
210
// If null, the default includes client_secret_basic
115
211
return ClientAuthenticationMethod .BASIC ;
@@ -120,10 +216,11 @@ private static ClientAuthenticationMethod getClientAuthenticationMethod(String i
120
216
if (metadataAuthMethods .contains (com .nimbusds .oauth2 .sdk .auth .ClientAuthenticationMethod .NONE )) {
121
217
return ClientAuthenticationMethod .NONE ;
122
218
}
123
- throw new IllegalArgumentException ("Only ClientAuthenticationMethod.BASIC, ClientAuthenticationMethod.POST and ClientAuthenticationMethod.NONE are supported. The issuer \" " + issuer + "\" returned a configuration of " + metadataAuthMethods );
219
+ throw new IllegalArgumentException ("Only ClientAuthenticationMethod.BASIC, ClientAuthenticationMethod.POST and "
220
+ + "ClientAuthenticationMethod.NONE are supported. The issuer \" " + issuer + "\" returned a configuration of " + metadataAuthMethods );
124
221
}
125
222
126
- private static List <String > getScopes (OIDCProviderMetadata metadata ) {
223
+ private static List <String > getScopes (AuthorizationServerMetadata metadata ) {
127
224
Scope scope = metadata .getScopes ();
128
225
if (scope == null ) {
129
226
// If null, default to "openid" which must be supported
@@ -133,15 +230,18 @@ private static List<String> getScopes(OIDCProviderMetadata metadata) {
133
230
}
134
231
}
135
232
136
- private static OIDCProviderMetadata parse (String body ) {
233
+ private static < T > T parse (String body , ThrowingFunction < String , T , ParseException > parser ) {
137
234
try {
138
- return OIDCProviderMetadata .parse (body );
139
- }
140
- catch (ParseException e ) {
235
+ return parser .apply (body );
236
+ } catch (ParseException e ) {
141
237
throw new RuntimeException (e );
142
238
}
143
239
}
144
240
241
+ private interface ThrowingFunction <S , T , E extends Throwable > {
242
+ T apply (S src ) throws E ;
243
+ }
244
+
145
245
private ClientRegistrations () {}
146
246
147
247
}
0 commit comments