Skip to content

Commit cd6f1d7

Browse files
sahariardevjgrandja
authored andcommitted
Return registration_endpoint when client registration is enabled
Closes gh-370
1 parent 4d94e70 commit cd6f1d7

File tree

6 files changed

+202
-0
lines changed

6 files changed

+202
-0
lines changed

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/AbstractOAuth2AuthorizationServerMetadata.java

+11
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,17 @@ public B tokenIntrospectionEndpointAuthenticationMethods(Consumer<List<String>>
274274
return getThis();
275275
}
276276

277+
/**
278+
* Use this {@code registration_endpoint} in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, Optional.
279+
*
280+
* @param clientRegistrationEndpoint the {@code URL} of the OAuth 2.0 Dynamic Client Registration Endpoint
281+
* @return the {@link AbstractBuilder} for further configuration
282+
* @since 0.4.0
283+
*/
284+
public B clientRegistrationEndpoint(String clientRegistrationEndpoint) {
285+
return claim(OAuth2AuthorizationServerMetadataClaimNames.REGISTRATION_ENDPOINT, clientRegistrationEndpoint);
286+
}
287+
277288
/**
278289
* Add this Proof Key for Code Exchange (PKCE) {@code code_challenge_method} to the collection of {@code code_challenge_methods_supported}
279290
* in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL.

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimAccessor.java

+10
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,16 @@ default List<String> getTokenIntrospectionEndpointAuthenticationMethods() {
141141
return getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED);
142142
}
143143

144+
/**
145+
* Returns the {@code URL} of the authorization server's OAuth 2.0 Dynamic Client Registration endpoint {@code (registration_endpoint)}.
146+
*
147+
* @return the {@code URL} of the authorization server's OAuth 2.0 Dynamic Client Registration endpoint
148+
* @since 0.4.0
149+
*/
150+
default URL getClientRegistrationEndpoint() {
151+
return getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.REGISTRATION_ENDPOINT);
152+
}
153+
144154
/**
145155
* Returns the Proof Key for Code Exchange (PKCE) {@code code_challenge_method} values supported {@code (code_challenge_methods_supported)}.
146156
*

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimNames.java

+6
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ public class OAuth2AuthorizationServerMetadataClaimNames {
8686
*/
8787
public static final String INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED = "introspection_endpoint_auth_methods_supported";
8888

89+
/**
90+
* {@code registration_endpoint} - the {@code URL} of the authorization server's OAuth 2.0 Dynamic Client Registration endpoint
91+
* @since 0.4.0
92+
*/
93+
public static final String REGISTRATION_ENDPOINT = "registration_endpoint";
94+
8995
/**
9096
* {@code code_challenge_methods_supported} - the Proof Key for Code Exchange (PKCE) {@code code_challenge_method} values supported
9197
*/

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcConfigurer.java

+23
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,12 @@
2323
import org.springframework.security.config.Customizer;
2424
import org.springframework.security.config.annotation.ObjectPostProcessor;
2525
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
26+
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContext;
27+
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
28+
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
2629
import org.springframework.security.web.util.matcher.OrRequestMatcher;
2730
import org.springframework.security.web.util.matcher.RequestMatcher;
31+
import org.springframework.web.util.UriComponentsBuilder;
2832

2933
/**
3034
* Configurer for OpenID Connect 1.0 support.
@@ -102,6 +106,25 @@ void init(HttpSecurity httpSecurity) {
102106

103107
@Override
104108
void configure(HttpSecurity httpSecurity) {
109+
OidcClientRegistrationEndpointConfigurer clientRegistrationEndpointConfigurer =
110+
getConfigurer(OidcClientRegistrationEndpointConfigurer.class);
111+
if (clientRegistrationEndpointConfigurer != null) {
112+
OidcProviderConfigurationEndpointConfigurer providerConfigurationEndpointConfigurer =
113+
getConfigurer(OidcProviderConfigurationEndpointConfigurer.class);
114+
115+
providerConfigurationEndpointConfigurer
116+
.addDefaultProviderConfigurationCustomizer(builder -> {
117+
AuthorizationServerContext authorizationServerContext = AuthorizationServerContextHolder.getContext();
118+
String issuer = authorizationServerContext.getIssuer();
119+
AuthorizationServerSettings authorizationServerSettings = authorizationServerContext.getAuthorizationServerSettings();
120+
121+
String clientRegistrationEndpoint = UriComponentsBuilder.fromUriString(issuer)
122+
.path(authorizationServerSettings.getOidcClientRegistrationEndpoint()).build().toUriString();
123+
124+
builder.clientRegistrationEndpoint(clientRegistrationEndpoint);
125+
});
126+
}
127+
105128
this.configurers.values().forEach(configurer -> configurer.configure(httpSecurity));
106129
}
107130

oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataTests.java

+6
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public void buildWhenAllClaimsProvidedThenCreated() {
6060
.tokenRevocationEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue())
6161
.tokenIntrospectionEndpoint("https://example.com/issuer1/oauth2/introspect")
6262
.tokenIntrospectionEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue())
63+
.clientRegistrationEndpoint("https://example.com/issuer1/connect/register")
6364
.codeChallengeMethod("S256")
6465
.claim("a-claim", "a-value")
6566
.build();
@@ -76,6 +77,7 @@ public void buildWhenAllClaimsProvidedThenCreated() {
7677
assertThat(authorizationServerMetadata.getTokenRevocationEndpointAuthenticationMethods()).containsExactly(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue());
7778
assertThat(authorizationServerMetadata.getTokenIntrospectionEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/introspect"));
7879
assertThat(authorizationServerMetadata.getTokenIntrospectionEndpointAuthenticationMethods()).containsExactly(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue());
80+
assertThat(authorizationServerMetadata.getClientRegistrationEndpoint()).isEqualTo(url("https://example.com/issuer1/connect/register"));
7981
assertThat(authorizationServerMetadata.getCodeChallengeMethods()).containsExactly("S256");
8082
assertThat(authorizationServerMetadata.getClaimAsString("a-claim")).isEqualTo("a-value");
8183
}
@@ -115,6 +117,7 @@ public void withClaimsWhenClaimsProvidedThenCreated() {
115117
claims.put(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, Collections.singletonList("code"));
116118
claims.put(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT, "https://example.com/issuer1/oauth2/revoke");
117119
claims.put(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT, "https://example.com/issuer1/oauth2/introspect");
120+
claims.put(OAuth2AuthorizationServerMetadataClaimNames.REGISTRATION_ENDPOINT, "https://example.com/issuer1/connect/register");
118121
claims.put("some-claim", "some-value");
119122

120123
OAuth2AuthorizationServerMetadata authorizationServerMetadata = OAuth2AuthorizationServerMetadata.withClaims(claims).build();
@@ -131,6 +134,7 @@ public void withClaimsWhenClaimsProvidedThenCreated() {
131134
assertThat(authorizationServerMetadata.getTokenRevocationEndpointAuthenticationMethods()).isNull();
132135
assertThat(authorizationServerMetadata.getTokenIntrospectionEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/introspect"));
133136
assertThat(authorizationServerMetadata.getTokenIntrospectionEndpointAuthenticationMethods()).isNull();
137+
assertThat(authorizationServerMetadata.getClientRegistrationEndpoint()).isEqualTo(url("https://example.com/issuer1/connect/register"));
134138
assertThat(authorizationServerMetadata.getCodeChallengeMethods()).isNull();
135139
assertThat(authorizationServerMetadata.getClaimAsString("some-claim")).isEqualTo("some-value");
136140
}
@@ -145,6 +149,7 @@ public void withClaimsWhenClaimsWithUrlsProvidedThenCreated() {
145149
claims.put(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, Collections.singletonList("code"));
146150
claims.put(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT, url("https://example.com/issuer1/oauth2/revoke"));
147151
claims.put(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT, url("https://example.com/issuer1/oauth2/introspect"));
152+
claims.put(OAuth2AuthorizationServerMetadataClaimNames.REGISTRATION_ENDPOINT, url("https://example.com/issuer1/connect/register"));
148153
claims.put("some-claim", "some-value");
149154

150155
OAuth2AuthorizationServerMetadata authorizationServerMetadata = OAuth2AuthorizationServerMetadata.withClaims(claims).build();
@@ -161,6 +166,7 @@ public void withClaimsWhenClaimsWithUrlsProvidedThenCreated() {
161166
assertThat(authorizationServerMetadata.getTokenRevocationEndpointAuthenticationMethods()).isNull();
162167
assertThat(authorizationServerMetadata.getTokenIntrospectionEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/introspect"));
163168
assertThat(authorizationServerMetadata.getTokenIntrospectionEndpointAuthenticationMethods()).isNull();
169+
assertThat(authorizationServerMetadata.getClientRegistrationEndpoint()).isEqualTo(url("https://example.com/issuer1/connect/register"));
164170
assertThat(authorizationServerMetadata.getCodeChallengeMethods()).isNull();
165171
assertThat(authorizationServerMetadata.getClaimAsString("some-claim")).isEqualTo("some-value");
166172
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers;
2+
3+
import org.junit.Rule;
4+
import org.junit.Test;
5+
import org.springframework.beans.factory.annotation.Autowired;
6+
import org.springframework.context.annotation.Bean;
7+
import org.springframework.security.config.Customizer;
8+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
9+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
10+
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
11+
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
12+
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
13+
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
14+
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
15+
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
16+
import org.springframework.security.oauth2.server.authorization.test.SpringTestRule;
17+
import org.springframework.security.web.SecurityFilterChain;
18+
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
19+
import org.springframework.test.web.servlet.MockMvc;
20+
import org.springframework.test.web.servlet.ResultMatcher;
21+
22+
import static org.springframework.test.web.servlet.ResultMatcher.matchAll;
23+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
24+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
25+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
26+
27+
/**
28+
* Integration tests for OpenID Provider Configuration Endpoint.
29+
*
30+
* @author Sahariar Alam Khandoker
31+
*/
32+
public class OidcProviderConfigurationMetaDataTests {
33+
private static final String DEFAULT_OAUTH2_PROVIDER_CONFIGURATION_METADATA_ENDPOINT_URI = "/.well-known/openid-configuration";
34+
private static final String issuerUrl = "https://example.com/issuer1";
35+
36+
@Rule
37+
public final SpringTestRule spring = new SpringTestRule();
38+
39+
@Autowired
40+
private MockMvc mvc;
41+
42+
@Test
43+
public void requestWhenProviderConfigurationRequestGetTheProviderConfigurationResponseWithoutRegistrationEndpoint() throws Exception {
44+
this.spring.register(AuthorizationServerConfiguration.class).autowire();
45+
46+
this.mvc.perform(get(DEFAULT_OAUTH2_PROVIDER_CONFIGURATION_METADATA_ENDPOINT_URI))
47+
.andExpect(status().is2xxSuccessful())
48+
.andExpect(providerConfigurationResponse())
49+
.andExpect(jsonPath("$.registration_endpoint").doesNotExist())
50+
.andReturn();
51+
}
52+
53+
@Test
54+
public void requestWhenProviderConfigurationWithClientRegistrationEnabledRequestGetTheProviderConfigurationResponseWithRegistrationEndpoint() throws Exception {
55+
this.spring.register(AuthorizationServerConfigurationWithClientRegistrationEnabled.class).autowire();
56+
57+
this.mvc.perform(get(DEFAULT_OAUTH2_PROVIDER_CONFIGURATION_METADATA_ENDPOINT_URI))
58+
.andExpect(status().is2xxSuccessful())
59+
.andExpect(providerConfigurationResponse())
60+
.andExpect(jsonPath("$.registration_endpoint").value("https://example.com/issuer1/connect/register"))
61+
.andReturn();
62+
}
63+
64+
private static ResultMatcher providerConfigurationResponse() {
65+
// @formatter:off
66+
return matchAll(
67+
jsonPath("issuer").value("https://example.com/issuer1"),
68+
jsonPath("authorization_endpoint").value("https://example.com/issuer1/oauth2/authorize"),
69+
jsonPath("token_endpoint").value("https://example.com/issuer1/oauth2/token"),
70+
jsonPath("jwks_uri").value("https://example.com/issuer1/oauth2/jwks"),
71+
jsonPath("scopes_supported").value("openid"),
72+
jsonPath("response_types_supported").value("code"),
73+
jsonPath("$.grant_types_supported[0]").value("authorization_code"),
74+
jsonPath("$.grant_types_supported[1]").value("client_credentials"),
75+
jsonPath("$.grant_types_supported[2]").value("refresh_token"),
76+
jsonPath("revocation_endpoint").value("https://example.com/issuer1/oauth2/revoke"),
77+
jsonPath("$.revocation_endpoint_auth_methods_supported[0]").value("client_secret_basic"),
78+
jsonPath("$.revocation_endpoint_auth_methods_supported[1]").value("client_secret_post"),
79+
jsonPath("$.revocation_endpoint_auth_methods_supported[2]").value("client_secret_jwt"),
80+
jsonPath("$.revocation_endpoint_auth_methods_supported[3]").value("private_key_jwt"),
81+
jsonPath("introspection_endpoint").value("https://example.com/issuer1/oauth2/introspect"),
82+
jsonPath("$.introspection_endpoint_auth_methods_supported[0]").value("client_secret_basic"),
83+
jsonPath("$.introspection_endpoint_auth_methods_supported[1]").value("client_secret_post"),
84+
jsonPath("$.introspection_endpoint_auth_methods_supported[2]").value("client_secret_jwt"),
85+
jsonPath("$.introspection_endpoint_auth_methods_supported[3]").value("private_key_jwt"),
86+
jsonPath("subject_types_supported").value("public"),
87+
jsonPath("id_token_signing_alg_values_supported").value("RS256"),
88+
jsonPath("userinfo_endpoint").value("https://example.com/issuer1/userinfo"),
89+
jsonPath("$.token_endpoint_auth_methods_supported[0]").value("client_secret_basic"),
90+
jsonPath("$.token_endpoint_auth_methods_supported[1]").value("client_secret_post"),
91+
jsonPath("$.token_endpoint_auth_methods_supported[2]").value("client_secret_jwt"),
92+
jsonPath("$.token_endpoint_auth_methods_supported[3]").value("private_key_jwt")
93+
);
94+
// @formatter:on
95+
}
96+
97+
98+
@EnableWebSecurity
99+
static class AuthorizationServerConfigurationWithClientRegistrationEnabled extends AuthorizationServerConfiguration {
100+
@Bean
101+
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
102+
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
103+
new OAuth2AuthorizationServerConfigurer();
104+
http.apply(authorizationServerConfigurer);
105+
106+
authorizationServerConfigurer
107+
.oidc(oidc ->
108+
oidc
109+
.clientRegistrationEndpoint(Customizer.withDefaults())
110+
);
111+
112+
return http.build();
113+
}
114+
}
115+
116+
@EnableWebSecurity
117+
static class AuthorizationServerConfiguration {
118+
119+
@Bean
120+
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
121+
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
122+
// @formatter:off
123+
http
124+
.exceptionHandling(exceptions ->
125+
exceptions.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
126+
);
127+
// @formatter:on
128+
return http.build();
129+
}
130+
131+
@Bean
132+
RegisteredClientRepository registeredClientRepository() {
133+
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
134+
return new InMemoryRegisteredClientRepository(registeredClient);
135+
}
136+
137+
@Bean
138+
AuthorizationServerSettings authorizationServerSettings() {
139+
return AuthorizationServerSettings.builder()
140+
.issuer(issuerUrl)
141+
.build();
142+
}
143+
144+
}
145+
146+
}

0 commit comments

Comments
 (0)