Skip to content

Commit 65d88b7

Browse files
committed
Add ClientRegistrations.fromOidcConfiguration method
ClientRegistrations now provides the fromOidcConfiguration method to create a ClientRegistration.Builder from a map representation of an OpenID Provider Configuration Response. This is useful when the OpenID Provider Configuration is not available at a well-known location, or if custom validation is needed for the issuer location (e.g. if the issuer is only reachable via a back-channel URI that is different from the issuer value in the configuration). Fixes: gh-14633
1 parent 4a9a350 commit 65d88b7

File tree

2 files changed

+154
-1
lines changed

2 files changed

+154
-1
lines changed

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,46 @@ public final class ClientRegistrations {
7272
private ClientRegistrations() {
7373
}
7474

75+
/**
76+
* Creates a {@link ClientRegistration.Builder} using the provided map representation
77+
* of an <a href=
78+
* "https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse">OpenID
79+
* Provider Configuration Response</a> to initialize the
80+
* {@link ClientRegistration.Builder}.
81+
*
82+
* <p>
83+
* This is useful when the OpenID Provider Configuration is not available at a
84+
* well-known location, or if custom validation is needed for the issuer location
85+
* (e.g. if the issuer is only accessible from a back-channel URI that is different
86+
* from the issuer value in the configuration).
87+
* </p>
88+
*
89+
* <p>
90+
* Example usage:
91+
* </p>
92+
* <pre>
93+
* RequestEntity<Void> request = RequestEntity.get(metadataEndpoint).build();
94+
* ParameterizedTypeReference<Map<String, Object>> typeReference = new ParameterizedTypeReference<>() {};
95+
* Map<String, Object> configuration = rest.exchange(request, typeReference).getBody();
96+
* // Validate configuration.get("issuer") as per in the OIDC specification
97+
* ClientRegistration registration = ClientRegistrations.fromOidcConfiguration(configuration)
98+
* .clientId("client-id")
99+
* .clientSecret("client-secret")
100+
* .build();
101+
* </pre>
102+
* @param configuration
103+
* @return
104+
*/
105+
public static ClientRegistration.Builder fromOidcConfiguration(Map<String, Object> configuration) {
106+
OIDCProviderMetadata metadata = parse(configuration, OIDCProviderMetadata::parse);
107+
ClientRegistration.Builder builder = withProviderConfiguration(metadata, metadata.getIssuer().getValue());
108+
builder.jwkSetUri(metadata.getJWKSetURI().toASCIIString());
109+
if (metadata.getUserInfoEndpointURI() != null) {
110+
builder.userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString());
111+
}
112+
return builder;
113+
}
114+
75115
/**
76116
* Creates a {@link ClientRegistration.Builder} using the provided <a href=
77117
* "https://openid.net/specs/openid-connect-core-1_0.html#IssuerIdentifier">Issuer</a>

oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTests.java

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
import org.junit.jupiter.api.AfterEach;
3030
import org.junit.jupiter.api.BeforeEach;
3131
import org.junit.jupiter.api.Test;
32-
3332
import org.springframework.http.HttpHeaders;
3433
import org.springframework.http.MediaType;
3534
import org.springframework.security.oauth2.core.AuthorizationGrantType;
@@ -455,6 +454,120 @@ public void issuerWhenOAuth2ConfigurationDoesNotMatchThenMeaningfulErrorMessage(
455454
// @formatter:on
456455
}
457456

457+
@Test
458+
public void issuerWhenOidcConfigurationAllInformationThenSuccess() throws Exception {
459+
ClientRegistration registration = registration(this.response).build();
460+
ClientRegistration.ProviderDetails provider = registration.getProviderDetails();
461+
assertIssuerMetadata(registration, provider);
462+
assertThat(provider.getUserInfoEndpoint().getUri()).isEqualTo("https://example.com/oauth2/v3/userinfo");
463+
}
464+
465+
private ClientRegistration.Builder registration(Map<String, Object> configuration) {
466+
this.issuer = "https://example.com";
467+
return ClientRegistrations.fromOidcConfiguration(configuration)
468+
.clientId("client-id")
469+
.clientSecret("client-secret");
470+
}
471+
472+
@Test
473+
public void issuerWhenOidcConfigurationResponseMissingJwksUriThenThrowsIllegalArgumentException() throws Exception {
474+
this.response.remove("jwks_uri");
475+
assertThatIllegalArgumentException().isThrownBy(() -> registration(this.response).build())
476+
.withMessageContaining("The public JWK set URI must not be null");
477+
}
478+
479+
@Test
480+
public void issuerWhenOidcConfigurationResponseMissingUserInfoUriThenSuccess() throws Exception {
481+
this.response.remove("userinfo_endpoint");
482+
ClientRegistration registration = registration(this.response).build();
483+
assertThat(registration.getProviderDetails().getUserInfoEndpoint().getUri()).isNull();
484+
}
485+
486+
@Test
487+
public void issuerWhenOidcConfigurationGrantTypesSupportedNullThenDefaulted() throws Exception {
488+
this.response.remove("grant_types_supported");
489+
ClientRegistration registration = registration(this.response).build();
490+
assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
491+
}
492+
493+
@Test
494+
public void issuerWhenOidcConfigurationImplicitGrantTypeThenSuccess() throws Exception {
495+
this.response.put("grant_types_supported", Arrays.asList("implicit"));
496+
ClientRegistration registration = registration(this.response).build();
497+
// The authorization_code grant type is still the default
498+
assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
499+
}
500+
501+
@Test
502+
public void issuerWhenOidcConfigurationResponseAuthorizationEndpointIsNullThenSuccess() throws Exception {
503+
this.response.put("grant_types_supported", Arrays.asList("urn:ietf:params:oauth:grant-type:jwt-bearer"));
504+
this.response.remove("authorization_endpoint");
505+
ClientRegistration registration = registration(this.response)
506+
.authorizationGrantType(AuthorizationGrantType.JWT_BEARER)
507+
.build();
508+
assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.JWT_BEARER);
509+
ClientRegistration.ProviderDetails provider = registration.getProviderDetails();
510+
assertThat(provider.getAuthorizationUri()).isNull();
511+
}
512+
513+
@Test
514+
public void issuerWhenOidcConfigurationTokenEndpointAuthMethodsNullThenDefaulted() throws Exception {
515+
this.response.remove("token_endpoint_auth_methods_supported");
516+
ClientRegistration registration = registration(this.response).build();
517+
assertThat(registration.getClientAuthenticationMethod())
518+
.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
519+
}
520+
521+
@Test
522+
public void issuerWhenOidcConfigurationClientSecretBasicAuthMethodThenMethodIsBasic() throws Exception {
523+
this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("client_secret_basic"));
524+
ClientRegistration registration = registration(this.response).build();
525+
assertThat(registration.getClientAuthenticationMethod())
526+
.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
527+
}
528+
529+
@Test
530+
public void issuerWhenOidcConfigurationTokenEndpointAuthMethodsPostThenMethodIsPost() throws Exception {
531+
this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("client_secret_post"));
532+
ClientRegistration registration = registration(this.response).build();
533+
assertThat(registration.getClientAuthenticationMethod())
534+
.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_POST);
535+
}
536+
537+
@Test
538+
public void issuerWhenOidcConfigurationClientSecretJwtAuthMethodThenMethodIsClientSecretBasic() throws Exception {
539+
this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("client_secret_jwt"));
540+
ClientRegistration registration = registration(this.response).build();
541+
// The client_secret_basic auth method is still the default
542+
assertThat(registration.getClientAuthenticationMethod())
543+
.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
544+
}
545+
546+
@Test
547+
public void issuerWhenOidcConfigurationPrivateKeyJwtAuthMethodThenMethodIsClientSecretBasic() throws Exception {
548+
this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("private_key_jwt"));
549+
ClientRegistration registration = registration(this.response).build();
550+
// The client_secret_basic auth method is still the default
551+
assertThat(registration.getClientAuthenticationMethod())
552+
.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
553+
}
554+
555+
@Test
556+
public void issuerWhenOidcConfigurationTokenEndpointAuthMethodsNoneThenMethodIsNone() throws Exception {
557+
this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("none"));
558+
ClientRegistration registration = registration(this.response).build();
559+
assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.NONE);
560+
}
561+
562+
@Test
563+
public void issuerWhenOidcConfigurationTlsClientAuthMethodThenSuccess() throws Exception {
564+
this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("tls_client_auth"));
565+
ClientRegistration registration = registration(this.response).build();
566+
// The client_secret_basic auth method is still the default
567+
assertThat(registration.getClientAuthenticationMethod())
568+
.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
569+
}
570+
458571
private ClientRegistration.Builder registration(String path) throws Exception {
459572
this.issuer = createIssuerFromServer(path);
460573
this.response.put("issuer", this.issuer);

0 commit comments

Comments
 (0)