Skip to content

Commit fbaaa60

Browse files
committed
Extract OIDC client configuration implementation
Closes spring-projectsgh-941
1 parent c3dbc70 commit fbaaa60

File tree

10 files changed

+846
-405
lines changed

10 files changed

+846
-405
lines changed

docs/src/docs/asciidoc/protocol-endpoints.adoc

+2-1
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,8 @@ The OpenID Connect 1.0 Client Registration endpoint is disabled by default becau
369369

370370
`OidcClientRegistrationEndpointFilter` is configured with the following defaults:
371371

372-
* `*AuthenticationManager*` -- An `AuthenticationManager` composed of `OidcClientRegistrationAuthenticationProvider`.
372+
* `*AuthenticationConverter*` -- An `OidcClientRegistrationAuthenticationConverter`.
373+
* `*AuthenticationManager*` -- An `AuthenticationManager` composed of `OidcClientRegistrationAuthenticationProvider` and `OidcClientConfigurationAuthenticationProvider`.
373374

374375
The OpenID Connect 1.0 Client Registration endpoint is an https://openid.net/specs/openid-connect-registration-1_0.html#ClientRegistration[OAuth2 protected resource], which *REQUIRES* an access token to be sent as a bearer token in the Client Registration (or Client Read) request.
375376

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

+7
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import org.springframework.security.authentication.AuthenticationManager;
2020
import org.springframework.security.config.annotation.ObjectPostProcessor;
2121
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
22+
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientConfigurationAuthenticationProvider;
2223
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationProvider;
2324
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcClientRegistrationEndpointFilter;
2425
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
@@ -59,6 +60,12 @@ void init(HttpSecurity httpSecurity) {
5960
OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity),
6061
OAuth2ConfigurerUtils.getTokenGenerator(httpSecurity));
6162
httpSecurity.authenticationProvider(postProcess(oidcClientRegistrationAuthenticationProvider));
63+
64+
OidcClientConfigurationAuthenticationProvider oidcClientConfigurationAuthenticationProvider =
65+
new OidcClientConfigurationAuthenticationProvider(
66+
OAuth2ConfigurerUtils.getRegisteredClientRepository(httpSecurity),
67+
OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity));
68+
httpSecurity.authenticationProvider(postProcess(oidcClientConfigurationAuthenticationProvider));
6269
}
6370

6471
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*
2+
* Copyright 2020-2022 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.Collections;
20+
import java.util.Set;
21+
22+
import org.springframework.core.convert.converter.Converter;
23+
import org.springframework.security.authentication.AuthenticationProvider;
24+
import org.springframework.security.core.Authentication;
25+
import org.springframework.security.core.AuthenticationException;
26+
import org.springframework.security.oauth2.core.OAuth2AccessToken;
27+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
28+
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
29+
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
30+
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
31+
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
32+
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
33+
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
34+
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
35+
import org.springframework.security.oauth2.server.authorization.oidc.OidcClientRegistration;
36+
import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken;
37+
import org.springframework.util.Assert;
38+
import org.springframework.util.StringUtils;
39+
40+
/**
41+
* An {@link AuthenticationProvider} implementation for OpenID Connect 1.0 Dynamic Client Configuration Endpoint.
42+
*
43+
* @author Ovidiu Popa
44+
* @author Joe Grandja
45+
* @author Rafal Lewczuk
46+
* @since 0.4.0
47+
* @see RegisteredClientRepository
48+
* @see OAuth2AuthorizationService
49+
* @see OidcClientRegistrationAuthenticationProvider
50+
* @see <a href="https://openid.net/specs/openid-connect-registration-1_0.html#ClientConfigurationEndpoint">4. Client Configuration Endpoint</a>
51+
*/
52+
public final class OidcClientConfigurationAuthenticationProvider implements AuthenticationProvider {
53+
static final String DEFAULT_CLIENT_CONFIGURATION_AUTHORIZED_SCOPE = "client.read";
54+
private final RegisteredClientRepository registeredClientRepository;
55+
private final OAuth2AuthorizationService authorizationService;
56+
private final Converter<RegisteredClient, OidcClientRegistration> clientRegistrationConverter;
57+
58+
/**
59+
* Constructs an {@code OidcClientConfigurationAuthenticationProvider} using the provided parameters.
60+
*
61+
* @param registeredClientRepository the repository of registered clients
62+
* @param authorizationService the authorization service
63+
*/
64+
public OidcClientConfigurationAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
65+
OAuth2AuthorizationService authorizationService) {
66+
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
67+
Assert.notNull(authorizationService, "authorizationService cannot be null");
68+
this.registeredClientRepository = registeredClientRepository;
69+
this.authorizationService = authorizationService;
70+
this.clientRegistrationConverter = new OidcClientRegistrationConverter();
71+
}
72+
73+
@Override
74+
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
75+
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication =
76+
(OidcClientRegistrationAuthenticationToken) authentication;
77+
78+
if (!StringUtils.hasText(clientRegistrationAuthentication.getClientId())) {
79+
// This is not a Client Configuration Request.
80+
// Return null to allow OidcClientRegistrationAuthenticationProvider to handle it.
81+
return null;
82+
}
83+
84+
// Validate the "registration" access token
85+
AbstractOAuth2TokenAuthenticationToken<?> accessTokenAuthentication = null;
86+
if (AbstractOAuth2TokenAuthenticationToken.class.isAssignableFrom(clientRegistrationAuthentication.getPrincipal().getClass())) {
87+
accessTokenAuthentication = (AbstractOAuth2TokenAuthenticationToken<?>) clientRegistrationAuthentication.getPrincipal();
88+
}
89+
if (accessTokenAuthentication == null || !accessTokenAuthentication.isAuthenticated()) {
90+
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
91+
}
92+
93+
String accessTokenValue = accessTokenAuthentication.getToken().getTokenValue();
94+
OAuth2Authorization authorization = this.authorizationService.findByToken(
95+
accessTokenValue, OAuth2TokenType.ACCESS_TOKEN);
96+
if (authorization == null) {
97+
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
98+
}
99+
100+
OAuth2Authorization.Token<OAuth2AccessToken> authorizedAccessToken = authorization.getAccessToken();
101+
if (!authorizedAccessToken.isActive()) {
102+
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
103+
}
104+
checkScope(authorizedAccessToken, Collections.singleton(DEFAULT_CLIENT_CONFIGURATION_AUTHORIZED_SCOPE));
105+
106+
return findRegistration(clientRegistrationAuthentication, authorization);
107+
}
108+
109+
@Override
110+
public boolean supports(Class<?> authentication) {
111+
return OidcClientRegistrationAuthenticationToken.class.isAssignableFrom(authentication);
112+
}
113+
114+
private OidcClientRegistrationAuthenticationToken findRegistration(OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication,
115+
OAuth2Authorization authorization) {
116+
117+
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(
118+
clientRegistrationAuthentication.getClientId());
119+
if (registeredClient == null) {
120+
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
121+
}
122+
123+
if (!registeredClient.getId().equals(authorization.getRegisteredClientId())) {
124+
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
125+
}
126+
127+
OidcClientRegistration clientRegistration = this.clientRegistrationConverter.convert(registeredClient);
128+
129+
return new OidcClientRegistrationAuthenticationToken(
130+
(Authentication) clientRegistrationAuthentication.getPrincipal(), clientRegistration);
131+
}
132+
133+
@SuppressWarnings("unchecked")
134+
private static void checkScope(OAuth2Authorization.Token<OAuth2AccessToken> authorizedAccessToken, Set<String> requiredScope) {
135+
Collection<String> authorizedScope = Collections.emptySet();
136+
if (authorizedAccessToken.getClaims().containsKey(OAuth2ParameterNames.SCOPE)) {
137+
authorizedScope = (Collection<String>) authorizedAccessToken.getClaims().get(OAuth2ParameterNames.SCOPE);
138+
}
139+
if (!authorizedScope.containsAll(requiredScope)) {
140+
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INSUFFICIENT_SCOPE);
141+
} else if (authorizedScope.size() != requiredScope.size()) {
142+
// Restrict the access token to only contain the required scope
143+
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
144+
}
145+
}
146+
147+
}

0 commit comments

Comments
 (0)