diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcClientRegistrationEndpointConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcClientRegistrationEndpointConfigurer.java index dda93881f..248c81352 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcClientRegistrationEndpointConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcClientRegistrationEndpointConfigurer.java @@ -24,6 +24,7 @@ import org.springframework.security.oauth2.server.authorization.oidc.web.OidcClientRegistrationEndpointFilter; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; /** @@ -47,13 +48,16 @@ public final class OidcClientRegistrationEndpointConfigurer extends AbstractOAut @Override > void init(B builder) { ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder); - this.requestMatcher = new AntPathRequestMatcher( - providerSettings.getOidcClientRegistrationEndpoint(), HttpMethod.POST.name()); + this.requestMatcher = new OrRequestMatcher( + new AntPathRequestMatcher(providerSettings.getOidcClientRegistrationEndpoint(), HttpMethod.POST.name()), + new AntPathRequestMatcher(providerSettings.getOidcClientRegistrationEndpoint(), HttpMethod.GET.name()) + ); OidcClientRegistrationAuthenticationProvider oidcClientRegistrationAuthenticationProvider = new OidcClientRegistrationAuthenticationProvider( OAuth2ConfigurerUtils.getRegisteredClientRepository(builder), - OAuth2ConfigurerUtils.getAuthorizationService(builder)); + OAuth2ConfigurerUtils.getAuthorizationService(builder), + OAuth2ConfigurerUtils.getJwtEncoder(builder)); builder.authenticationProvider(postProcess(oidcClientRegistrationAuthenticationProvider)); } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcClientMetadataClaimAccessor.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcClientMetadataClaimAccessor.java index c91b9eb0c..a6630476f 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcClientMetadataClaimAccessor.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcClientMetadataClaimAccessor.java @@ -15,6 +15,7 @@ */ package org.springframework.security.oauth2.core.oidc; +import java.net.URL; import java.time.Instant; import java.util.List; @@ -134,4 +135,24 @@ default String getIdTokenSignedResponseAlgorithm() { return getClaimAsString(OidcClientMetadataClaimNames.ID_TOKEN_SIGNED_RESPONSE_ALG); } + /** + * Returns the Registration Access Token that can be used at the Client Configuration Endpoint. + * + * @return the Registration Access Token that can be used at the Client Configuration Endpoint + * @since 0.2.1 + */ + default String getRegistrationAccessToken() { + return getClaimAsString(OidcClientMetadataClaimNames.REGISTRATION_ACCESS_TOKEN); + } + + /** + * Returns the {@code URL} of the OAuth 2.0 Client Configuration Endpoint. + * + * @return the {@code URL} of the OAuth 2.0 Client Configuration Endpoint + * @since 0.2.1 + */ + default URL getRegistrationClientUri() { + return getClaimAsURL(OidcClientMetadataClaimNames.REGISTRATION_CLIENT_URI); + } + } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcClientMetadataClaimNames.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcClientMetadataClaimNames.java index 63a5d2054..786f4db9b 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcClientMetadataClaimNames.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcClientMetadataClaimNames.java @@ -83,4 +83,16 @@ public interface OidcClientMetadataClaimNames { */ String ID_TOKEN_SIGNED_RESPONSE_ALG = "id_token_signed_response_alg"; + /** + * {@code registration_access_token} - Registration Access Token that can be used at the Client Configuration Endpoint to perform subsequent operations upon the Client registration + * @since 0.2.1 + */ + String REGISTRATION_ACCESS_TOKEN = "registration_access_token"; + + /** + * {@code registration_client_uri} - the {@code URL} of the OAuth 2.0 Client Configuration Endpoint + * @since 0.2.1 + */ + String REGISTRATION_CLIENT_URI = "registration_client_uri"; + } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcClientRegistration.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcClientRegistration.java index e98183a3a..2361a775f 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcClientRegistration.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcClientRegistration.java @@ -251,6 +251,26 @@ public Builder idTokenSignedResponseAlgorithm(String idTokenSignedResponseAlgori return claim(OidcClientMetadataClaimNames.ID_TOKEN_SIGNED_RESPONSE_ALG, idTokenSignedResponseAlgorithm); } + /** + * Sets the Registration Access Token that can be used at the Client Configuration Endpoint to perform subsequent operations upon the Client registration, OPTIONAL. + * + * @param registrationAccessToken the Registration Access Token that can be used at the Client Configuration Endpoint to perform subsequent operations upon the Client registration + * @return the {@link Builder} for further configuration + */ + public Builder registrationAccessToken(String registrationAccessToken) { + return claim(OidcClientMetadataClaimNames.REGISTRATION_ACCESS_TOKEN, registrationAccessToken); + } + + /** + * Sets the {@code URL} of the OAuth 2.0 Client Configuration Endpoint, OPTIONAL. + * + * @param registrationClientUri the {@code URL} of the OAuth 2.0 Client Configuration Endpoint + * @return the {@link Builder} for further configuration + */ + public Builder registrationClientUri(String registrationClientUri) { + return claim(OidcClientMetadataClaimNames.REGISTRATION_CLIENT_URI, registrationClientUri); + } + /** * Sets the claim. * diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/JwtUtils.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/JwtUtils.java new file mode 100644 index 000000000..a477d689c --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/JwtUtils.java @@ -0,0 +1,72 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.server.authorization.oidc.authentication; + +import java.time.Instant; +import java.util.Collections; +import java.util.Set; + +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.security.oauth2.jwt.JoseHeader; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimsSet; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +/** + * + * Utility methods used by the {@link OidcClientRegistrationAuthenticationProvider} when issuing {@link Jwt}'s. + * @author Ovidiu Popa + * @since 0.2.1 + */ +final class JwtUtils { + + //TODO Duplicate of {@code org.springframework.security.oauth2.server.authorization.authentication.JwtUtils}. To be refactored + private JwtUtils() { + } + + static JoseHeader.Builder headers() { + return JoseHeader.withAlgorithm(SignatureAlgorithm.RS256); + } + + static JwtClaimsSet.Builder accessTokenClaims(RegisteredClient registeredClient, + String issuer, String subject, Set authorizedScopes) { + + Instant issuedAt = Instant.now(); + Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getAccessTokenTimeToLive()); + + // @formatter:off + JwtClaimsSet.Builder claimsBuilder = JwtClaimsSet.builder(); + if (StringUtils.hasText(issuer)) { + claimsBuilder.issuer(issuer); + } + claimsBuilder + .subject(subject) + .audience(Collections.singletonList(registeredClient.getClientId())) + .issuedAt(issuedAt) + .expiresAt(expiresAt) + .notBefore(issuedAt); + if (!CollectionUtils.isEmpty(authorizedScopes)) { + claimsBuilder.claim(OAuth2ParameterNames.SCOPE, authorizedScopes); + } + // @formatter:on + + return claimsBuilder; + } + +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProvider.java index 88a9023e2..d4b83a2f9 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProvider.java @@ -20,9 +20,13 @@ import java.time.Instant; import java.util.Base64; import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.UUID; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.Nullable; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -38,18 +42,26 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.oidc.OidcClientRegistration; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.security.oauth2.jwt.JoseHeader; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimsSet; +import org.springframework.security.oauth2.jwt.JwtEncoder; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.config.ClientSettings; +import org.springframework.security.oauth2.server.authorization.config.ProviderSettings; import org.springframework.security.oauth2.server.authorization.config.TokenSettings; import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.util.UriComponentsBuilder; /** - * An {@link AuthenticationProvider} implementation for OpenID Connect Dynamic Client Registration 1.0. + * An {@link AuthenticationProvider} implementation for OpenID Connect Dynamic Client Registration 1.0 and + * OpenID Connect Client Configuration 1.0. * * @author Ovidiu Popa * @author Joe Grandja @@ -57,28 +69,40 @@ * @see RegisteredClientRepository * @see OAuth2AuthorizationService * @see 3. Client Registration Endpoint + * @see 4. Client Configuration Endpoint */ public final class OidcClientRegistrationAuthenticationProvider implements AuthenticationProvider { private static final StringKeyGenerator CLIENT_ID_GENERATOR = new Base64StringKeyGenerator( Base64.getUrlEncoder().withoutPadding(), 32); private static final StringKeyGenerator CLIENT_SECRET_GENERATOR = new Base64StringKeyGenerator( Base64.getUrlEncoder().withoutPadding(), 48); - private static final String DEFAULT_AUTHORIZED_SCOPE = "client.create"; + private static final String DEFAULT_CLIENT_REGISTRATION_AUTHORIZED_SCOPE = "client.create"; + private static final String DEFAULT_CLIENT_CONFIGURATION_AUTHORIZED_SCOPE = "client.read"; private final RegisteredClientRepository registeredClientRepository; private final OAuth2AuthorizationService authorizationService; + private final JwtEncoder jwtEncoder; + private ProviderSettings providerSettings; /** * Constructs an {@code OidcClientRegistrationAuthenticationProvider} using the provided parameters. * * @param registeredClientRepository the repository of registered clients * @param authorizationService the authorization service + * @param jwtEncoder the jwt encoder */ public OidcClientRegistrationAuthenticationProvider(RegisteredClientRepository registeredClientRepository, - OAuth2AuthorizationService authorizationService) { + OAuth2AuthorizationService authorizationService, JwtEncoder jwtEncoder) { Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null"); Assert.notNull(authorizationService, "authorizationService cannot be null"); + Assert.notNull(jwtEncoder, "jwtEncoder cannot be null"); this.registeredClientRepository = registeredClientRepository; this.authorizationService = authorizationService; + this.jwtEncoder = jwtEncoder; + } + + @Autowired(required = false) + protected void setProviderSettings(ProviderSettings providerSettings) { + this.providerSettings = providerSettings; } @Override @@ -86,7 +110,7 @@ public Authentication authenticate(Authentication authentication) throws Authent OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication = (OidcClientRegistrationAuthenticationToken) authentication; - // Validate the "initial" access token + // Validate the "initial" and the registration access token AbstractOAuth2TokenAuthenticationToken accessTokenAuthentication = null; if (AbstractOAuth2TokenAuthenticationToken.class.isAssignableFrom(clientRegistrationAuthentication.getPrincipal().getClass())) { accessTokenAuthentication = (AbstractOAuth2TokenAuthenticationToken) clientRegistrationAuthentication.getPrincipal(); @@ -108,7 +132,36 @@ public Authentication authenticate(Authentication authentication) throws Authent throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN); } - if (!isAuthorized(authorizedAccessToken)) { + if (clientRegistrationAuthentication.getClientRegistration() != null) { + + return registerClient(clientRegistrationAuthentication, authorization); + } + + if (isNotAuthorized(authorizedAccessToken, DEFAULT_CLIENT_CONFIGURATION_AUTHORIZED_SCOPE)) { + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INSUFFICIENT_SCOPE); + } + + RegisteredClient registeredClient = this.registeredClientRepository + .findByClientId(clientRegistrationAuthentication.getClientId()); + + if (registeredClient == null) { + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT); + } + if (!registeredClient.getId().equals(authorization.getRegisteredClientId())) { + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT); + } + + String registrationClientUri = registrationClientUri(getIssuer(), this.providerSettings.getOidcClientRegistrationEndpoint(), registeredClient.getClientId()); + return new OidcClientRegistrationAuthenticationToken(accessTokenAuthentication, + convert(registeredClient, registrationClientUri, null)); + + } + + private OidcClientRegistrationAuthenticationToken registerClient(OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication, + OAuth2Authorization authorization) { + + OAuth2Authorization.Token authorizedAccessToken = authorization.getAccessToken(); + if (isNotAuthorized(authorizedAccessToken, DEFAULT_CLIENT_REGISTRATION_AUTHORIZED_SCOPE)) { throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INSUFFICIENT_SCOPE); } @@ -126,9 +179,12 @@ public Authentication authenticate(Authentication authentication) throws Authent authorization = OidcAuthenticationProviderUtils.invalidate(authorization, authorization.getRefreshToken().getToken()); } this.authorizationService.save(authorization); + String registrationClientUri = registrationClientUri(getIssuer(), this.providerSettings.getOidcClientRegistrationEndpoint(), registeredClient.getClientId()); + String registrationAccessToken = registerAccessToken(registeredClient) + .getAccessToken().getToken().getTokenValue(); - return new OidcClientRegistrationAuthenticationToken( - accessTokenAuthentication, convert(registeredClient)); + return new OidcClientRegistrationAuthenticationToken((AbstractOAuth2TokenAuthenticationToken) clientRegistrationAuthentication.getPrincipal(), + convert(registeredClient, registrationClientUri, registrationAccessToken)); } @Override @@ -136,10 +192,44 @@ public boolean supports(Class authentication) { return OidcClientRegistrationAuthenticationToken.class.isAssignableFrom(authentication); } + private String getIssuer() { + return this.providerSettings != null ? this.providerSettings.getIssuer() : null; + } + + private OAuth2Authorization registerAccessToken(RegisteredClient registeredClient) { + + String issuer = getIssuer(); + Set authorizedScopes = new HashSet<>(); + authorizedScopes.add(DEFAULT_CLIENT_CONFIGURATION_AUTHORIZED_SCOPE); + JoseHeader headers = JwtUtils.headers().build(); + JwtClaimsSet claims = JwtUtils.accessTokenClaims( + registeredClient, issuer, registeredClient.getClientId(), authorizedScopes).build(); + + Jwt registrationAccessToken = this.jwtEncoder.encode(headers, claims); + + OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + registrationAccessToken.getTokenValue(), registrationAccessToken.getIssuedAt(), + registrationAccessToken.getExpiresAt(), authorizedScopes); + + // @formatter:off + OAuth2Authorization accessTokenAuthorization = OAuth2Authorization.withRegisteredClient(registeredClient) + .principalName(registeredClient.getClientId()) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .token(accessToken, + (metadata) -> + metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, registrationAccessToken.getClaims())) + .attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizedScopes) + .build(); + // @formatter:on + + this.authorizationService.save(accessTokenAuthorization); + return accessTokenAuthorization; + } + @SuppressWarnings("unchecked") - private static boolean isAuthorized(OAuth2Authorization.Token authorizedAccessToken) { + private static boolean isNotAuthorized(OAuth2Authorization.Token authorizedAccessToken, String requiredScope) { Object scope = authorizedAccessToken.getClaims().get(OAuth2ParameterNames.SCOPE); - return scope != null && ((Collection) scope).contains(DEFAULT_AUTHORIZED_SCOPE); + return scope == null || !((Collection) scope).contains(requiredScope); } private static boolean isValidRedirectUris(List redirectUris) { @@ -208,7 +298,15 @@ private static RegisteredClient create(OidcClientRegistration clientRegistration // @formatter:on } - private static OidcClientRegistration convert(RegisteredClient registeredClient) { + + private static String registrationClientUri(String issuer, String oidcClientRegistrationEndpoint, String clientId){ + return UriComponentsBuilder.fromUriString(issuer) + .path(oidcClientRegistrationEndpoint) + .queryParam(OAuth2ParameterNames.CLIENT_ID, clientId).toUriString(); + } + + private static OidcClientRegistration convert(RegisteredClient registeredClient, String registrationClientUri, + @Nullable String registrationAccessToken) { // @formatter:off OidcClientRegistration.Builder builder = OidcClientRegistration.builder() .clientId(registeredClient.getClientId()) @@ -234,8 +332,11 @@ private static OidcClientRegistration convert(RegisteredClient registeredClient) builder .tokenEndpointAuthenticationMethod(registeredClient.getClientAuthenticationMethods().iterator().next().getValue()) - .idTokenSignedResponseAlgorithm(registeredClient.getTokenSettings().getIdTokenSignatureAlgorithm().getName()); - + .idTokenSignedResponseAlgorithm(registeredClient.getTokenSettings().getIdTokenSignatureAlgorithm().getName()) + .registrationClientUri(registrationClientUri); + if (StringUtils.hasText(registrationAccessToken)) { + builder.registrationAccessToken(registrationAccessToken); + } return builder.build(); // @formatter:on } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationToken.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationToken.java index 892a47f60..19e4753f4 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationToken.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationToken.java @@ -27,6 +27,7 @@ * An {@link Authentication} implementation used for OpenID Connect Dynamic Client Registration 1.0. * * @author Joe Grandja + * @author Ovidiu Popa * @since 0.1.1 * @see AbstractAuthenticationToken * @see OidcClientRegistration @@ -35,8 +36,8 @@ public class OidcClientRegistrationAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = Version.SERIAL_VERSION_UID; private final Authentication principal; - private final OidcClientRegistration clientRegistration; - + private OidcClientRegistration clientRegistration; + private String clientId; /** * Constructs an {@code OidcClientRegistrationAuthenticationToken} using the provided parameters. * @@ -52,6 +53,21 @@ public OidcClientRegistrationAuthenticationToken(Authentication principal, OidcC setAuthenticated(principal.isAuthenticated()); } + /** + * Constructs an {@code OidcClientRegistrationAuthenticationToken} using the provided parameters. + * + * @param principal the authenticated principal + * @param clientId the registered client_id + */ + public OidcClientRegistrationAuthenticationToken(Authentication principal, String clientId) { + super(Collections.emptyList()); + Assert.notNull(principal, "principal cannot be null"); + Assert.hasText(clientId, "clientId cannot be null or empty"); + this.principal = principal; + this.clientId = clientId; + setAuthenticated(principal.isAuthenticated()); + } + @Override public Object getPrincipal() { return this.principal; @@ -71,4 +87,14 @@ public OidcClientRegistration getClientRegistration() { return this.clientRegistration; } + /** + * Returns the registered client_id. + * + * @return the registered client_id + * @since 0.2.1 + */ + public String getClientId() { + return this.clientId; + } + } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcClientRegistrationEndpointFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcClientRegistrationEndpointFilter.java index 38b601ea3..3eeb2217c 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcClientRegistrationEndpointFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcClientRegistrationEndpointFilter.java @@ -33,23 +33,28 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter; import org.springframework.security.oauth2.core.oidc.OidcClientRegistration; import org.springframework.security.oauth2.core.oidc.http.converter.OidcClientRegistrationHttpMessageConverter; import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationToken; +import org.springframework.security.web.util.matcher.AndRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; /** - * A {@code Filter} that processes OpenID Connect Dynamic Client Registration 1.0 Requests. + * A {@code Filter} that processes OpenID Connect Dynamic Client Registration 1.0 Requests and OpenID Connect Client Configuration 1.0 Requests. * * @author Ovidiu Popa * @author Joe Grandja * @since 0.1.1 * @see OidcClientRegistration * @see 3. Client Registration Endpoint + * @see 4. Client Configuration Endpoint */ public final class OidcClientRegistrationEndpointFilter extends OncePerRequestFilter { /** @@ -59,6 +64,8 @@ public final class OidcClientRegistrationEndpointFilter extends OncePerRequestFi private final AuthenticationManager authenticationManager; private final RequestMatcher clientRegistrationEndpointMatcher; + private final RequestMatcher registerClientEndpointMatcher; + private final RequestMatcher clientConfigurationEndpointMatcher; private final HttpMessageConverter clientRegistrationHttpMessageConverter = new OidcClientRegistrationHttpMessageConverter(); private final HttpMessageConverter errorHttpResponseConverter = @@ -84,8 +91,23 @@ public OidcClientRegistrationEndpointFilter(AuthenticationManager authentication Assert.notNull(authenticationManager, "authenticationManager cannot be null"); Assert.hasText(clientRegistrationEndpointUri, "clientRegistrationEndpointUri cannot be empty"); this.authenticationManager = authenticationManager; - this.clientRegistrationEndpointMatcher = new AntPathRequestMatcher( + this.registerClientEndpointMatcher = new AntPathRequestMatcher( clientRegistrationEndpointUri, HttpMethod.POST.name()); + this.clientConfigurationEndpointMatcher = createClientConfigurationEndpointMatcher(clientRegistrationEndpointUri); + this.clientRegistrationEndpointMatcher = new OrRequestMatcher(this.registerClientEndpointMatcher, this.clientConfigurationEndpointMatcher); + } + + private static RequestMatcher createClientConfigurationEndpointMatcher(String clientRegistrationEndpointUri) { + + RequestMatcher clientConfigurationRequestGetMatcher = new AntPathRequestMatcher( + clientRegistrationEndpointUri, HttpMethod.GET.name()); + + RequestMatcher clientIdMatcher = request -> { + String clientId = request.getParameter(OAuth2ParameterNames.CLIENT_ID); + return StringUtils.hasText(clientId); + }; + + return new AndRequestMatcher(clientConfigurationRequestGetMatcher, clientIdMatcher); } @Override @@ -98,17 +120,17 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } try { - Authentication principal = SecurityContextHolder.getContext().getAuthentication(); - OidcClientRegistration clientRegistration = this.clientRegistrationHttpMessageConverter.read( - OidcClientRegistration.class, new ServletServerHttpRequest(request)); - - OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication = - new OidcClientRegistrationAuthenticationToken(principal, clientRegistration); + OidcClientRegistrationAuthenticationToken clientRegistrationAuthenticationToken = convert(request); OidcClientRegistrationAuthenticationToken clientRegistrationAuthenticationResult = - (OidcClientRegistrationAuthenticationToken) this.authenticationManager.authenticate(clientRegistrationAuthentication); + (OidcClientRegistrationAuthenticationToken) this.authenticationManager.authenticate(clientRegistrationAuthenticationToken); + + if (clientRegistrationAuthenticationToken.getClientRegistration() != null) { + sendClientRegistrationResponse(response, HttpStatus.CREATED, clientRegistrationAuthenticationResult.getClientRegistration()); + return; + } - sendClientRegistrationResponse(response, clientRegistrationAuthenticationResult.getClientRegistration()); + sendClientRegistrationResponse(response, HttpStatus.OK, clientRegistrationAuthenticationResult.getClientRegistration()); } catch (OAuth2AuthenticationException ex) { sendErrorResponse(response, ex.getError()); @@ -123,18 +145,57 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } } - private void sendClientRegistrationResponse(HttpServletResponse response, OidcClientRegistration clientRegistration) throws IOException { + private OidcClientRegistrationAuthenticationToken convert(HttpServletRequest request) { + if (this.registerClientEndpointMatcher.matches(request)) { + return convertOidcClientRegistrationRequest(request); + } + + if (this.clientConfigurationEndpointMatcher.matches(request)) { + return convertOidcClientConfigurationRequest(request); + } + + throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST)); + } + + private OidcClientRegistrationAuthenticationToken convertOidcClientConfigurationRequest(HttpServletRequest request) { + Authentication principal = SecurityContextHolder.getContext().getAuthentication(); + String clientId = request.getParameter(OAuth2ParameterNames.CLIENT_ID); + String[] clientIdParameters = request.getParameterValues(OAuth2ParameterNames.CLIENT_ID); + if (!StringUtils.hasText(clientId) || clientIdParameters.length != 1) { + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT); + } + return new OidcClientRegistrationAuthenticationToken(principal, clientId); + } + + private OidcClientRegistrationAuthenticationToken convertOidcClientRegistrationRequest(HttpServletRequest request) { + try { + Authentication principal = SecurityContextHolder.getContext().getAuthentication(); + OidcClientRegistration clientRegistration = this.clientRegistrationHttpMessageConverter.read( + OidcClientRegistration.class, new ServletServerHttpRequest(request)); + return new OidcClientRegistrationAuthenticationToken(principal, clientRegistration); + } catch (IOException ex) { + OAuth2Error error = new OAuth2Error( + OAuth2ErrorCodes.INVALID_REQUEST, + "OpenID Client Registration Error: " + ex.getMessage(), + "https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationError"); + throw new OAuth2AuthenticationException(error); + } + } + + private void sendClientRegistrationResponse(HttpServletResponse response, HttpStatus httpStatus, OidcClientRegistration clientRegistration) throws IOException { ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response); - httpResponse.setStatusCode(HttpStatus.CREATED); + httpResponse.setStatusCode(httpStatus); this.clientRegistrationHttpMessageConverter.write(clientRegistration, null, httpResponse); } private void sendErrorResponse(HttpServletResponse response, OAuth2Error error) throws IOException { HttpStatus httpStatus = HttpStatus.BAD_REQUEST; - if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_TOKEN)) { + if (OAuth2ErrorCodes.INVALID_TOKEN.equals(error.getErrorCode())) { httpStatus = HttpStatus.UNAUTHORIZED; - } else if (error.getErrorCode().equals(OAuth2ErrorCodes.INSUFFICIENT_SCOPE)) { + } else if (OAuth2ErrorCodes.INSUFFICIENT_SCOPE.equals(error.getErrorCode())) { httpStatus = HttpStatus.FORBIDDEN; + } else if (OAuth2ErrorCodes.INVALID_CLIENT.equals(error.getErrorCode())) { + httpStatus = HttpStatus.UNAUTHORIZED; } ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response); httpResponse.setStatusCode(httpStatus); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcClientRegistrationTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcClientRegistrationTests.java index 865703d18..447cd5344 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcClientRegistrationTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcClientRegistrationTests.java @@ -18,16 +18,17 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.Base64; +import java.util.stream.Collectors; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.proc.SecurityContext; + import org.junit.After; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.http.HttpHeaders; @@ -67,6 +68,7 @@ import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; +import org.springframework.security.oauth2.server.authorization.config.ProviderSettings; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.test.web.servlet.MockMvc; @@ -74,6 +76,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.CoreMatchers.containsString; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -199,8 +202,277 @@ public void requestWhenClientRegistrationRequestAuthorizedThenClientRegistration .isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()); assertThat(clientRegistrationResponse.getIdTokenSignedResponseAlgorithm()) .isEqualTo(SignatureAlgorithm.RS256.getName()); + assertThat(clientRegistrationResponse.getRegistrationClientUri()) + .isNotNull(); + assertThat(clientRegistrationResponse.getRegistrationAccessToken()).isNotEmpty(); + } + + @Test + public void requestWhenClientConfigurationRequestAndRegisteredClientNotEqualToAuthorizationRegisteredClientThenUnauthorized() throws Exception { + this.spring.register(AuthorizationServerConfiguration.class).autowire(); + + // ***** (1) Obtain the registration access token used for fetching the registered client configuration + + String clientConfigurationRequestScope = "client.read"; + RegisteredClient registeredClient = TestRegisteredClients.registeredClient() + .scope(clientConfigurationRequestScope) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .build(); + this.registeredClientRepository.save(registeredClient); + + RegisteredClient unauthorizedRegisteredClient = TestRegisteredClients.registeredClient() + .id("registration-2") + .clientId("client-2") + .scope(clientConfigurationRequestScope) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .build(); + this.registeredClientRepository.save(unauthorizedRegisteredClient); + + MvcResult mvcResult = this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI) + .param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) + .param(OAuth2ParameterNames.SCOPE, clientConfigurationRequestScope) + .header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth( + registeredClient.getClientId(), registeredClient.getClientSecret()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.access_token").isNotEmpty()) + .andExpect(jsonPath("$.scope").value(clientConfigurationRequestScope)) + .andReturn(); + + OAuth2AccessToken accessToken = readAccessTokenResponse(mvcResult.getResponse()).getAccessToken(); + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setBearerAuth(accessToken.getTokenValue()); + + // ***** (2) Get RegisteredClient Configuration + this.mvc.perform(get(DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI) + .headers(httpHeaders) + .queryParam(OAuth2ParameterNames.CLIENT_ID, unauthorizedRegisteredClient.getClientId())) + .andExpect(status().isUnauthorized()); + } + + @Test + public void requestWhenClientConfigurationRequestAuthorizedThenClientRegistrationResponse() throws Exception { + this.spring.register(AuthorizationServerConfiguration.class).autowire(); + + // ***** (1) Obtain the registration access token used for fetching the registered client configuration + + String clientConfigurationRequestScope = "client.read"; + RegisteredClient registeredClient = TestRegisteredClients.registeredClient() + .scope(clientConfigurationRequestScope) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .build(); + this.registeredClientRepository.save(registeredClient); + + MvcResult mvcResult = this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI) + .param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) + .param(OAuth2ParameterNames.SCOPE, clientConfigurationRequestScope) + .header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth( + registeredClient.getClientId(), registeredClient.getClientSecret()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.access_token").isNotEmpty()) + .andExpect(jsonPath("$.scope").value(clientConfigurationRequestScope)) + .andReturn(); + + OAuth2AccessToken accessToken = readAccessTokenResponse(mvcResult.getResponse()).getAccessToken(); + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setBearerAuth(accessToken.getTokenValue()); + + // ***** (2) Get RegisteredClient Configuration + mvcResult = this.mvc.perform(get(DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI) + .headers(httpHeaders) + .queryParam(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())) + .andExpect(status().isOk()) + .andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store"))) + .andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache"))) + .andReturn(); + + OidcClientRegistration clientConfigurationResponse = readClientRegistrationResponse(mvcResult.getResponse()); + assertThat(clientConfigurationResponse.getClientId()).isNotNull().isEqualTo(registeredClient.getClientId()); + assertThat(clientConfigurationResponse.getClientIdIssuedAt()).isNotNull(); + assertThat(clientConfigurationResponse.getClientSecret()).isNotNull().isEqualTo(registeredClient.getClientSecret()); + assertThat(clientConfigurationResponse.getClientSecretExpiresAt()).isNull(); + assertThat(clientConfigurationResponse.getClientName()).isEqualTo(registeredClient.getClientName()); + assertThat(clientConfigurationResponse.getRedirectUris()) + .containsExactlyInAnyOrderElementsOf(registeredClient.getRedirectUris()); + assertThat(clientConfigurationResponse.getGrantTypes()) + .containsExactlyInAnyOrderElementsOf(registeredClient.getAuthorizationGrantTypes().stream().map(AuthorizationGrantType::getValue).collect(Collectors.toList())); + assertThat(clientConfigurationResponse.getResponseTypes()) + .containsExactly(OAuth2AuthorizationResponseType.CODE.getValue()); + assertThat(clientConfigurationResponse.getScopes()) + .containsExactlyInAnyOrderElementsOf(registeredClient.getScopes()); + assertThat(clientConfigurationResponse.getTokenEndpointAuthenticationMethod()) + .isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()); + assertThat(clientConfigurationResponse.getIdTokenSignedResponseAlgorithm()) + .isEqualTo(SignatureAlgorithm.RS256.getName()); + assertThat(clientConfigurationResponse.getRegistrationClientUri()) + .isNotNull(); + assertThat(clientConfigurationResponse.getRegistrationAccessToken()).isNull(); + } + + @Test + public void requestWhenClientConfigurationRequestTwiceSameAccessTokenAuthorizedThenClientRegistrationResponse() throws Exception { + this.spring.register(AuthorizationServerConfiguration.class).autowire(); + + // ***** (1) Obtain the registration access token used for fetching the registered client configuration + + String clientConfigurationRequestScope = "client.read"; + RegisteredClient registeredClient = TestRegisteredClients.registeredClient() + .scope(clientConfigurationRequestScope) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .build(); + this.registeredClientRepository.save(registeredClient); + + MvcResult mvcResult = this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI) + .param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) + .param(OAuth2ParameterNames.SCOPE, clientConfigurationRequestScope) + .header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth( + registeredClient.getClientId(), registeredClient.getClientSecret()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.access_token").isNotEmpty()) + .andExpect(jsonPath("$.scope").value(clientConfigurationRequestScope)) + .andReturn(); + + OAuth2AccessToken accessToken = readAccessTokenResponse(mvcResult.getResponse()).getAccessToken(); + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setBearerAuth(accessToken.getTokenValue()); + + // ***** (2) Get RegisteredClient Configuration + mvcResult = this.mvc.perform(get(DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI) + .headers(httpHeaders) + .queryParam(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())) + .andExpect(status().isOk()) + .andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store"))) + .andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache"))) + .andReturn(); + + assertClientConfigurationResponse(registeredClient, mvcResult); + + // ***** (3) Get RegisteredClient Configuration with the same access token + mvcResult = this.mvc.perform(get(DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI) + .headers(httpHeaders) + .queryParam(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())) + .andExpect(status().isOk()) + .andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store"))) + .andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache"))) + .andReturn(); + assertClientConfigurationResponse(registeredClient, mvcResult); + } + + @Test + public void requestWhenClientRegistrationRequestAndClientConfigurationRequestAuthorizedThenClientRegistrationResponse() throws Exception { + this.spring.register(AuthorizationServerConfiguration.class).autowire(); + + // ***** (1) Obtain the "initial" access token used for registering the client + + String clientRegistrationScope = "client.create"; + RegisteredClient registeredClient = TestRegisteredClients.registeredClient2() + .scope(clientRegistrationScope) + .build(); + this.registeredClientRepository.save(registeredClient); + + MvcResult mvcResult = this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI) + .param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) + .param(OAuth2ParameterNames.SCOPE, clientRegistrationScope) + .header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth( + registeredClient.getClientId(), registeredClient.getClientSecret()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.access_token").isNotEmpty()) + .andExpect(jsonPath("$.scope").value(clientRegistrationScope)) + .andReturn(); + + OAuth2AccessToken accessToken = readAccessTokenResponse(mvcResult.getResponse()).getAccessToken(); + + // ***** (2) Register the client + + // @formatter:off + OidcClientRegistration clientRegistration = OidcClientRegistration.builder() + .clientName("client-name") + .redirectUri("https://client.example.com") + .grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()) + .grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) + .scope("scope1") + .scope("scope2") + .build(); + // @formatter:on + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setBearerAuth(accessToken.getTokenValue()); + + // Register the client + mvcResult = this.mvc.perform(post(DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI) + .headers(httpHeaders) + .contentType(MediaType.APPLICATION_JSON) + .content(getClientRegistrationRequestContent(clientRegistration))) + .andExpect(status().isCreated()) + .andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store"))) + .andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache"))) + .andReturn(); + + OidcClientRegistration clientRegistrationResponse = readClientRegistrationResponse(mvcResult.getResponse()); + + + httpHeaders = new HttpHeaders(); + httpHeaders.setBearerAuth(clientRegistrationResponse.getRegistrationAccessToken()); + + // ***** (3) Get RegisteredClient Configuration + mvcResult = this.mvc.perform(get(clientRegistrationResponse.getRegistrationClientUri().toString()) + .headers(httpHeaders)) + .andExpect(status().isOk()) + .andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store"))) + .andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache"))) + .andReturn(); + + OidcClientRegistration clientConfigurationResponse = readClientRegistrationResponse(mvcResult.getResponse()); + assertThat(clientConfigurationResponse.getClientId()).isNotNull().isEqualTo(clientRegistrationResponse.getClientId()); + assertThat(clientConfigurationResponse.getClientIdIssuedAt()).isNotNull(); + assertThat(clientConfigurationResponse.getClientSecret()).isNotNull().isEqualTo(clientRegistrationResponse.getClientSecret()); + assertThat(clientConfigurationResponse.getClientSecretExpiresAt()).isNull(); + assertThat(clientConfigurationResponse.getClientName()).isEqualTo(clientRegistrationResponse.getClientName()); + assertThat(clientConfigurationResponse.getRedirectUris()) + .containsExactlyInAnyOrderElementsOf(clientRegistrationResponse.getRedirectUris()); + assertThat(clientConfigurationResponse.getGrantTypes()) + .containsExactlyInAnyOrderElementsOf(clientRegistrationResponse.getGrantTypes()); + assertThat(clientConfigurationResponse.getResponseTypes()) + .containsExactly(OAuth2AuthorizationResponseType.CODE.getValue()); + assertThat(clientConfigurationResponse.getScopes()) + .containsExactlyInAnyOrderElementsOf(clientRegistrationResponse.getScopes()); + assertThat(clientConfigurationResponse.getTokenEndpointAuthenticationMethod()) + .isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()); + assertThat(clientConfigurationResponse.getIdTokenSignedResponseAlgorithm()) + .isEqualTo(SignatureAlgorithm.RS256.getName()); + assertThat(clientConfigurationResponse.getRegistrationClientUri()) + .isNotNull(); + assertThat(clientConfigurationResponse.getRegistrationAccessToken()).isNull(); } + private static void assertClientConfigurationResponse(RegisteredClient registeredClient, MvcResult mvcResult) throws Exception { + OidcClientRegistration clientConfigurationResponse; + clientConfigurationResponse = readClientRegistrationResponse(mvcResult.getResponse()); + assertThat(clientConfigurationResponse.getClientId()).isNotNull().isEqualTo(registeredClient.getClientId()); + assertThat(clientConfigurationResponse.getClientIdIssuedAt()).isNotNull(); + assertThat(clientConfigurationResponse.getClientSecret()).isNotNull().isEqualTo(registeredClient.getClientSecret()); + assertThat(clientConfigurationResponse.getClientSecretExpiresAt()).isNull(); + assertThat(clientConfigurationResponse.getClientName()).isEqualTo(registeredClient.getClientName()); + assertThat(clientConfigurationResponse.getRedirectUris()) + .containsExactlyInAnyOrderElementsOf(registeredClient.getRedirectUris()); + assertThat(clientConfigurationResponse.getGrantTypes()) + .containsExactlyInAnyOrderElementsOf(registeredClient.getAuthorizationGrantTypes().stream().map(AuthorizationGrantType::getValue).collect(Collectors.toList())); + assertThat(clientConfigurationResponse.getResponseTypes()) + .containsExactly(OAuth2AuthorizationResponseType.CODE.getValue()); + assertThat(clientConfigurationResponse.getScopes()) + .containsExactlyInAnyOrderElementsOf(registeredClient.getScopes()); + assertThat(clientConfigurationResponse.getTokenEndpointAuthenticationMethod()) + .isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()); + assertThat(clientConfigurationResponse.getIdTokenSignedResponseAlgorithm()) + .isEqualTo(SignatureAlgorithm.RS256.getName()); + assertThat(clientConfigurationResponse.getRegistrationClientUri()) + .isNotNull(); + assertThat(clientConfigurationResponse.getRegistrationAccessToken()).isNull(); + } + + private static String encodeBasicAuth(String clientId, String secret) throws Exception { clientId = URLEncoder.encode(clientId, StandardCharsets.UTF_8.name()); secret = URLEncoder.encode(secret, StandardCharsets.UTF_8.name()); @@ -278,6 +550,12 @@ JwtDecoder jwtDecoder(JWKSource jwkSource) { return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); } + @Bean + ProviderSettings providerSettings() { + return ProviderSettings.builder().issuer("http://auth-server:9000") + .oidcClientRegistrationEndpoint("/connect/register").build(); + } + @Bean PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/OidcClientRegistrationTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/OidcClientRegistrationTests.java index 3b9bc938b..9f9562292 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/OidcClientRegistrationTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/OidcClientRegistrationTests.java @@ -64,6 +64,8 @@ public void buildWhenAllClaimsProvidedThenCreated() { .scope("scope2") .idTokenSignedResponseAlgorithm(SignatureAlgorithm.RS256.getName()) .claim("a-claim", "a-value") + .registrationAccessToken("registration-access-token") + .registrationClientUri("https://auth-server.com/connect/register?client_id=1") .build(); // @formatter:on @@ -79,6 +81,8 @@ public void buildWhenAllClaimsProvidedThenCreated() { assertThat(clientRegistration.getScopes()).containsExactlyInAnyOrder("scope1", "scope2"); assertThat(clientRegistration.getIdTokenSignedResponseAlgorithm()).isEqualTo("RS256"); assertThat(clientRegistration.getClaimAsString("a-claim")).isEqualTo("a-value"); + assertThat(clientRegistration.getRegistrationAccessToken()).isEqualTo("registration-access-token"); + assertThat(clientRegistration.getRegistrationClientUri().toString()).isEqualTo("https://auth-server.com/connect/register?client_id=1"); } @Test @@ -105,6 +109,8 @@ public void withClaimsWhenClaimsProvidedThenCreated() { claims.put(OidcClientMetadataClaimNames.SCOPE, Arrays.asList("scope1", "scope2")); claims.put(OidcClientMetadataClaimNames.ID_TOKEN_SIGNED_RESPONSE_ALG, SignatureAlgorithm.RS256.getName()); claims.put("a-claim", "a-value"); + claims.put(OidcClientMetadataClaimNames.REGISTRATION_ACCESS_TOKEN, "registration-access-token"); + claims.put(OidcClientMetadataClaimNames.REGISTRATION_CLIENT_URI, "https://auth-server.com/connect/register?client_id=1"); OidcClientRegistration clientRegistration = OidcClientRegistration.withClaims(claims).build(); @@ -120,6 +126,8 @@ public void withClaimsWhenClaimsProvidedThenCreated() { assertThat(clientRegistration.getScopes()).containsExactlyInAnyOrder("scope1", "scope2"); assertThat(clientRegistration.getIdTokenSignedResponseAlgorithm()).isEqualTo("RS256"); assertThat(clientRegistration.getClaimAsString("a-claim")).isEqualTo("a-value"); + assertThat(clientRegistration.getRegistrationAccessToken()).isEqualTo("registration-access-token"); + assertThat(clientRegistration.getRegistrationClientUri().toString()).isEqualTo("https://auth-server.com/connect/register?client_id=1"); } @Test diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcClientRegistrationHttpMessageConverterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcClientRegistrationHttpMessageConverterTests.java index 312e4c2d0..dc547e421 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcClientRegistrationHttpMessageConverterTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcClientRegistrationHttpMessageConverterTests.java @@ -185,6 +185,8 @@ public void writeInternalWhenClientRegistrationThenSuccess() { .scope("scope2") .idTokenSignedResponseAlgorithm(SignatureAlgorithm.RS256.getName()) .claim("a-claim", "a-value") + .registrationClientUri("https://auth-server.com/connect/register?client_id=1") + .registrationAccessToken("registration-access-token") .build(); // @formatter:on @@ -204,6 +206,8 @@ public void writeInternalWhenClientRegistrationThenSuccess() { assertThat(clientRegistrationResponse).contains("\"scope\":\"scope1 scope2\""); assertThat(clientRegistrationResponse).contains("\"id_token_signed_response_alg\":\"RS256\""); assertThat(clientRegistrationResponse).contains("\"a-claim\":\"a-value\""); + assertThat(clientRegistrationResponse).contains("\"registration_access_token\":\"registration-access-token\""); + assertThat(clientRegistrationResponse).contains("\"registration_client_uri\":\"https://auth-server.com/connect/register?client_id=1\""); } @Test diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProviderTests.java index 2b341910d..7c40d8eac 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProviderTests.java @@ -23,7 +23,6 @@ import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; - import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.oauth2.core.AuthorizationGrantType; @@ -39,6 +38,7 @@ import org.springframework.security.oauth2.jwt.JoseHeader; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtClaimsSet; +import org.springframework.security.oauth2.jwt.JwtEncoder; import org.springframework.security.oauth2.jwt.TestJoseHeaders; import org.springframework.security.oauth2.jwt.TestJwtClaimsSets; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; @@ -47,13 +47,17 @@ import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; +import org.springframework.security.oauth2.server.authorization.config.ProviderSettings; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.web.util.UriComponentsBuilder; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -67,43 +71,56 @@ public class OidcClientRegistrationAuthenticationProviderTests { private RegisteredClientRepository registeredClientRepository; private OAuth2AuthorizationService authorizationService; private OidcClientRegistrationAuthenticationProvider authenticationProvider; + private JwtEncoder jwtEncoder; + private ProviderSettings providerSettings; @Before public void setUp() { + this.registeredClientRepository = mock(RegisteredClientRepository.class); this.authorizationService = mock(OAuth2AuthorizationService.class); + this.jwtEncoder = mock(JwtEncoder.class); + this.providerSettings = ProviderSettings.builder().issuer("http://auth-server:9000").build(); this.authenticationProvider = new OidcClientRegistrationAuthenticationProvider( - this.registeredClientRepository, this.authorizationService); + this.registeredClientRepository, this.authorizationService, + this.jwtEncoder); + this.authenticationProvider.setProviderSettings(providerSettings); } @Test public void constructorWhenRegisteredClientRepositoryNullThenThrowIllegalArgumentException() { assertThatIllegalArgumentException() - .isThrownBy(() -> new OidcClientRegistrationAuthenticationProvider(null, this.authorizationService)) + .isThrownBy(() -> new OidcClientRegistrationAuthenticationProvider(null, this.authorizationService, jwtEncoder)) .withMessage("registeredClientRepository cannot be null"); } @Test public void constructorWhenAuthorizationServiceNullThenThrowIllegalArgumentException() { assertThatIllegalArgumentException() - .isThrownBy(() -> new OidcClientRegistrationAuthenticationProvider(this.registeredClientRepository, null)) + .isThrownBy(() -> new OidcClientRegistrationAuthenticationProvider(this.registeredClientRepository, null, jwtEncoder)) .withMessage("authorizationService cannot be null"); } + @Test + public void constructorWhenJwtEncoderNullThenThrowIllegalArgumentException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new OidcClientRegistrationAuthenticationProvider(this.registeredClientRepository, this.authorizationService, null)) + .withMessage("jwtEncoder cannot be null"); + } + @Test public void supportsWhenTypeOidcClientRegistrationAuthenticationTokenThenReturnTrue() { assertThat(this.authenticationProvider.supports(OidcClientRegistrationAuthenticationToken.class)).isTrue(); } @Test - public void authenticateWhenPrincipalNotOAuth2TokenAuthenticationTokenThenThrowOAuth2AuthenticationException() { + public void authenticateWhenClientRegistrationRequestAndPrincipalNotOAuth2TokenAuthenticationTokenThenThrowOAuth2AuthenticationException() { TestingAuthenticationToken principal = new TestingAuthenticationToken("principal", "credentials"); OidcClientRegistration clientRegistration = OidcClientRegistration.builder() .redirectUri("https://client.example.com") .build(); - OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken( - principal, clientRegistration); + OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(principal, clientRegistration); assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) .isInstanceOf(OAuth2AuthenticationException.class) @@ -112,14 +129,13 @@ public void authenticateWhenPrincipalNotOAuth2TokenAuthenticationTokenThenThrowO } @Test - public void authenticateWhenPrincipalNotAuthenticatedThenThrowOAuth2AuthenticationException() { - JwtAuthenticationToken principal = new JwtAuthenticationToken(createJwt()); + public void authenticateWhenClientRegistrationRequestAndPrincipalNotAuthenticatedThenThrowOAuth2AuthenticationException() { + JwtAuthenticationToken principal = new JwtAuthenticationToken(createJwtClientRegistration()); OidcClientRegistration clientRegistration = OidcClientRegistration.builder() .redirectUri("https://client.example.com") .build(); - OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken( - principal, clientRegistration); + OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(principal, clientRegistration); assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) .isInstanceOf(OAuth2AuthenticationException.class) @@ -128,16 +144,15 @@ public void authenticateWhenPrincipalNotAuthenticatedThenThrowOAuth2Authenticati } @Test - public void authenticateWhenAccessTokenNotFoundThenThrowOAuth2AuthenticationException() { - Jwt jwt = createJwt(); + public void authenticateWhenClientRegistrationRequestAndAccessTokenNotFoundThenThrowOAuth2AuthenticationException() { + Jwt jwt = createJwtClientRegistration(); JwtAuthenticationToken principal = new JwtAuthenticationToken( jwt, AuthorityUtils.createAuthorityList("SCOPE_client.create")); OidcClientRegistration clientRegistration = OidcClientRegistration.builder() .redirectUri("https://client.example.com") .build(); - OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken( - principal, clientRegistration); + OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(principal, clientRegistration); assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) .isInstanceOf(OAuth2AuthenticationException.class) @@ -148,8 +163,8 @@ public void authenticateWhenAccessTokenNotFoundThenThrowOAuth2AuthenticationExce } @Test - public void authenticateWhenAccessTokenNotActiveThenThrowOAuth2AuthenticationException() { - Jwt jwt = createJwt(); + public void authenticateWhenClientRegistrationRequestAndAccessTokenNotActiveThenThrowOAuth2AuthenticationException() { + Jwt jwt = createJwtClientRegistration(); OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE)); @@ -167,8 +182,7 @@ public void authenticateWhenAccessTokenNotActiveThenThrowOAuth2AuthenticationExc .redirectUri("https://client.example.com") .build(); - OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken( - principal, clientRegistration); + OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(principal, clientRegistration); assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) .isInstanceOf(OAuth2AuthenticationException.class) @@ -179,7 +193,7 @@ public void authenticateWhenAccessTokenNotActiveThenThrowOAuth2AuthenticationExc } @Test - public void authenticateWhenAccessTokenNotAuthorizedThenThrowOAuth2AuthenticationException() { + public void authenticateWhenClientRegistrationRequestAndAccessTokenNotAuthorizedThenThrowOAuth2AuthenticationException() { Jwt jwt = createJwt(Collections.singleton("unauthorized.scope")); OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, jwt.getTokenValue(), jwt.getIssuedAt(), @@ -197,8 +211,7 @@ public void authenticateWhenAccessTokenNotAuthorizedThenThrowOAuth2Authenticatio .redirectUri("https://client.example.com") .build(); - OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken( - principal, clientRegistration); + OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(principal, clientRegistration); assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) .isInstanceOf(OAuth2AuthenticationException.class) @@ -209,8 +222,8 @@ public void authenticateWhenAccessTokenNotAuthorizedThenThrowOAuth2Authenticatio } @Test - public void authenticateWhenInvalidRedirectUriThenThrowOAuth2AuthenticationException() { - Jwt jwt = createJwt(); + public void authenticateWhenClientRegistrationRequestAndInvalidRedirectUriThenThrowOAuth2AuthenticationException() { + Jwt jwt = createJwtClientRegistration(); OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE)); @@ -229,8 +242,7 @@ public void authenticateWhenInvalidRedirectUriThenThrowOAuth2AuthenticationExcep .build(); // @formatter:on - OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken( - principal, clientRegistration); + OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(principal, clientRegistration); assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) .isInstanceOf(OAuth2AuthenticationException.class) @@ -241,8 +253,8 @@ public void authenticateWhenInvalidRedirectUriThenThrowOAuth2AuthenticationExcep } @Test - public void authenticateWhenRedirectUriContainsFragmentThenThrowOAuth2AuthenticationException() { - Jwt jwt = createJwt(); + public void authenticateWhenClientRegistrationRequestAndRedirectUriContainsFragmentThenThrowOAuth2AuthenticationException() { + Jwt jwt = createJwtClientRegistration(); OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE)); @@ -261,8 +273,7 @@ public void authenticateWhenRedirectUriContainsFragmentThenThrowOAuth2Authentica .build(); // @formatter:on - OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken( - principal, clientRegistration); + OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(principal, clientRegistration); assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) .isInstanceOf(OAuth2AuthenticationException.class) @@ -273,8 +284,8 @@ public void authenticateWhenRedirectUriContainsFragmentThenThrowOAuth2Authentica } @Test - public void authenticateWhenValidAccessTokenThenReturnClientRegistration() { - Jwt jwt = createJwt(); + public void authenticateWhenClientRegistrationRequestAndValidAccessTokenThenReturnClientRegistration() { + Jwt jwt = createJwtClientRegistration(); OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE)); @@ -284,6 +295,7 @@ public void authenticateWhenValidAccessTokenThenReturnClientRegistration() { when(this.authorizationService.findByToken( eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN))) .thenReturn(authorization); + when(this.jwtEncoder.encode(any(), any())).thenReturn(createJwt(Collections.singleton("client.read"))); JwtAuthenticationToken principal = new JwtAuthenticationToken( jwt, AuthorityUtils.createAuthorityList("SCOPE_client.create")); @@ -298,8 +310,7 @@ public void authenticateWhenValidAccessTokenThenReturnClientRegistration() { .build(); // @formatter:on - OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken( - principal, clientRegistration); + OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(principal, clientRegistration); OidcClientRegistrationAuthenticationToken authenticationResult = (OidcClientRegistrationAuthenticationToken) this.authenticationProvider.authenticate(authentication); @@ -309,14 +320,23 @@ public void authenticateWhenValidAccessTokenThenReturnClientRegistration() { verify(this.authorizationService).findByToken( eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN)); verify(this.registeredClientRepository).save(registeredClientCaptor.capture()); - verify(this.authorizationService).save(authorizationCaptor.capture()); + verify(this.authorizationService, times(2)).save(authorizationCaptor.capture()); + verify(this.jwtEncoder).encode(any(), any()); - OAuth2Authorization authorizationResult = authorizationCaptor.getValue(); + // assert access token + OAuth2Authorization authorizationResult = authorizationCaptor.getAllValues().get(0); assertThat(authorizationResult.getAccessToken().isInvalidated()).isTrue(); if (authorizationResult.getRefreshToken() != null) { assertThat(authorizationResult.getRefreshToken().isInvalidated()).isTrue(); } + // assert registration access token which should be used for subsequent calls to client configuration endpoint + authorizationResult = authorizationCaptor.getAllValues().get(1); + assertThat(authorizationResult.getAccessToken().isInvalidated()).isFalse(); + assertThat(authorizationResult.getRefreshToken()).isNull(); + assertThat(authorizationResult.getAccessToken().getToken().getScopes()) + .containsExactly("client.read"); + RegisteredClient registeredClientResult = registeredClientCaptor.getValue(); assertThat(registeredClientResult.getId()).isNotNull(); assertThat(registeredClientResult.getClientId()).isNotNull(); @@ -354,12 +374,249 @@ public void authenticateWhenValidAccessTokenThenReturnClientRegistration() { .isEqualTo(registeredClientResult.getClientAuthenticationMethods().iterator().next().getValue()); assertThat(clientRegistrationResult.getIdTokenSignedResponseAlgorithm()) .isEqualTo(registeredClientResult.getTokenSettings().getIdTokenSignatureAlgorithm().getName()); + + String expectedRegistrationClientUri = UriComponentsBuilder.fromUriString(this.providerSettings.getIssuer()) + .path(this.providerSettings.getOidcClientRegistrationEndpoint()) + .queryParam("client_id", registeredClientResult.getClientId()).toUriString(); + + assertThat(clientRegistrationResult.getRegistrationClientUri().toString()).isEqualTo(expectedRegistrationClientUri); + assertThat(clientRegistrationResult.getRegistrationAccessToken()).isNotEmpty().isEqualTo(jwt.getTokenValue()); + } + + @Test + public void authenticateWhenClientConfigurationRequestAndPrincipalNotOAuth2TokenAuthenticationTokenThenThrowOAuth2AuthenticationException() { + TestingAuthenticationToken principal = new TestingAuthenticationToken("principal", "credentials"); + + OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(principal, "client-1"); + + assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) + .isInstanceOf(OAuth2AuthenticationException.class) + .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode") + .isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN); + } + + @Test + public void authenticateWhenClientConfigurationRequestAndPrincipalNotAuthenticatedThenThrowOAuth2AuthenticationException() { + JwtAuthenticationToken principal = new JwtAuthenticationToken(createJwtClientConfiguration()); + + OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(principal, "client-1"); + + assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) + .isInstanceOf(OAuth2AuthenticationException.class) + .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode") + .isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN); + } + + @Test + public void authenticateWhenClientConfigurationRequestAndAccessTokenNotFoundThenThrowOAuth2AuthenticationException() { + Jwt jwt = createJwtClientConfiguration(); + JwtAuthenticationToken principal = new JwtAuthenticationToken( + jwt, AuthorityUtils.createAuthorityList("SCOPE_client.read")); + + OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(principal, "client-1"); + + assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) + .isInstanceOf(OAuth2AuthenticationException.class) + .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode") + .isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN); + verify(this.authorizationService).findByToken( + eq(jwt.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN)); + } + + @Test + public void authenticateWhenClientConfigurationRequestAndAccessTokenNotActiveThenThrowOAuth2AuthenticationException() { + Jwt jwt = createJwtClientConfiguration(); + OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + jwt.getTokenValue(), jwt.getIssuedAt(), + jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE)); + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + OAuth2Authorization authorization = TestOAuth2Authorizations.authorization( + registeredClient, jwtAccessToken, jwt.getClaims()).build(); + authorization = OidcAuthenticationProviderUtils.invalidate(authorization, jwtAccessToken); + when(this.authorizationService.findByToken( + eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN))) + .thenReturn(authorization); + + JwtAuthenticationToken principal = new JwtAuthenticationToken( + jwt, AuthorityUtils.createAuthorityList("SCOPE_client.read")); + + OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(principal, "client-1"); + + assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) + .isInstanceOf(OAuth2AuthenticationException.class) + .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode") + .isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN); + verify(this.authorizationService).findByToken( + eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN)); + } + + @Test + public void authenticateWhenClientConfigurationRequestAndAccessTokenNotAuthorizedThenThrowOAuth2AuthenticationException() { + Jwt jwt = createJwt(Collections.singleton("unauthorized.scope")); + OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + jwt.getTokenValue(), jwt.getIssuedAt(), + jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE)); + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + OAuth2Authorization authorization = TestOAuth2Authorizations.authorization( + registeredClient, jwtAccessToken, jwt.getClaims()).build(); + when(this.authorizationService.findByToken( + eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN))) + .thenReturn(authorization); + + JwtAuthenticationToken principal = new JwtAuthenticationToken( + jwt, AuthorityUtils.createAuthorityList("SCOPE_unauthorized.scope")); + + OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(principal, registeredClient.getClientId()); + + assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) + .isInstanceOf(OAuth2AuthenticationException.class) + .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode") + .isEqualTo(OAuth2ErrorCodes.INSUFFICIENT_SCOPE); + verify(this.authorizationService).findByToken( + eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN)); } - private static Jwt createJwt() { + @Test + public void authenticateWhenClientConfigurationRequestAndRegisteredClientNotFoundThenThrowOAuth2AuthenticationException() { + Jwt jwt = createJwtClientConfiguration(); + OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + jwt.getTokenValue(), jwt.getIssuedAt(), + jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE)); + RegisteredClient registeredClient = TestRegisteredClients.registeredClient() + .build(); + OAuth2Authorization authorization = TestOAuth2Authorizations.authorization( + registeredClient, jwtAccessToken, jwt.getClaims()).build(); + when(this.authorizationService.findByToken( + eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN))) + .thenReturn(authorization); + + when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) + .thenReturn(null); + + JwtAuthenticationToken principal = new JwtAuthenticationToken( + jwt, AuthorityUtils.createAuthorityList("SCOPE_client.read")); + + OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(principal, registeredClient.getClientId()); + + assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) + .isInstanceOf(OAuth2AuthenticationException.class) + .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode") + .isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT); + verify(this.authorizationService).findByToken( + eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN)); + verify(this.registeredClientRepository).findByClientId( + eq(registeredClient.getClientId())); + } + + @Test + public void authenticateWhenClientConfigurationRequestRegisteredClientNotEqualToAuthorizationRegisteredClientThenThrowOAuth2AuthenticationException() { + Jwt jwt = createJwtClientConfiguration(); + OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + jwt.getTokenValue(), jwt.getIssuedAt(), + jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE)); + + RegisteredClient registeredClient = TestRegisteredClients.registeredClient() + .id("registration-1").clientId("client-1").build(); + RegisteredClient authorizationRegisteredClient = TestRegisteredClients.registeredClient() + .id("registration-2").clientId("client-2").build(); + + OAuth2Authorization authorization = TestOAuth2Authorizations.authorization( + authorizationRegisteredClient, jwtAccessToken, jwt.getClaims()).build(); + when(this.authorizationService.findByToken( + eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN))) + .thenReturn(authorization); + + when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) + .thenReturn(registeredClient); + + JwtAuthenticationToken principal = new JwtAuthenticationToken( + jwt, AuthorityUtils.createAuthorityList("SCOPE_client.read")); + + OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(principal, registeredClient.getClientId()); + + assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) + .isInstanceOf(OAuth2AuthenticationException.class) + .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode") + .isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT); + verify(this.authorizationService).findByToken( + eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN)); + verify(this.registeredClientRepository).findByClientId( + eq(registeredClient.getClientId())); + } + + @Test + public void authenticateWhenClientConfigurationRequestAndValidAccessTokenThenReturnClientRegistration() { + Jwt jwt = createJwtClientConfiguration(); + OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + jwt.getTokenValue(), jwt.getIssuedAt(), + jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE)); + RegisteredClient registeredClient = TestRegisteredClients.registeredClient() + .clientName("client-name") + .build(); + OAuth2Authorization authorization = TestOAuth2Authorizations.authorization( + registeredClient, jwtAccessToken, jwt.getClaims()).build(); + when(this.authorizationService.findByToken( + eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN))) + .thenReturn(authorization); + + JwtAuthenticationToken principal = new JwtAuthenticationToken( + jwt, AuthorityUtils.createAuthorityList("SCOPE_client.read")); + + when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) + .thenReturn(registeredClient); + + OidcClientRegistrationAuthenticationToken authentication = + new OidcClientRegistrationAuthenticationToken(principal, registeredClient.getClientId()); + + OidcClientRegistrationAuthenticationToken authenticationResult = + (OidcClientRegistrationAuthenticationToken) this.authenticationProvider.authenticate(authentication); + + verify(this.authorizationService).findByToken( + eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN)); + verify(this.registeredClientRepository).findByClientId( + eq(registeredClient.getClientId())); + + // verify that the registration access token is not invalidated after its used + verify(this.authorizationService, times(0)).save(eq(authorization)); + assertThat(authorization.getAccessToken().isInvalidated()).isFalse(); + + OidcClientRegistration clientRegistrationResult = authenticationResult.getClientRegistration(); + assertThat(clientRegistrationResult.getClientId()).isEqualTo(registeredClient.getClientId()); + assertThat(clientRegistrationResult.getClientIdIssuedAt()).isEqualTo(registeredClient.getClientIdIssuedAt()); + assertThat(clientRegistrationResult.getClientSecret()).isEqualTo(registeredClient.getClientSecret()); + assertThat(clientRegistrationResult.getClientSecretExpiresAt()).isEqualTo(registeredClient.getClientSecretExpiresAt()); + assertThat(clientRegistrationResult.getClientName()).isEqualTo(registeredClient.getClientName()); + assertThat(clientRegistrationResult.getRedirectUris()) + .containsExactlyInAnyOrderElementsOf(registeredClient.getRedirectUris()); + + List grantTypes = new ArrayList<>(); + registeredClient.getAuthorizationGrantTypes().forEach(authorizationGrantType -> + grantTypes.add(authorizationGrantType.getValue())); + assertThat(clientRegistrationResult.getGrantTypes()).containsExactlyInAnyOrderElementsOf(grantTypes); + + assertThat(clientRegistrationResult.getResponseTypes()) + .containsExactly(OAuth2AuthorizationResponseType.CODE.getValue()); + assertThat(clientRegistrationResult.getScopes()) + .containsExactlyInAnyOrderElementsOf(registeredClient.getScopes()); + assertThat(clientRegistrationResult.getTokenEndpointAuthenticationMethod()) + .isEqualTo(registeredClient.getClientAuthenticationMethods().iterator().next().getValue()); + assertThat(clientRegistrationResult.getIdTokenSignedResponseAlgorithm()) + .isEqualTo(registeredClient.getTokenSettings().getIdTokenSignatureAlgorithm().getName()); + String expectedRegistrationClientUri = UriComponentsBuilder.fromUriString(this.providerSettings.getIssuer()) + .path(this.providerSettings.getOidcClientRegistrationEndpoint()) + .queryParam("client_id", registeredClient.getClientId()).toUriString(); + assertThat(clientRegistrationResult.getRegistrationClientUri().toString()).isEqualTo(expectedRegistrationClientUri); + assertThat(clientRegistrationResult.getRegistrationAccessToken()).isNull(); + } + + private static Jwt createJwtClientRegistration() { return createJwt(Collections.singleton("client.create")); } + private static Jwt createJwtClientConfiguration() { + return createJwt(Collections.singleton("client.read")); + } + private static Jwt createJwt(Set scopes) { // @formatter:off JoseHeader joseHeader = TestJoseHeaders.joseHeader() diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationTokenTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationTokenTests.java index 9c647a963..948baaa2f 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationTokenTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationTokenTests.java @@ -43,18 +43,45 @@ public void constructorWhenPrincipalNullThenThrowIllegalArgumentException() { @Test public void constructorWhenClientRegistrationNullThenThrowIllegalArgumentException() { assertThatIllegalArgumentException() - .isThrownBy(() -> new OidcClientRegistrationAuthenticationToken(this.principal, null)) + .isThrownBy(() -> new OidcClientRegistrationAuthenticationToken(this.principal, (OidcClientRegistration) null)) .withMessage("clientRegistration cannot be null"); } @Test - public void constructorWhenAllValuesProvidedThenCreated() { + public void constructorWhenClientIdNullThenThrowIllegalArgumentException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new OidcClientRegistrationAuthenticationToken(this.principal, (String) null)) + .withMessage("clientId cannot be null or empty"); + } + + @Test + public void constructorWhenClientIdEmptyThenThrowIllegalArgumentException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new OidcClientRegistrationAuthenticationToken(this.principal, "")) + .withMessage("clientId cannot be null or empty"); + } + + @Test + public void constructorWhenOidcClientRegistrationProvidedThenCreated() { OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken( this.principal, this.clientRegistration); assertThat(authentication.getPrincipal()).isEqualTo(this.principal); assertThat(authentication.getCredentials().toString()).isEmpty(); assertThat(authentication.getClientRegistration()).isEqualTo(this.clientRegistration); + assertThat(authentication.getClientId()).isNull(); + assertThat(authentication.isAuthenticated()).isEqualTo(this.principal.isAuthenticated()); + } + + @Test + public void constructorWhenClientIdProvidedThenCreated() { + OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken( + this.principal, "client-1"); + + assertThat(authentication.getPrincipal()).isEqualTo(this.principal); + assertThat(authentication.getCredentials().toString()).isEmpty(); + assertThat(authentication.getClientRegistration()).isNull(); + assertThat(authentication.getClientId()).isEqualTo("client-1"); assertThat(authentication.isAuthenticated()).isEqualTo(this.principal.isAuthenticated()); } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcClientRegistrationEndpointFilterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcClientRegistrationEndpointFilterTests.java index ac58b919f..3e2ed0148 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcClientRegistrationEndpointFilterTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcClientRegistrationEndpointFilterTests.java @@ -25,7 +25,6 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; - import org.springframework.http.HttpStatus; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.mock.http.client.MockClientHttpRequest; @@ -162,7 +161,7 @@ public void doFilterWhenClientRegistrationRequestInsufficientTokenScopeThenForbi private void doFilterWhenClientRegistrationRequestInvalidThenError( String errorCode, HttpStatus status) throws Exception { - Jwt jwt = createJwt(); + Jwt jwt = createJwt("client.create"); JwtAuthenticationToken principal = new JwtAuthenticationToken( jwt, AuthorityUtils.createAuthorityList("SCOPE_client.create")); @@ -220,10 +219,12 @@ public void doFilterWhenClientRegistrationRequestValidThenSuccessResponse() thro .tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()) .responseType(OAuth2AuthorizationResponseType.CODE.getValue()) .idTokenSignedResponseAlgorithm(SignatureAlgorithm.RS256.getName()) + .registrationClientUri("http://auth-server:9000/connect/register?client_id=client-id") + .registrationAccessToken("registration-access-token") .build(); // @formatter:on - Jwt jwt = createJwt(); + Jwt jwt = createJwt("client.create"); JwtAuthenticationToken principal = new JwtAuthenticationToken( jwt, AuthorityUtils.createAuthorityList("SCOPE_client.create")); @@ -269,6 +270,198 @@ public void doFilterWhenClientRegistrationRequestValidThenSuccessResponse() thro .isEqualTo(expectedClientRegistrationResponse.getTokenEndpointAuthenticationMethod()); assertThat(clientRegistrationResponse.getIdTokenSignedResponseAlgorithm()) .isEqualTo(expectedClientRegistrationResponse.getIdTokenSignedResponseAlgorithm()); + assertThat(clientRegistrationResponse.getRegistrationClientUri()) + .isEqualTo(expectedClientRegistrationResponse.getRegistrationClientUri()); + assertThat(clientRegistrationResponse.getRegistrationAccessToken()) + .isEqualTo(expectedClientRegistrationResponse.getRegistrationAccessToken()); + } + + @Test + public void doFilterWhenNotClientConfigurationRequestThenNotProcessed() throws Exception { + String requestUri = "/path"; + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + this.filter.doFilter(request, response, filterChain); + + verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); + } + + @Test + public void doFilterWhenClientConfigurationRequestPutThenNotProcessed() throws Exception { + String requestUri = DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI; + MockHttpServletRequest request = new MockHttpServletRequest("PUT", requestUri); + request.setServletPath(requestUri); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + this.filter.doFilter(request, response, filterChain); + + verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); + } + + @Test + public void doFilterWhenClientConfigurationRequestMissingClientIdThenNotProcessed() throws Exception { + String requestUri = DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI; + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + this.filter.doFilter(request, response, filterChain); + + verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); + } + + @Test + public void doFilterWhenClientConfigurationRequestEmptyClientIdThenNotProcessed() throws Exception { + String requestUri = DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI; + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + request.addParameter(OAuth2ParameterNames.CLIENT_ID, ""); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + this.filter.doFilter(request, response, filterChain); + + verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); + } + + @Test + public void doFilterWhenClientConfigurationRequestMultipleClientIdParametersThenInvalidClientError() throws Exception { + String requestUri = DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI; + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-id"); + request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-id2"); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + this.filter.doFilter(request, response, filterChain); + verifyNoInteractions(filterChain); + assertThat(response.getStatus()).isEqualTo(HttpStatus.UNAUTHORIZED.value()); + OAuth2Error error = readError(response); + assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT); + } + + @Test + public void doFilterWhenClientConfigurationRequestInvalidTokenThenUnauthorizedError() throws Exception { + doFilterWhenClientConfigurationRequestInvalidThenError( + OAuth2ErrorCodes.INVALID_TOKEN, HttpStatus.UNAUTHORIZED); + } + + @Test + public void doFilterWhenClientConfigurationRequestInsufficientTokenScopeThenForbiddenError() throws Exception { + doFilterWhenClientConfigurationRequestInvalidThenError( + OAuth2ErrorCodes.INSUFFICIENT_SCOPE, HttpStatus.FORBIDDEN); + } + + @Test + public void doFilterWhenClientConfigurationRequestInvalidClientThenUnauthorizedError() throws Exception { + doFilterWhenClientConfigurationRequestInvalidThenError( + OAuth2ErrorCodes.INVALID_CLIENT, HttpStatus.UNAUTHORIZED); + } + + @Test + public void doFilterWhenClientConfigurationRequestValidThenSuccessResponse() throws Exception { + // @formatter:off + OidcClientRegistration expectedClientRegistrationResponse = OidcClientRegistration.builder() + .clientId("client-id") + .clientIdIssuedAt(Instant.now()) + .clientSecret("client-secret") + .clientName("client-name") + .redirectUri("https://client.example.com") + .grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()) + .grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) + .tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()) + .responseType(OAuth2AuthorizationResponseType.CODE.getValue()) + .idTokenSignedResponseAlgorithm(SignatureAlgorithm.RS256.getName()) + .scope("scope1") + .scope("scope2") + .registrationClientUri("http://auth-server:9000/connect/register?client_id=client-id") + .build(); + // @formatter:on + + Jwt jwt = createJwt("client.read"); + JwtAuthenticationToken principal = new JwtAuthenticationToken( + jwt, AuthorityUtils.createAuthorityList("SCOPE_client.read")); + + OidcClientRegistrationAuthenticationToken clientConfigurationAuthenticationResult = + new OidcClientRegistrationAuthenticationToken(principal, expectedClientRegistrationResponse); + + when(this.authenticationManager.authenticate(any())).thenReturn(clientConfigurationAuthenticationResult); + + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(principal); + SecurityContextHolder.setContext(securityContext); + + String requestUri = DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI; + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + request.setParameter(OAuth2ParameterNames.CLIENT_ID, "client-id"); + + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + this.filter.doFilter(request, response, filterChain); + + verifyNoInteractions(filterChain); + + assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); + OidcClientRegistration clientRegistrationResponse = readClientRegistrationResponse(response); + assertThat(clientRegistrationResponse.getClientId()).isEqualTo(expectedClientRegistrationResponse.getClientId()); + assertThat(clientRegistrationResponse.getClientIdIssuedAt()).isBetween( + expectedClientRegistrationResponse.getClientIdIssuedAt().minusSeconds(1), + expectedClientRegistrationResponse.getClientIdIssuedAt().plusSeconds(1)); + assertThat(clientRegistrationResponse.getClientSecret()).isEqualTo(expectedClientRegistrationResponse.getClientSecret()); + assertThat(clientRegistrationResponse.getClientSecretExpiresAt()).isEqualTo(expectedClientRegistrationResponse.getClientSecretExpiresAt()); + assertThat(clientRegistrationResponse.getClientName()).isEqualTo(expectedClientRegistrationResponse.getClientName()); + assertThat(clientRegistrationResponse.getRedirectUris()) + .containsExactlyInAnyOrderElementsOf(expectedClientRegistrationResponse.getRedirectUris()); + assertThat(clientRegistrationResponse.getGrantTypes()) + .containsExactlyInAnyOrderElementsOf(expectedClientRegistrationResponse.getGrantTypes()); + assertThat(clientRegistrationResponse.getResponseTypes()) + .containsExactlyInAnyOrderElementsOf(expectedClientRegistrationResponse.getResponseTypes()); + assertThat(clientRegistrationResponse.getScopes()) + .containsExactlyInAnyOrderElementsOf(expectedClientRegistrationResponse.getScopes()); + assertThat(clientRegistrationResponse.getTokenEndpointAuthenticationMethod()) + .isEqualTo(expectedClientRegistrationResponse.getTokenEndpointAuthenticationMethod()); + assertThat(clientRegistrationResponse.getIdTokenSignedResponseAlgorithm()) + .isEqualTo(expectedClientRegistrationResponse.getIdTokenSignedResponseAlgorithm()); + assertThat(clientRegistrationResponse.getRegistrationClientUri()) + .isEqualTo(expectedClientRegistrationResponse.getRegistrationClientUri()); + } + + + private void doFilterWhenClientConfigurationRequestInvalidThenError( + String errorCode, HttpStatus status) throws Exception { + Jwt jwt = createJwt("client.read"); + JwtAuthenticationToken principal = new JwtAuthenticationToken( + jwt, AuthorityUtils.createAuthorityList("SCOPE_client.read")); + + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(principal); + SecurityContextHolder.setContext(securityContext); + + when(this.authenticationManager.authenticate(any())) + .thenThrow(new OAuth2AuthenticationException(errorCode)); + + String requestUri = DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI; + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + request.setParameter(OAuth2ParameterNames.CLIENT_ID, "client1"); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + this.filter.doFilter(request, response, filterChain); + + verifyNoInteractions(filterChain); + + assertThat(response.getStatus()).isEqualTo(status.value()); + OAuth2Error error = readError(response); + assertThat(error.getErrorCode()).isEqualTo(errorCode); } private OAuth2Error readError(MockHttpServletResponse response) throws Exception { @@ -290,12 +483,12 @@ private OidcClientRegistration readClientRegistrationResponse(MockHttpServletRes return this.clientRegistrationHttpMessageConverter.read(OidcClientRegistration.class, httpResponse); } - private static Jwt createJwt() { + private static Jwt createJwt(String scope) { // @formatter:off JoseHeader joseHeader = TestJoseHeaders.joseHeader() .build(); JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet() - .claim(OAuth2ParameterNames.SCOPE, Collections.singleton("client.create")) + .claim(OAuth2ParameterNames.SCOPE, Collections.singleton(scope)) .build(); Jwt jwt = Jwt.withTokenValue("jwt-access-token") .headers(headers -> headers.putAll(joseHeader.getHeaders()))