Skip to content

Commit 7c864b5

Browse files
committed
Implement Client Configuration Endpoint
See: https://openid.net/specs/openid-connect-registration-1_0.html#ClientConfigurationEndpoint Generate registration_client_uri and registration_access_token when registering a new client (see: https://openid.net/specs/openid-connect-registration-1_0.html#ClientRegistration) Closes gh-355
1 parent 8e8979a commit 7c864b5

18 files changed

+1918
-47
lines changed

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

+21-3
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@
2020
import org.springframework.security.config.annotation.ObjectPostProcessor;
2121
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
2222
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
23+
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientConfigurationAuthenticationProvider;
2324
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationProvider;
25+
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcClientConfigurationEndpointFilter;
2426
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcClientRegistrationEndpointFilter;
2527
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
2628
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
29+
import org.springframework.security.web.util.matcher.OrRequestMatcher;
2730
import org.springframework.security.web.util.matcher.RequestMatcher;
2831

2932
/**
@@ -47,14 +50,24 @@ public final class OidcClientRegistrationEndpointConfigurer extends AbstractOAut
4750
@Override
4851
<B extends HttpSecurityBuilder<B>> void init(B builder) {
4952
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
50-
this.requestMatcher = new AntPathRequestMatcher(
51-
providerSettings.getOidcClientRegistrationEndpoint(), HttpMethod.POST.name());
53+
this.requestMatcher = new OrRequestMatcher(
54+
new AntPathRequestMatcher(providerSettings.getOidcClientRegistrationEndpoint(), HttpMethod.POST.name()),
55+
new AntPathRequestMatcher(providerSettings.getOidcClientRegistrationEndpoint(), HttpMethod.GET.name())
56+
);
5257

5358
OidcClientRegistrationAuthenticationProvider oidcClientRegistrationAuthenticationProvider =
5459
new OidcClientRegistrationAuthenticationProvider(
5560
OAuth2ConfigurerUtils.getRegisteredClientRepository(builder),
56-
OAuth2ConfigurerUtils.getAuthorizationService(builder));
61+
OAuth2ConfigurerUtils.getAuthorizationService(builder),
62+
OAuth2ConfigurerUtils.getJwtEncoder(builder),
63+
providerSettings);
64+
OidcClientConfigurationAuthenticationProvider oidcClientConfigurationAuthenticationProvider =
65+
new OidcClientConfigurationAuthenticationProvider(
66+
OAuth2ConfigurerUtils.getRegisteredClientRepository(builder),
67+
OAuth2ConfigurerUtils.getAuthorizationService(builder),
68+
providerSettings);
5769
builder.authenticationProvider(postProcess(oidcClientRegistrationAuthenticationProvider));
70+
builder.authenticationProvider(postProcess(oidcClientConfigurationAuthenticationProvider));
5871
}
5972

6073
@Override
@@ -66,7 +79,12 @@ <B extends HttpSecurityBuilder<B>> void configure(B builder) {
6679
new OidcClientRegistrationEndpointFilter(
6780
authenticationManager,
6881
providerSettings.getOidcClientRegistrationEndpoint());
82+
OidcClientConfigurationEndpointFilter oidcClientConfigurationEndpointFilter =
83+
new OidcClientConfigurationEndpointFilter(
84+
authenticationManager,
85+
providerSettings.getOidcClientRegistrationEndpoint());
6986
builder.addFilterAfter(postProcess(oidcClientRegistrationEndpointFilter), FilterSecurityInterceptor.class);
87+
builder.addFilterAfter(postProcess(oidcClientConfigurationEndpointFilter), FilterSecurityInterceptor.class);
7088
}
7189

7290
@Override

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcClientMetadataClaimAccessor.java

+21
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.security.oauth2.core.oidc;
1717

18+
import java.net.URL;
1819
import java.time.Instant;
1920
import java.util.List;
2021

@@ -134,4 +135,24 @@ default String getIdTokenSignedResponseAlgorithm() {
134135
return getClaimAsString(OidcClientMetadataClaimNames.ID_TOKEN_SIGNED_RESPONSE_ALG);
135136
}
136137

138+
/**
139+
* Returns the Registration Access Token that can be used at the Client Configuration Endpoint.
140+
*
141+
* @return the Registration Access Token that can be used at the Client Configuration Endpoint
142+
* @since 0.2.1
143+
*/
144+
default String getRegistrationAccessToken() {
145+
return getClaimAsString(OidcClientMetadataClaimNames.REGISTRATION_ACCESS_TOKEN);
146+
}
147+
148+
/**
149+
* Returns the {@code URL} of the OAuth 2.0 Client Configuration Endpoint.
150+
*
151+
* @return the {@code URL} of the OAuth 2.0 Client Configuration Endpoint
152+
* @since 0.2.1
153+
*/
154+
default URL getRegistrationClientUri() {
155+
return getClaimAsURL(OidcClientMetadataClaimNames.REGISTRATION_CLIENT_URI);
156+
}
157+
137158
}

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcClientMetadataClaimNames.java

+12
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,16 @@ public interface OidcClientMetadataClaimNames {
8383
*/
8484
String ID_TOKEN_SIGNED_RESPONSE_ALG = "id_token_signed_response_alg";
8585

86+
/**
87+
* {@code registration_access_token} - Registration Access Token that can be used at the Client Configuration Endpoint to perform subsequent operations upon the Client registration
88+
* @since 0.2.1
89+
*/
90+
String REGISTRATION_ACCESS_TOKEN = "registration_access_token";
91+
92+
/**
93+
* {@code registration_client_uri} - the {@code URL} of the OAuth 2.0 Client Configuration Endpoint
94+
* @since 0.2.1
95+
*/
96+
String REGISTRATION_CLIENT_URI = "registration_client_uri";
97+
8698
}

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcClientRegistration.java

+20
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,26 @@ public Builder idTokenSignedResponseAlgorithm(String idTokenSignedResponseAlgori
251251
return claim(OidcClientMetadataClaimNames.ID_TOKEN_SIGNED_RESPONSE_ALG, idTokenSignedResponseAlgorithm);
252252
}
253253

254+
/**
255+
* Sets the Registration Access Token that can be used at the Client Configuration Endpoint to perform subsequent operations upon the Client registration, OPTIONAL.
256+
*
257+
* @param registrationAccessToken the Registration Access Token that can be used at the Client Configuration Endpoint to perform subsequent operations upon the Client registration
258+
* @return the {@link Builder} for further configuration
259+
*/
260+
public Builder registrationAccessToken(String registrationAccessToken) {
261+
return claim(OidcClientMetadataClaimNames.REGISTRATION_ACCESS_TOKEN, registrationAccessToken);
262+
}
263+
264+
/**
265+
* Sets the {@code URL} of the OAuth 2.0 Client Configuration Endpoint, OPTIONAL.
266+
*
267+
* @param registrationClientUri the {@code URL} of the OAuth 2.0 Client Configuration Endpoint
268+
* @return the {@link Builder} for further configuration
269+
*/
270+
public Builder registrationClientUri(String registrationClientUri) {
271+
return claim(OidcClientMetadataClaimNames.REGISTRATION_CLIENT_URI, registrationClientUri);
272+
}
273+
254274
/**
255275
* Sets the claim.
256276
*

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcAuthenticationProviderUtils.java

+53-1
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,26 @@
1515
*/
1616
package org.springframework.security.oauth2.server.authorization.oidc.authentication;
1717

18+
import org.springframework.lang.Nullable;
1819
import org.springframework.security.authentication.AuthenticationProvider;
1920
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
21+
import org.springframework.security.oauth2.core.AuthorizationGrantType;
22+
import org.springframework.security.oauth2.core.OAuth2AuthorizationCode;
2023
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
24+
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
25+
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
26+
import org.springframework.security.oauth2.core.oidc.OidcClientRegistration;
2127
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
22-
import org.springframework.security.oauth2.core.OAuth2AuthorizationCode;
28+
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
29+
import org.springframework.util.CollectionUtils;
30+
import org.springframework.util.StringUtils;
31+
import org.springframework.web.util.UriComponentsBuilder;
2332

2433
/**
2534
* Utility methods for the OpenID Connect 1.0 {@link AuthenticationProvider}'s.
2635
*
2736
* @author Joe Grandja
37+
* @author Ovidiu Popa
2838
* @since 0.1.1
2939
*/
3040
final class OidcAuthenticationProviderUtils {
@@ -60,4 +70,46 @@ static <T extends AbstractOAuth2Token> OAuth2Authorization invalidate(
6070

6171
return authorizationBuilder.build();
6272
}
73+
74+
static String registrationClientUri(String issuer, String oidcClientRegistrationEndpoint, String clientId){
75+
return UriComponentsBuilder.fromUriString(issuer)
76+
.path(oidcClientRegistrationEndpoint)
77+
.queryParam(OAuth2ParameterNames.CLIENT_ID, clientId).toUriString();
78+
}
79+
80+
static OidcClientRegistration convert(RegisteredClient registeredClient, String registrationClientUri,
81+
@Nullable String registrationAccessToken) {
82+
// @formatter:off
83+
OidcClientRegistration.Builder builder = OidcClientRegistration.builder()
84+
.clientId(registeredClient.getClientId())
85+
.clientIdIssuedAt(registeredClient.getClientIdIssuedAt())
86+
.clientSecret(registeredClient.getClientSecret())
87+
.clientName(registeredClient.getClientName());
88+
89+
builder.redirectUris(redirectUris ->
90+
redirectUris.addAll(registeredClient.getRedirectUris()));
91+
92+
builder.grantTypes(grantTypes ->
93+
registeredClient.getAuthorizationGrantTypes().forEach(authorizationGrantType ->
94+
grantTypes.add(authorizationGrantType.getValue())));
95+
96+
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE)) {
97+
builder.responseType(OAuth2AuthorizationResponseType.CODE.getValue());
98+
}
99+
100+
if (!CollectionUtils.isEmpty(registeredClient.getScopes())) {
101+
builder.scopes(scopes ->
102+
scopes.addAll(registeredClient.getScopes()));
103+
}
104+
105+
builder
106+
.tokenEndpointAuthenticationMethod(registeredClient.getClientAuthenticationMethods().iterator().next().getValue())
107+
.idTokenSignedResponseAlgorithm(registeredClient.getTokenSettings().getIdTokenSignatureAlgorithm().getName())
108+
.registrationClientUri(registrationClientUri);
109+
if (StringUtils.hasText(registrationAccessToken)) {
110+
builder.registrationAccessToken(registrationAccessToken);
111+
}
112+
return builder.build();
113+
// @formatter:on
114+
}
63115
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Copyright 2020-2021 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+
package org.springframework.security.oauth2.server.authorization.oidc.authentication;
17+
18+
import java.util.Collection;
19+
import java.util.Map;
20+
21+
import org.springframework.security.authentication.AuthenticationProvider;
22+
import org.springframework.security.core.Authentication;
23+
import org.springframework.security.core.AuthenticationException;
24+
import org.springframework.security.oauth2.core.OAuth2AccessToken;
25+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
26+
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
27+
import org.springframework.security.oauth2.core.OAuth2TokenType;
28+
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
29+
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
30+
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
31+
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
32+
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
33+
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
34+
import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken;
35+
import org.springframework.util.Assert;
36+
37+
/**
38+
* An {@link AuthenticationProvider} implementation for OpenID Connect Client Configuration 1.0.
39+
*
40+
* @author Ovidiu Popa
41+
* @since 0.2.1
42+
* @see RegisteredClientRepository
43+
* @see OAuth2AuthorizationService
44+
* @see <a href="https://openid.net/specs/openid-connect-registration-1_0.html#ClientConfigurationEndpoint">4. Client Configuration Endpoint</a>
45+
*/
46+
public final class OidcClientConfigurationAuthenticationProvider implements AuthenticationProvider {
47+
48+
private static final String DEFAULT_AUTHORIZED_SCOPE = "client.read";
49+
50+
private final RegisteredClientRepository registeredClientRepository;
51+
private final OAuth2AuthorizationService authorizationService;
52+
private final ProviderSettings providerSettings;
53+
54+
/**
55+
* Constructs an {@code OidcClientConfigurationAuthenticationProvider} using the provided parameters.
56+
*
57+
* @param registeredClientRepository the repository of registered clients
58+
* @param authorizationService the authorization service
59+
* @param providerSettings the provider settings
60+
*/
61+
public OidcClientConfigurationAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
62+
OAuth2AuthorizationService authorizationService, ProviderSettings providerSettings) {
63+
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
64+
Assert.notNull(authorizationService, "authorizationService cannot be null");
65+
Assert.notNull(providerSettings, "providerSettings cannot be null");
66+
this.registeredClientRepository = registeredClientRepository;
67+
this.authorizationService = authorizationService;
68+
this.providerSettings = providerSettings;
69+
}
70+
71+
@Override
72+
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
73+
OidcClientConfigurationAuthenticationToken clientConfigurationAuthentication =
74+
(OidcClientConfigurationAuthenticationToken) authentication;
75+
76+
// Validate the "registration" access token
77+
AbstractOAuth2TokenAuthenticationToken<?> accessTokenAuthentication = null;
78+
if (AbstractOAuth2TokenAuthenticationToken.class.isAssignableFrom(clientConfigurationAuthentication.getPrincipal().getClass())) {
79+
accessTokenAuthentication = (AbstractOAuth2TokenAuthenticationToken<?>) clientConfigurationAuthentication.getPrincipal();
80+
}
81+
if (accessTokenAuthentication == null || !accessTokenAuthentication.isAuthenticated()) {
82+
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
83+
}
84+
85+
String accessTokenValue = accessTokenAuthentication.getToken().getTokenValue();
86+
87+
OAuth2Authorization authorization = this.authorizationService.findByToken(
88+
accessTokenValue, OAuth2TokenType.ACCESS_TOKEN);
89+
if (authorization == null) {
90+
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
91+
}
92+
93+
OAuth2Authorization.Token<OAuth2AccessToken> authorizedAccessToken = authorization.getAccessToken();
94+
if (!authorizedAccessToken.isActive()) {
95+
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
96+
}
97+
98+
if (!isAuthorized(authorizedAccessToken)) {
99+
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INSUFFICIENT_SCOPE);
100+
}
101+
102+
RegisteredClient registeredClient = this.registeredClientRepository
103+
.findByClientId(clientConfigurationAuthentication.getClientId());
104+
105+
if (registeredClient == null) {
106+
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
107+
}
108+
if (!registeredClient.getId().equals(authorization.getRegisteredClientId())) {
109+
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
110+
}
111+
112+
String registrationClientUri = OidcAuthenticationProviderUtils.registrationClientUri(this.providerSettings.getIssuer(), this.providerSettings.getOidcClientRegistrationEndpoint(), registeredClient.getClientId());
113+
return new OidcClientConfigurationAuthenticationToken(
114+
accessTokenAuthentication, OidcAuthenticationProviderUtils.convert(registeredClient, registrationClientUri, null));
115+
}
116+
117+
@Override
118+
public boolean supports(Class<?> authentication) {
119+
return OidcClientConfigurationAuthenticationToken.class.isAssignableFrom(authentication);
120+
}
121+
122+
@SuppressWarnings("unchecked")
123+
private static boolean isAuthorized(OAuth2Authorization.Token<OAuth2AccessToken> authorizedAccessToken) {
124+
Map<String, Object> accessTokenClaims = authorizedAccessToken.getClaims();
125+
Object scope = accessTokenClaims != null ? accessTokenClaims.get(OAuth2ParameterNames.SCOPE) : null;
126+
return scope != null && ((Collection<String>) scope).contains(DEFAULT_AUTHORIZED_SCOPE);
127+
}
128+
}

0 commit comments

Comments
 (0)