Skip to content

Commit f3761af

Browse files
committed
Add support for OAuth 2.0 Dynamic Client Registration Protocol
Closes gh-17964
1 parent 667cd4a commit f3761af

File tree

31 files changed

+4520
-437
lines changed

31 files changed

+4520
-437
lines changed

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
4949
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
5050
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
51+
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContext;
52+
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
5153
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
5254
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
5355
import org.springframework.security.oauth2.server.authorization.web.NimbusJwkSetEndpointFilter;
@@ -58,6 +60,7 @@
5860
import org.springframework.security.web.util.matcher.OrRequestMatcher;
5961
import org.springframework.security.web.util.matcher.RequestMatcher;
6062
import org.springframework.util.Assert;
63+
import org.springframework.web.util.UriComponentsBuilder;
6164

6265
/**
6366
* An {@link AbstractHttpConfigurer} for OAuth 2.1 Authorization Server support.
@@ -78,6 +81,7 @@
7881
* @see OAuth2TokenRevocationEndpointConfigurer
7982
* @see OAuth2DeviceAuthorizationEndpointConfigurer
8083
* @see OAuth2DeviceVerificationEndpointConfigurer
84+
* @see OAuth2ClientRegistrationEndpointConfigurer
8185
* @see OidcConfigurer
8286
* @see RegisteredClientRepository
8387
* @see OAuth2AuthorizationService
@@ -268,6 +272,25 @@ public OAuth2AuthorizationServerConfigurer deviceVerificationEndpoint(
268272
return this;
269273
}
270274

275+
/**
276+
* Configures the OAuth 2.0 Dynamic Client Registration Endpoint.
277+
* @param clientRegistrationEndpointCustomizer the {@link Customizer} providing access
278+
* to the {@link OAuth2ClientRegistrationEndpointConfigurer}
279+
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
280+
*/
281+
public OAuth2AuthorizationServerConfigurer clientRegistrationEndpoint(
282+
Customizer<OAuth2ClientRegistrationEndpointConfigurer> clientRegistrationEndpointCustomizer) {
283+
OAuth2ClientRegistrationEndpointConfigurer clientRegistrationEndpointConfigurer = getConfigurer(
284+
OAuth2ClientRegistrationEndpointConfigurer.class);
285+
if (clientRegistrationEndpointConfigurer == null) {
286+
addConfigurer(OAuth2ClientRegistrationEndpointConfigurer.class,
287+
new OAuth2ClientRegistrationEndpointConfigurer(this::postProcess));
288+
clientRegistrationEndpointConfigurer = getConfigurer(OAuth2ClientRegistrationEndpointConfigurer.class);
289+
}
290+
clientRegistrationEndpointCustomizer.customize(clientRegistrationEndpointConfigurer);
291+
return this;
292+
}
293+
271294
/**
272295
* Configures OpenID Connect 1.0 support (disabled by default).
273296
* @param oidcCustomizer the {@link Customizer} providing access to the
@@ -377,6 +400,12 @@ public void init(HttpSecurity httpSecurity) {
377400

378401
httpSecurity.csrf((csrf) -> csrf.ignoringRequestMatchers(this.endpointsMatcher));
379402

403+
if (getConfigurer(OAuth2ClientRegistrationEndpointConfigurer.class) != null) {
404+
httpSecurity
405+
// Accept access tokens for Client Registration
406+
.oauth2ResourceServer((oauth2ResourceServer) -> oauth2ResourceServer.jwt(Customizer.withDefaults()));
407+
}
408+
380409
OidcConfigurer oidcConfigurer = getConfigurer(OidcConfigurer.class);
381410
if (oidcConfigurer != null) {
382411
if (oidcConfigurer.getConfigurer(OidcUserInfoEndpointConfigurer.class) != null
@@ -392,6 +421,27 @@ public void init(HttpSecurity httpSecurity) {
392421

393422
@Override
394423
public void configure(HttpSecurity httpSecurity) {
424+
OAuth2ClientRegistrationEndpointConfigurer clientRegistrationEndpointConfigurer = getConfigurer(
425+
OAuth2ClientRegistrationEndpointConfigurer.class);
426+
if (clientRegistrationEndpointConfigurer != null) {
427+
OAuth2AuthorizationServerMetadataEndpointConfigurer authorizationServerMetadataEndpointConfigurer = getConfigurer(
428+
OAuth2AuthorizationServerMetadataEndpointConfigurer.class);
429+
430+
authorizationServerMetadataEndpointConfigurer.addDefaultAuthorizationServerMetadataCustomizer((builder) -> {
431+
AuthorizationServerContext authorizationServerContext = AuthorizationServerContextHolder.getContext();
432+
String issuer = authorizationServerContext.getIssuer();
433+
AuthorizationServerSettings authorizationServerSettings = authorizationServerContext
434+
.getAuthorizationServerSettings();
435+
436+
String clientRegistrationEndpoint = UriComponentsBuilder.fromUriString(issuer)
437+
.path(authorizationServerSettings.getClientRegistrationEndpoint())
438+
.build()
439+
.toUriString();
440+
441+
builder.clientRegistrationEndpoint(clientRegistrationEndpoint);
442+
});
443+
}
444+
395445
this.configurers.values().forEach((configurer) -> configurer.configure(httpSecurity));
396446

397447
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
/*
2+
* Copyright 2004-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
18+
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
import java.util.function.Consumer;
22+
23+
import jakarta.servlet.http.HttpServletRequest;
24+
25+
import org.springframework.http.HttpMethod;
26+
import org.springframework.security.authentication.AuthenticationManager;
27+
import org.springframework.security.authentication.AuthenticationProvider;
28+
import org.springframework.security.config.ObjectPostProcessor;
29+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
30+
import org.springframework.security.crypto.password.PasswordEncoder;
31+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
32+
import org.springframework.security.oauth2.core.OAuth2Error;
33+
import org.springframework.security.oauth2.server.authorization.OAuth2ClientRegistration;
34+
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientRegistrationAuthenticationProvider;
35+
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientRegistrationAuthenticationToken;
36+
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
37+
import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientRegistrationEndpointFilter;
38+
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ClientRegistrationAuthenticationConverter;
39+
import org.springframework.security.web.access.intercept.AuthorizationFilter;
40+
import org.springframework.security.web.authentication.AuthenticationConverter;
41+
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
42+
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
43+
import org.springframework.security.web.authentication.DelegatingAuthenticationConverter;
44+
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
45+
import org.springframework.security.web.util.matcher.RequestMatcher;
46+
import org.springframework.util.Assert;
47+
48+
/**
49+
* Configurer for OAuth 2.0 Dynamic Client Registration Endpoint.
50+
*
51+
* @author Joe Grandja
52+
* @since 7.0
53+
* @see OAuth2AuthorizationServerConfigurer#clientRegistrationEndpoint
54+
* @see OAuth2ClientRegistrationEndpointFilter
55+
*/
56+
public final class OAuth2ClientRegistrationEndpointConfigurer extends AbstractOAuth2Configurer {
57+
58+
private RequestMatcher requestMatcher;
59+
60+
private final List<AuthenticationConverter> clientRegistrationRequestConverters = new ArrayList<>();
61+
62+
private Consumer<List<AuthenticationConverter>> clientRegistrationRequestConvertersConsumer = (
63+
clientRegistrationRequestConverters) -> {
64+
};
65+
66+
private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
67+
68+
private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {
69+
};
70+
71+
private AuthenticationSuccessHandler clientRegistrationResponseHandler;
72+
73+
private AuthenticationFailureHandler errorResponseHandler;
74+
75+
private boolean openRegistrationAllowed;
76+
77+
/**
78+
* Restrict for internal use only.
79+
* @param objectPostProcessor an {@code ObjectPostProcessor}
80+
*/
81+
OAuth2ClientRegistrationEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
82+
super(objectPostProcessor);
83+
}
84+
85+
/**
86+
* Adds an {@link AuthenticationConverter} used when attempting to extract a Client
87+
* Registration Request from {@link HttpServletRequest} to an instance of
88+
* {@link OAuth2ClientRegistrationAuthenticationToken} used for authenticating the
89+
* request.
90+
* @param clientRegistrationRequestConverter an {@link AuthenticationConverter} used
91+
* when attempting to extract a Client Registration Request from
92+
* {@link HttpServletRequest}
93+
* @return the {@link OAuth2ClientRegistrationEndpointConfigurer} for further
94+
* configuration
95+
*/
96+
public OAuth2ClientRegistrationEndpointConfigurer clientRegistrationRequestConverter(
97+
AuthenticationConverter clientRegistrationRequestConverter) {
98+
Assert.notNull(clientRegistrationRequestConverter, "clientRegistrationRequestConverter cannot be null");
99+
this.clientRegistrationRequestConverters.add(clientRegistrationRequestConverter);
100+
return this;
101+
}
102+
103+
/**
104+
* Sets the {@code Consumer} providing access to the {@code List} of default and
105+
* (optionally) added
106+
* {@link #clientRegistrationRequestConverter(AuthenticationConverter)
107+
* AuthenticationConverter}'s allowing the ability to add, remove, or customize a
108+
* specific {@link AuthenticationConverter}.
109+
* @param clientRegistrationRequestConvertersConsumer the {@code Consumer} providing
110+
* access to the {@code List} of default and (optionally) added
111+
* {@link AuthenticationConverter}'s
112+
* @return the {@link OAuth2ClientRegistrationEndpointConfigurer} for further
113+
* configuration
114+
*/
115+
public OAuth2ClientRegistrationEndpointConfigurer clientRegistrationRequestConverters(
116+
Consumer<List<AuthenticationConverter>> clientRegistrationRequestConvertersConsumer) {
117+
Assert.notNull(clientRegistrationRequestConvertersConsumer,
118+
"clientRegistrationRequestConvertersConsumer cannot be null");
119+
this.clientRegistrationRequestConvertersConsumer = clientRegistrationRequestConvertersConsumer;
120+
return this;
121+
}
122+
123+
/**
124+
* Adds an {@link AuthenticationProvider} used for authenticating an
125+
* {@link OAuth2ClientRegistrationAuthenticationToken}.
126+
* @param authenticationProvider an {@link AuthenticationProvider} used for
127+
* authenticating an {@link OAuth2ClientRegistrationAuthenticationToken}
128+
* @return the {@link OAuth2ClientRegistrationEndpointConfigurer} for further
129+
* configuration
130+
*/
131+
public OAuth2ClientRegistrationEndpointConfigurer authenticationProvider(
132+
AuthenticationProvider authenticationProvider) {
133+
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
134+
this.authenticationProviders.add(authenticationProvider);
135+
return this;
136+
}
137+
138+
/**
139+
* Sets the {@code Consumer} providing access to the {@code List} of default and
140+
* (optionally) added {@link #authenticationProvider(AuthenticationProvider)
141+
* AuthenticationProvider}'s allowing the ability to add, remove, or customize a
142+
* specific {@link AuthenticationProvider}.
143+
* @param authenticationProvidersConsumer the {@code Consumer} providing access to the
144+
* {@code List} of default and (optionally) added {@link AuthenticationProvider}'s
145+
* @return the {@link OAuth2ClientRegistrationEndpointConfigurer} for further
146+
* configuration
147+
*/
148+
public OAuth2ClientRegistrationEndpointConfigurer authenticationProviders(
149+
Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer) {
150+
Assert.notNull(authenticationProvidersConsumer, "authenticationProvidersConsumer cannot be null");
151+
this.authenticationProvidersConsumer = authenticationProvidersConsumer;
152+
return this;
153+
}
154+
155+
/**
156+
* Sets the {@link AuthenticationSuccessHandler} used for handling an
157+
* {@link OAuth2ClientRegistrationAuthenticationToken} and returning the
158+
* {@link OAuth2ClientRegistration Client Registration Response}.
159+
* @param clientRegistrationResponseHandler the {@link AuthenticationSuccessHandler}
160+
* used for handling an {@link OAuth2ClientRegistrationAuthenticationToken}
161+
* @return the {@link OAuth2ClientRegistrationEndpointConfigurer} for further
162+
* configuration
163+
*/
164+
public OAuth2ClientRegistrationEndpointConfigurer clientRegistrationResponseHandler(
165+
AuthenticationSuccessHandler clientRegistrationResponseHandler) {
166+
this.clientRegistrationResponseHandler = clientRegistrationResponseHandler;
167+
return this;
168+
}
169+
170+
/**
171+
* Sets the {@link AuthenticationFailureHandler} used for handling an
172+
* {@link OAuth2AuthenticationException} and returning the {@link OAuth2Error Error
173+
* Response}.
174+
* @param errorResponseHandler the {@link AuthenticationFailureHandler} used for
175+
* handling an {@link OAuth2AuthenticationException}
176+
* @return the {@link OAuth2ClientRegistrationEndpointConfigurer} for further
177+
* configuration
178+
*/
179+
public OAuth2ClientRegistrationEndpointConfigurer errorResponseHandler(
180+
AuthenticationFailureHandler errorResponseHandler) {
181+
this.errorResponseHandler = errorResponseHandler;
182+
return this;
183+
}
184+
185+
/**
186+
* Set to {@code true} if open client registration (with no initial access token) is
187+
* allowed. The default is {@code false}.
188+
* @param openRegistrationAllowed {@code true} if open client registration is allowed,
189+
* {@code false} otherwise
190+
* @return the {@link OAuth2ClientRegistrationEndpointConfigurer} for further
191+
* configuration
192+
*/
193+
public OAuth2ClientRegistrationEndpointConfigurer openRegistrationAllowed(boolean openRegistrationAllowed) {
194+
this.openRegistrationAllowed = openRegistrationAllowed;
195+
return this;
196+
}
197+
198+
@Override
199+
void init(HttpSecurity httpSecurity) {
200+
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
201+
.getAuthorizationServerSettings(httpSecurity);
202+
String clientRegistrationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
203+
? OAuth2ConfigurerUtils
204+
.withMultipleIssuersPattern(authorizationServerSettings.getClientRegistrationEndpoint())
205+
: authorizationServerSettings.getClientRegistrationEndpoint();
206+
this.requestMatcher = PathPatternRequestMatcher.withDefaults()
207+
.matcher(HttpMethod.POST, clientRegistrationEndpointUri);
208+
209+
List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity,
210+
this.openRegistrationAllowed);
211+
if (!this.authenticationProviders.isEmpty()) {
212+
authenticationProviders.addAll(0, this.authenticationProviders);
213+
}
214+
this.authenticationProvidersConsumer.accept(authenticationProviders);
215+
authenticationProviders.forEach(
216+
(authenticationProvider) -> httpSecurity.authenticationProvider(postProcess(authenticationProvider)));
217+
}
218+
219+
@Override
220+
void configure(HttpSecurity httpSecurity) {
221+
AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
222+
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
223+
.getAuthorizationServerSettings(httpSecurity);
224+
225+
String clientRegistrationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
226+
? OAuth2ConfigurerUtils
227+
.withMultipleIssuersPattern(authorizationServerSettings.getClientRegistrationEndpoint())
228+
: authorizationServerSettings.getClientRegistrationEndpoint();
229+
OAuth2ClientRegistrationEndpointFilter clientRegistrationEndpointFilter = new OAuth2ClientRegistrationEndpointFilter(
230+
authenticationManager, clientRegistrationEndpointUri);
231+
List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
232+
if (!this.clientRegistrationRequestConverters.isEmpty()) {
233+
authenticationConverters.addAll(0, this.clientRegistrationRequestConverters);
234+
}
235+
this.clientRegistrationRequestConvertersConsumer.accept(authenticationConverters);
236+
clientRegistrationEndpointFilter
237+
.setAuthenticationConverter(new DelegatingAuthenticationConverter(authenticationConverters));
238+
if (this.clientRegistrationResponseHandler != null) {
239+
clientRegistrationEndpointFilter.setAuthenticationSuccessHandler(this.clientRegistrationResponseHandler);
240+
}
241+
if (this.errorResponseHandler != null) {
242+
clientRegistrationEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
243+
}
244+
httpSecurity.addFilterAfter(postProcess(clientRegistrationEndpointFilter), AuthorizationFilter.class);
245+
}
246+
247+
@Override
248+
RequestMatcher getRequestMatcher() {
249+
return this.requestMatcher;
250+
}
251+
252+
private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
253+
List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
254+
255+
authenticationConverters.add(new OAuth2ClientRegistrationAuthenticationConverter());
256+
257+
return authenticationConverters;
258+
}
259+
260+
private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity,
261+
boolean openRegistrationAllowed) {
262+
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
263+
264+
OAuth2ClientRegistrationAuthenticationProvider clientRegistrationAuthenticationProvider = new OAuth2ClientRegistrationAuthenticationProvider(
265+
OAuth2ConfigurerUtils.getRegisteredClientRepository(httpSecurity),
266+
OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity));
267+
PasswordEncoder passwordEncoder = OAuth2ConfigurerUtils.getOptionalBean(httpSecurity, PasswordEncoder.class);
268+
if (passwordEncoder != null) {
269+
clientRegistrationAuthenticationProvider.setPasswordEncoder(passwordEncoder);
270+
}
271+
clientRegistrationAuthenticationProvider.setOpenRegistrationAllowed(openRegistrationAllowed);
272+
authenticationProviders.add(clientRegistrationAuthenticationProvider);
273+
274+
return authenticationProviders;
275+
}
276+
277+
}

0 commit comments

Comments
 (0)