Skip to content

Commit ab5fa47

Browse files
committed
Support for OAuth 2.0 Authorization Server Metadata
Added support for OAuth 2.0 Authorization Server Metadata as per the RFC 8414 specification. Updated the existing implementation of OpenId to comply with the Compatibility Section of RFC 8414 specification. Fixes: spring-projectsgh-6500
1 parent 98a8467 commit ab5fa47

File tree

4 files changed

+511
-58
lines changed

4 files changed

+511
-58
lines changed

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

Lines changed: 116 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,17 @@
1919
import com.nimbusds.oauth2.sdk.GrantType;
2020
import com.nimbusds.oauth2.sdk.ParseException;
2121
import com.nimbusds.oauth2.sdk.Scope;
22+
import com.nimbusds.oauth2.sdk.as.AuthorizationServerMetadata;
2223
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
24+
25+
import org.springframework.http.HttpStatus;
2326
import org.springframework.security.oauth2.core.AuthorizationGrantType;
2427
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
2528
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
2629
import org.springframework.security.oauth2.core.oidc.OidcScopes;
30+
import org.springframework.web.client.HttpClientErrorException;
2731
import org.springframework.web.client.RestTemplate;
32+
import org.springframework.web.util.UriComponentsBuilder;
2833

2934
import java.net.URI;
3035
import java.util.Collections;
@@ -34,14 +39,21 @@
3439

3540
/**
3641
* 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>.
3845
*
3946
* @author Rob Winch
4047
* @author Josh Cummings
48+
* @author Rafiullah Hamedy
4149
* @since 5.1
4250
*/
4351
public final class ClientRegistrations {
4452

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+
4557
/**
4658
* Creates a {@link ClientRegistration.Builder} using the provided
4759
* <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 {
5062
* <a href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse">OpenID
5163
* Provider Configuration Response</a> to initialize the {@link ClientRegistration.Builder}.
5264
*
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+
*
5371
* <p>
5472
* For example, if the issuer provided is "https://example.com", then an "OpenID Provider Configuration Request" will
5573
* 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 {
6987
* @return a {@link ClientRegistration.Builder} that was initialized by the OpenID Provider Configuration.
7088
*/
7189
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) {
74127
String metadataIssuer = metadata.getIssuer().getValue();
75128
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 + "\"");
77131
}
78132

79133
String name = URI.create(issuer).getHost();
80134
ClientAuthenticationMethod method = getClientAuthenticationMethod(issuer, metadata.getTokenEndpointAuthMethods());
81135
List<GrantType> grantTypes = metadata.getGrantTypes();
82136
// If null, the default includes authorization_code
83137
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);
85140
}
86141
List<String> scopes = getScopes(metadata);
87142
Map<String, Object> configurationMetadata = new LinkedHashMap<>(metadata.toJSONObject());
@@ -95,21 +150,62 @@ public static ClientRegistration.Builder fromOidcIssuerLocation(String issuer) {
95150
.authorizationUri(metadata.getAuthorizationEndpointURI().toASCIIString())
96151
.jwkSetUri(metadata.getJWKSetURI().toASCIIString())
97152
.providerConfigurationMetadata(configurationMetadata)
98-
.userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString())
99153
.tokenUri(metadata.getTokenEndpointURI().toASCIIString())
100154
.clientName(issuer);
101155
}
102156

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+
104161
RestTemplate rest = new RestTemplate();
162+
163+
URI uri = URI.create(issuer);
105164
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+
}
107184
} 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);
109186
}
110187
}
111188

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) {
113209
if (metadataAuthMethods == null || metadataAuthMethods.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.CLIENT_SECRET_BASIC)) {
114210
// If null, the default includes client_secret_basic
115211
return ClientAuthenticationMethod.BASIC;
@@ -120,10 +216,11 @@ private static ClientAuthenticationMethod getClientAuthenticationMethod(String i
120216
if (metadataAuthMethods.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.NONE)) {
121217
return ClientAuthenticationMethod.NONE;
122218
}
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);
124221
}
125222

126-
private static List<String> getScopes(OIDCProviderMetadata metadata) {
223+
private static List<String> getScopes(AuthorizationServerMetadata metadata) {
127224
Scope scope = metadata.getScopes();
128225
if (scope == null) {
129226
// If null, default to "openid" which must be supported
@@ -133,15 +230,18 @@ private static List<String> getScopes(OIDCProviderMetadata metadata) {
133230
}
134231
}
135232

136-
private static OIDCProviderMetadata parse(String body) {
233+
private static <T> T parse(String body, ThrowingFunction<String, T, ParseException> parser) {
137234
try {
138-
return OIDCProviderMetadata.parse(body);
139-
}
140-
catch (ParseException e) {
235+
return parser.apply(body);
236+
} catch (ParseException e) {
141237
throw new RuntimeException(e);
142238
}
143239
}
144240

241+
private interface ThrowingFunction<S, T, E extends Throwable> {
242+
T apply(S src) throws E;
243+
}
244+
145245
private ClientRegistrations() {}
146246

147247
}

0 commit comments

Comments
 (0)