Skip to content

Commit 4e4bb0a

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 spring-projectsgh-355
1 parent 8e8979a commit 4e4bb0a

18 files changed

+1760
-45
lines changed

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

+20-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,23 @@ 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.getProviderSettings(builder),
63+
OAuth2ConfigurerUtils.getJwtEncoder(builder));
64+
OidcClientConfigurationAuthenticationProvider oidcClientConfigurationAuthenticationProvider =
65+
new OidcClientConfigurationAuthenticationProvider(
66+
OAuth2ConfigurerUtils.getRegisteredClientRepository(builder),
67+
OAuth2ConfigurerUtils.getAuthorizationService(builder), providerSettings);
5768
builder.authenticationProvider(postProcess(oidcClientRegistrationAuthenticationProvider));
69+
builder.authenticationProvider(postProcess(oidcClientConfigurationAuthenticationProvider));
5870
}
5971

6072
@Override
@@ -66,7 +78,12 @@ <B extends HttpSecurityBuilder<B>> void configure(B builder) {
6678
new OidcClientRegistrationEndpointFilter(
6779
authenticationManager,
6880
providerSettings.getOidcClientRegistrationEndpoint());
81+
OidcClientConfigurationEndpointFilter oidcClientConfigurationEndpointFilter =
82+
new OidcClientConfigurationEndpointFilter(
83+
authenticationManager,
84+
providerSettings.getOidcClientRegistrationEndpoint());
6985
builder.addFilterAfter(postProcess(oidcClientRegistrationEndpointFilter), FilterSecurityInterceptor.class);
86+
builder.addFilterAfter(postProcess(oidcClientConfigurationEndpointFilter), FilterSecurityInterceptor.class);
7087
}
7188

7289
@Override

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

+19
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,22 @@ 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+
*/
143+
default String getRegistrationAccessToken() {
144+
return getClaimAsString(OidcClientMetadataClaimNames.REGISTRATION_ACCESS_TOKEN);
145+
}
146+
147+
/**
148+
* Returns the {@code URL} of the OAuth 2.0 Client Configuration Endpoint.
149+
*
150+
* @return the {@code URL} of the OAuth 2.0 Client Configuration Endpoint
151+
*/
152+
default URL getRegistrationClientUri() {
153+
return getClaimAsURL(OidcClientMetadataClaimNames.REGISTRATION_CLIENT_URI);
154+
}
155+
137156
}

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

+10
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,14 @@ 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+
*/
89+
String REGISTRATION_ACCESS_TOKEN = "registration_access_token";
90+
91+
/**
92+
* {@code registration_client_uri} - the {@code URL} of the OAuth 2.0 Client Configuration Endpoint
93+
*/
94+
String REGISTRATION_CLIENT_URI = "registration_client_uri";
95+
8696
}

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

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

18+
import java.time.Instant;
19+
import java.util.Collections;
20+
import java.util.Set;
21+
22+
import org.springframework.lang.Nullable;
1823
import org.springframework.security.authentication.AuthenticationProvider;
1924
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
25+
import org.springframework.security.oauth2.core.AuthorizationGrantType;
2026
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
27+
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
28+
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
29+
import org.springframework.security.oauth2.core.oidc.OidcClientRegistration;
30+
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
31+
import org.springframework.security.oauth2.jwt.JoseHeader;
32+
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
2133
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
2234
import org.springframework.security.oauth2.core.OAuth2AuthorizationCode;
35+
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
36+
import org.springframework.util.CollectionUtils;
37+
import org.springframework.util.StringUtils;
38+
import org.springframework.web.util.UriComponentsBuilder;
2339

2440
/**
2541
* Utility methods for the OpenID Connect 1.0 {@link AuthenticationProvider}'s.
2642
*
2743
* @author Joe Grandja
44+
* @author Ovidiu Popa
2845
* @since 0.1.1
2946
*/
3047
final class OidcAuthenticationProviderUtils {
@@ -60,4 +77,75 @@ static <T extends AbstractOAuth2Token> OAuth2Authorization invalidate(
6077

6178
return authorizationBuilder.build();
6279
}
80+
81+
static String registrationClientUri(String issuer, String oidcClientRegistrationEndpoint, String clientId){
82+
return UriComponentsBuilder.fromUriString(issuer)
83+
.path(oidcClientRegistrationEndpoint)
84+
.queryParam("client_id", clientId).toUriString();
85+
}
86+
87+
static OidcClientRegistration convert(RegisteredClient registeredClient, String registrationClientUri,
88+
@Nullable String registrationAccessToken) {
89+
// @formatter:off
90+
OidcClientRegistration.Builder builder = OidcClientRegistration.builder()
91+
.clientId(registeredClient.getClientId())
92+
.clientIdIssuedAt(registeredClient.getClientIdIssuedAt())
93+
.clientSecret(registeredClient.getClientSecret())
94+
.clientName(registeredClient.getClientName());
95+
96+
builder.redirectUris(redirectUris ->
97+
redirectUris.addAll(registeredClient.getRedirectUris()));
98+
99+
builder.grantTypes(grantTypes ->
100+
registeredClient.getAuthorizationGrantTypes().forEach(authorizationGrantType ->
101+
grantTypes.add(authorizationGrantType.getValue())));
102+
103+
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE)) {
104+
builder.responseType(OAuth2AuthorizationResponseType.CODE.getValue());
105+
}
106+
107+
if (!CollectionUtils.isEmpty(registeredClient.getScopes())) {
108+
builder.scopes(scopes ->
109+
scopes.addAll(registeredClient.getScopes()));
110+
}
111+
112+
builder
113+
.tokenEndpointAuthenticationMethod(registeredClient.getClientAuthenticationMethods().iterator().next().getValue())
114+
.idTokenSignedResponseAlgorithm(registeredClient.getTokenSettings().getIdTokenSignatureAlgorithm().getName())
115+
.registrationClientUri(registrationClientUri);
116+
if (StringUtils.hasText(registrationAccessToken)) {
117+
builder.registrationAccessToken(registrationAccessToken);
118+
}
119+
return builder.build();
120+
// @formatter:on
121+
}
122+
123+
static JoseHeader.Builder headers() {
124+
return JoseHeader.withAlgorithm(SignatureAlgorithm.RS256);
125+
}
126+
127+
static JwtClaimsSet.Builder accessTokenClaims(RegisteredClient registeredClient,
128+
String issuer, String subject, Set<String> authorizedScopes) {
129+
130+
Instant issuedAt = Instant.now();
131+
Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getAccessTokenTimeToLive());
132+
133+
// @formatter:off
134+
JwtClaimsSet.Builder claimsBuilder = JwtClaimsSet.builder();
135+
if (StringUtils.hasText(issuer)) {
136+
claimsBuilder.issuer(issuer);
137+
}
138+
claimsBuilder
139+
.subject(subject)
140+
.audience(Collections.singletonList(registeredClient.getClientId()))
141+
.issuedAt(issuedAt)
142+
.expiresAt(expiresAt)
143+
.notBefore(issuedAt);
144+
if (!CollectionUtils.isEmpty(authorizedScopes)) {
145+
claimsBuilder.claim(OAuth2ParameterNames.SCOPE, authorizedScopes);
146+
}
147+
// @formatter:on
148+
149+
return claimsBuilder;
150+
}
63151
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
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 "initial" 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+
invalidateAccessToken(authorization, authorizedAccessToken);
106+
107+
if (registeredClient == null) {
108+
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
109+
}
110+
String registrationClientUri = OidcAuthenticationProviderUtils.registrationClientUri(this.providerSettings.getIssuer(), this.providerSettings.getOidcClientRegistrationEndpoint(), registeredClient.getClientId());
111+
return new OidcClientConfigurationAuthenticationToken(
112+
accessTokenAuthentication, OidcAuthenticationProviderUtils.convert(registeredClient, registrationClientUri, null));
113+
}
114+
115+
private void invalidateAccessToken(OAuth2Authorization authorization, OAuth2Authorization.Token<OAuth2AccessToken> authorizedAccessToken) {
116+
// Invalidate the "initial" access token as it can only be used once
117+
authorization = OidcAuthenticationProviderUtils.invalidate(authorization, authorizedAccessToken.getToken());
118+
if (authorization.getRefreshToken() != null) {
119+
authorization = OidcAuthenticationProviderUtils.invalidate(authorization, authorization.getRefreshToken().getToken());
120+
}
121+
122+
this.authorizationService.save(authorization);
123+
}
124+
125+
@Override
126+
public boolean supports(Class<?> authentication) {
127+
return OidcClientConfigurationAuthenticationToken.class.isAssignableFrom(authentication);
128+
}
129+
130+
@SuppressWarnings("unchecked")
131+
private static boolean isAuthorized(OAuth2Authorization.Token<OAuth2AccessToken> authorizedAccessToken) {
132+
Map<String, Object> accessTokenClaims = authorizedAccessToken.getClaims();
133+
Object scope = accessTokenClaims != null ? accessTokenClaims.get(OAuth2ParameterNames.SCOPE) : null;
134+
return scope != null && ((Collection<String>) scope).contains(DEFAULT_AUTHORIZED_SCOPE);
135+
}
136+
}

0 commit comments

Comments
 (0)