Implement OpenID client registration endpoint

See: https://openid.net/specs/openid-connect-registration-1_0.html#ClientRegistration

Closes gh-57
This commit is contained in:
Ovidiu Popa
2020-12-21 17:21:19 +02:00
committed by Joe Grandja
parent 2712a7b86c
commit 8224a0d971
25 changed files with 2476 additions and 36 deletions

View File

@@ -5,6 +5,7 @@ dependencies {
compile 'org.springframework.security:spring-security-web'
compile 'org.springframework.security:spring-security-oauth2-core'
compile 'org.springframework.security:spring-security-oauth2-jose'
compile 'org.springframework.security:spring-security-oauth2-resource-server'
compile springCoreDependency
compile 'com.nimbusds:nimbus-jose-jwt'
compile 'com.fasterxml.jackson.core:jackson-databind'
@@ -15,6 +16,7 @@ dependencies {
testCompile 'org.assertj:assertj-core'
testCompile 'org.mockito:mockito-core'
testCompile 'com.jayway.jsonpath:json-path'
testCompile 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
provided 'javax.servlet:javax.servlet-api'
}

View File

@@ -21,6 +21,7 @@ import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.RequestMatcher;
@@ -28,8 +29,8 @@ import org.springframework.security.web.util.matcher.RequestMatcher;
* {@link Configuration} for OAuth 2.0 Authorization Server support.
*
* @author Joe Grandja
* @since 0.0.1
* @see OAuth2AuthorizationServerConfigurer
* @since 0.0.1
*/
@Configuration(proxyBeanMethods = false)
public class OAuth2AuthorizationServerConfiguration {
@@ -47,14 +48,16 @@ public class OAuth2AuthorizationServerConfiguration {
new OAuth2AuthorizationServerConfigurer<>();
RequestMatcher endpointsMatcher = authorizationServerConfigurer
.getEndpointsMatcher();
http
.requestMatcher(endpointsMatcher)
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
authorizeRequests.anyRequest().authenticated()
).csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
.apply(authorizationServerConfigurer);
if (authorizationServerConfigurer.isOidcClientRegistrationEnabled()) {
http.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
}
}
// @formatter:on
}

View File

@@ -44,8 +44,10 @@ import org.springframework.security.oauth2.server.authorization.authentication.O
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OidcClientRegistrationAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcClientRegistrationEndpointFilter;
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcProviderConfigurationEndpointFilter;
import org.springframework.security.oauth2.server.authorization.web.NimbusJwkSetEndpointFilter;
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
@@ -69,6 +71,7 @@ import org.springframework.util.StringUtils;
* @author Joe Grandja
* @author Daniel Garnier-Moiroux
* @author Gerardo Roza
* @author Ovidiu Popa
* @since 0.0.1
* @see AbstractHttpConfigurer
* @see RegisteredClientRepository
@@ -81,6 +84,7 @@ import org.springframework.util.StringUtils;
* @see OidcProviderConfigurationEndpointFilter
* @see OAuth2AuthorizationServerMetadataEndpointFilter
* @see OAuth2ClientAuthenticationFilter
* @see OidcClientRegistrationEndpointFilter
*/
public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBuilder<B>>
extends AbstractHttpConfigurer<OAuth2AuthorizationServerConfigurer<B>, B> {
@@ -92,6 +96,7 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
private RequestMatcher jwkSetEndpointMatcher;
private RequestMatcher oidcProviderConfigurationEndpointMatcher;
private RequestMatcher authorizationServerMetadataEndpointMatcher;
private RequestMatcher oidcClientRegistrationEndpointMatcher;
private final RequestMatcher endpointsMatcher = (request) ->
this.authorizationEndpointMatcher.matches(request) ||
this.tokenEndpointMatcher.matches(request) ||
@@ -99,7 +104,8 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
this.tokenRevocationEndpointMatcher.matches(request) ||
this.jwkSetEndpointMatcher.matches(request) ||
this.oidcProviderConfigurationEndpointMatcher.matches(request) ||
this.authorizationServerMetadataEndpointMatcher.matches(request);
this.authorizationServerMetadataEndpointMatcher.matches(request) ||
this.oidcClientRegistrationEndpointMatcher.matches(request);
/**
* Sets the repository of registered clients.
@@ -146,6 +152,17 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
return this.endpointsMatcher;
}
/**
* Returns {@code true} if the OIDC Client Registration endpoint is enabled.
* The default is {@code false}.
*
* @return {@code true} if the OIDC Client Registration endpoint is enabled, {@code false} otherwise
*/
public boolean isOidcClientRegistrationEnabled() {
ProviderSettings providerSettings = getProviderSettings(this.getBuilder());
return providerSettings.isOidClientRegistrationEndpointEnabled();
}
@Override
public void init(B builder) {
ProviderSettings providerSettings = getProviderSettings(builder);
@@ -199,6 +216,11 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
getAuthorizationService(builder));
builder.authenticationProvider(postProcess(tokenRevocationAuthenticationProvider));
OidcClientRegistrationAuthenticationProvider clientRegistrationAuthenticationProvider =
new OidcClientRegistrationAuthenticationProvider(
getAuthorizationService(builder));
builder.authenticationProvider(postProcess(clientRegistrationAuthenticationProvider));
ExceptionHandlingConfigurer<B> exceptionHandling = builder.getConfigurer(ExceptionHandlingConfigurer.class);
if (exceptionHandling != null) {
exceptionHandling.defaultAuthenticationEntryPointFor(
@@ -224,6 +246,9 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
builder.addFilterBefore(postProcess(authorizationServerMetadataEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
}
RegisteredClientRepository registeredClientRepository = getRegisteredClientRepository(builder);
OAuth2AuthorizationService authorizationService = getAuthorizationService(builder);
JWKSource<SecurityContext> jwkSource = getJwkSource(builder);
NimbusJwkSetEndpointFilter jwkSetEndpointFilter = new NimbusJwkSetEndpointFilter(
jwkSource,
@@ -243,8 +268,8 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
OAuth2AuthorizationEndpointFilter authorizationEndpointFilter =
new OAuth2AuthorizationEndpointFilter(
getRegisteredClientRepository(builder),
getAuthorizationService(builder),
registeredClientRepository,
authorizationService,
providerSettings.authorizationEndpoint());
builder.addFilterBefore(postProcess(authorizationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
@@ -265,6 +290,15 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
authenticationManager,
providerSettings.tokenRevocationEndpoint());
builder.addFilterAfter(postProcess(tokenRevocationEndpointFilter), OAuth2TokenIntrospectionEndpointFilter.class);
if (providerSettings.isOidClientRegistrationEndpointEnabled()) {
OidcClientRegistrationEndpointFilter oidcClientRegistrationEndpointFilter =
new OidcClientRegistrationEndpointFilter(
registeredClientRepository,
authenticationManager,
providerSettings.oidcClientRegistrationEndpoint());
builder.addFilterAfter(postProcess(oidcClientRegistrationEndpointFilter), OAuth2TokenRevocationEndpointFilter.class);
}
}
private void initEndpointMatchers(ProviderSettings providerSettings) {
@@ -287,6 +321,9 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
OidcProviderConfigurationEndpointFilter.DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI, HttpMethod.GET.name());
this.authorizationServerMetadataEndpointMatcher = new AntPathRequestMatcher(
OAuth2AuthorizationServerMetadataEndpointFilter.DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI, HttpMethod.GET.name());
this.oidcClientRegistrationEndpointMatcher = new AntPathRequestMatcher(
providerSettings.oidcClientRegistrationEndpoint(),
HttpMethod.POST.name());
}
private static void validateProviderSettings(ProviderSettings providerSettings) {

View File

@@ -0,0 +1,130 @@
/*
* 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.core.oidc;
import org.springframework.security.oauth2.core.ClaimAccessor;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import java.time.Instant;
import java.util.List;
/**
* A {@link ClaimAccessor} for the "claims" that can be returned
* in the OpenID Client Registration Response.
*
* @author Ovidiu Popa
* @since 0.1.1
* @see ClaimAccessor
* @see OidcClientMetadataClaimNames
* @see OidcClientRegistration
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata">2. Client Metadata</a>
*/
public interface OidcClientMetadataClaimAccessor extends ClaimAccessor {
/**
* Returns the redirect URI(s) that the client may use in redirect-based flows.
*
* @return the {@code List} of redirect URI(s)
*/
default List<String> getRedirectUris() {
return getClaimAsStringList(OidcClientMetadataClaimNames.REDIRECT_URIS);
}
/**
* Returns the OAuth 2.0 {@code response_type} values that the client may use.
*
* @return the {@code List} of {@code response_type}
*/
default List<String> getResponseTypes() {
return getClaimAsStringList(OidcClientMetadataClaimNames.RESPONSE_TYPES);
}
/**
* Returns the authorization {@code grant_types} that the client may use.
*
* @return the {@code List} of authorization {@code grant_types}
*/
default List<String> getGrantTypes() {
return getClaimAsStringList(OidcClientMetadataClaimNames.GRANT_TYPES);
}
/**
* Returns the {@code client_name}.
*
* @return the {@code client_name}
*/
default String getClientName() {
return getClaimAsString(OidcClientMetadataClaimNames.CLIENT_NAME);
}
/**
* Returns the scope(s) that the client may use.
*
* @return the scope(s)
*/
default String getScope() {
return getClaimAsString(OidcClientMetadataClaimNames.SCOPE);
}
/**
* Returns the {@link ClientAuthenticationMethod authentication method} that the client may use.
*
* @return the {@link ClientAuthenticationMethod authentication method}
*/
default String getTokenEndpointAuthenticationMethod() {
return getClaimAsString(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD);
}
/**
* Returns the {@code client_id}.
*
* @return the {@code client_id}
*/
default String getClientId() {
return getClaimAsString(OidcClientMetadataClaimNames.CLIENT_ID);
}
/**
* Returns the {@code client_id_issued_at} timestamp.
*
* @return the {@code client_id_issued_at} timestamp
*/
default Instant getClientIdIssuedAt() {
return getClaimAsInstant(OidcClientMetadataClaimNames.CLIENT_ID_ISSUED_AT);
}
/**
* Returns the {@code client_secret}.
*
* @return the {@code client_secret}
*/
default String getClientSecret() {
return getClaimAsString(OidcClientMetadataClaimNames.CLIENT_SECRET);
}
/**
* Returns the {@code client_secret_expires_at} timestamp.
*
* @return the {@code client_secret_expires_at} timestamp
*/
default Instant getClientSecretExpiresAt() {
return getClaimAsInstant(OidcClientMetadataClaimNames.CLIENT_SECRET_EXPIRES_AT);
}
}

View File

@@ -0,0 +1,79 @@
/*
* 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.core.oidc;
/**
* The names of the "claims" defined by OpenID Client Registration 1.0 that can be returned
* in the OpenID Client Registration Response.
*
* @author Ovidiu Popa
* @since 0.1.1
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata">2. Client Metadata</a>
*/
public interface OidcClientMetadataClaimNames {
//request
/**
* {@code redirect_uris} - the redirect URI(s) that the client may use in redirect-based flows
*/
String REDIRECT_URIS = "redirect_uris";
/**
* {@code response_types} - the OAuth 2.0 {@code response_type} values that the client may use
*/
String RESPONSE_TYPES = "response_types";
/**
* {@code grant_types} - the OAuth 2.0 authorization {@code grant_types} that the client may use
*/
String GRANT_TYPES = "grant_types";
/**
* {@code client_name} - the {@code client_name}
*/
String CLIENT_NAME = "client_name";
/**
* {@code scope} - the scope(s) that the client may use
*/
String SCOPE = "scope";
/**
* {@code token_endpoint_auth_method} - the {@link org.springframework.security.oauth2.core.ClientAuthenticationMethod authentication method} that the client may use.
*/
String TOKEN_ENDPOINT_AUTH_METHOD = "token_endpoint_auth_method";
//response
/**
* {@code client_id} - the {@code client_id}
*/
String CLIENT_ID = "client_id";
/**
* {@code client_secret} - the {@code client_secret}
*/
String CLIENT_SECRET = "client_secret";
/**
* {@code client_id_issued_at} - the timestamp when the client id was issued
*/
String CLIENT_ID_ISSUED_AT = "client_id_issued_at";
/**
* {@code client_secret_expires_at} - the timestamp when the client secret expires
*/
String CLIENT_SECRET_EXPIRES_AT = "client_secret_expires_at";
}

View File

@@ -0,0 +1,349 @@
/*
* 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.core.oidc;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.Version;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
import org.springframework.util.Assert;
import java.io.Serializable;
import java.net.URI;
import java.net.URL;
import java.time.Instant;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
/**
* A representation of an OpenID Client Registration Request and Response,
* which contains a set of claims defined by the
* OpenID Connect Registration 1.0 specification.
*
* @author Ovidiu Popa
* @since 0.1.1
* @see OidcClientMetadataClaimAccessor
* @see <a href="https://openid.net/specs/openid-connect-registration-1_0.html#ClientRegistration">3.1. Client Registration Request</a>
*/
public final class OidcClientRegistration implements OidcClientMetadataClaimAccessor, Serializable {
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
private final Map<String, Object> claims;
private OidcClientRegistration(Map<String, Object> claims) {
this.claims = Collections.unmodifiableMap(claims);
}
/**
* Returns the OpenID Client Registration metadata.
*
* @return a {@code Map} of the metadata values
*/
@Override
public Map<String, Object> getClaims() {
return this.claims;
}
/**
* Constructs a new {@link OidcClientRegistration.Builder} with empty claims.
*
* @return the {@link OidcClientRegistration.Builder}
*/
public static Builder builder() {
return new Builder();
}
/**
* Constructs a new {@link Builder} with the provided claims.
*
* @param claims the claims to initialize the builder
*/
public static Builder withClaims(Map<String, Object> claims) {
Assert.notEmpty(claims, "claims cannot be empty");
return new Builder()
.claims(c -> c.putAll(claims));
}
public static class Builder {
private final Map<String, Object> claims = new LinkedHashMap<>();
private Builder() {
}
/**
* Add this Redirect URI to the collection of {@code redirect_uris} in the resulting
* {@link OidcClientRegistration}, REQUIRED.
*
* @param redirectUri the OAuth 2.0 {@code redirect_uri} value that client supports
* @return the {@link Builder} for further configuration
*/
public Builder redirectUri(String redirectUri) {
addClaimToClaimList(OidcClientMetadataClaimNames.REDIRECT_URIS, redirectUri);
return this;
}
/**
* A {@code Consumer} of the Redirect URI(s) allowing the ability to add, replace, or remove.
*
* @param redirectUriConsumer a {@code Consumer} of the Redirect URI(s)
* @return the {@link Builder} for further configuration
*/
public Builder redirectUris(Consumer<List<String>> redirectUriConsumer) {
acceptClaimValues(OidcClientMetadataClaimNames.REDIRECT_URIS, redirectUriConsumer);
return this;
}
/**
* Add this Response Type to the collection of {@code response_types} in the resulting
* {@link OidcClientRegistration}, OPTIONAL.
*
* @param responseType the OAuth 2.0 {@code response_type} value that client supports
* @return the {@link Builder} for further configuration
*/
public Builder responseType(String responseType) {
addClaimToClaimList(OidcClientMetadataClaimNames.RESPONSE_TYPES, responseType);
return this;
}
/**
* Add {@code Consumer} of {@code response_types} allowing the ability to add, replace, or remove
* {@link OidcClientRegistration}, OPTIONAL.
*
* @param responseType the OAuth 2.0 {@code response_type} value that client supports
* @return the {@link Builder} for further configuration
*/
public Builder responseTypes(Consumer<List<String>> responseType) {
acceptClaimValues(OidcClientMetadataClaimNames.RESPONSE_TYPES, responseType);
return this;
}
/**
* Sets {@code client_name} claim in the resulting
* {@link OidcClientRegistration}, OPTIONAL.
*
* @param clientName the OAuth 2.0 {@code client_name} of the registered client
* @return the {@link Builder} for further configuration
*/
public Builder clientName(String clientName) {
return claim(OidcClientMetadataClaimNames.CLIENT_NAME, clientName);
}
/**
* Sets {@code client_id} claim in the resulting
* {@link OidcClientRegistration}.
*
* @param clientId the OAuth 2.0 {@code client_id} of the registered client
* @return the {@link Builder} for further configuration
*/
public Builder clientId(String clientId) {
return claim(OidcClientMetadataClaimNames.CLIENT_ID, clientId);
}
/**
* Sets {@code client_id_issued_at} claim in the resulting
* {@link OidcClientRegistration}.
*
* @param clientIssuedAt the timestamp {@code client_id_issued_at} when the client was issued
* @return the {@link Builder} for further configuration
*/
public Builder clientIdIssuedAt(Instant clientIssuedAt) {
return claim(OidcClientMetadataClaimNames.CLIENT_ID_ISSUED_AT, clientIssuedAt);
}
/**
* Sets {@code client_secret} claim in the resulting
* {@link OidcClientRegistration}.
*
* @param clientSecret the {@code client_secret} of the registered client
* @return the {@link Builder} for further configuration
*/
public Builder clientSecret(String clientSecret) {
return claim(OidcClientMetadataClaimNames.CLIENT_SECRET, clientSecret);
}
/**
* Sets {@code client_secret_expires_at} claim in the resulting
* {@link OidcClientRegistration}.
*
* @param clientSecretExpiresAt the timestamp {@code client_secret_expires_at} when the client_secret expires
* @return the {@link Builder} for further configuration
*/
public Builder clientSecretExpiresAt(Instant clientSecretExpiresAt) {
return claim(OidcClientMetadataClaimNames.CLIENT_SECRET_EXPIRES_AT, clientSecretExpiresAt);
}
/**
* Add this Grant Type to the collection of {@code grant_types_supported} in the resulting
* {@link OidcClientRegistration}, OPTIONAL.
*
* @param grantType the OAuth 2.0 {@code grant_type} value that client supports
* @return the {@link Builder} for further configuration
*/
public Builder grantType(String grantType) {
addClaimToClaimList(OidcClientMetadataClaimNames.GRANT_TYPES, grantType);
return this;
}
/**
* A {@code Consumer} of the Grant Type(s) allowing the ability to add, replace, or remove.
*
* @param grantTypesConsumer a {@code Consumer} of the Grant Type(s)
* @return the {@link Builder} for further configuration
*/
public Builder grantTypes(Consumer<List<String>> grantTypesConsumer) {
acceptClaimValues(OidcClientMetadataClaimNames.GRANT_TYPES, grantTypesConsumer);
return this;
}
/**
* Add this Scope to the collection of {@code scopes_supported} in the resulting
* {@link OidcClientRegistration}, RECOMMENDED.
*
* @param scope the OAuth 2.0 {@code scope} value that client supports
* @return the {@link Builder} for further configuration
*/
public Builder scope(String scope) {
claim(OidcClientMetadataClaimNames.SCOPE, scope);
return this;
}
/**
* Add {@code Consumer} of {@code scopes} allowing the ability to add, replace, or remove
* {@link OidcClientRegistration}, RECOMMENDED.
*
* @param scopesConsumer the OAuth 2.0 {@code scope} value that client supports
* @return the {@link Builder} for further configuration
*/
public Builder scopes(Consumer<List<String>> scopesConsumer) {
acceptClaimValues(OidcClientMetadataClaimNames.SCOPE, scopesConsumer);
return this;
}
/**
* Add this Token endpoint authentication method to the collection of {@code token_endpoint_auth_method} in the resulting
* {@link OidcClientRegistration}, OPTIONAL.
*
* @param tokenEndpointAuthenticationMethod the OAuth 2.0 {@code token_endpoint_auth_method} value that client supports
* @return the {@link Builder} for further configuration
*/
public Builder tokenEndpointAuthenticationMethod(String tokenEndpointAuthenticationMethod) {
claim(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD, tokenEndpointAuthenticationMethod);
return this;
}
/**
* Add this claim in the resulting {@link OidcClientRegistration}.
*
* @param name the claim name
* @param value the claim value
* @return the {@link Builder} for further configuration
*/
public Builder claim(String name, Object value) {
Assert.hasText(name, "name cannot be empty");
Assert.notNull(value, "value cannot be null");
this.claims.put(name, value);
return this;
}
/**
* Provides access to every {@link #claim(String, Object)} declared so far with
* the possibility to add, replace, or remove.
*
* @param claimsConsumer a {@code Consumer} of the claims
* @return the {@link Builder} for further configurations
*/
public Builder claims(Consumer<Map<String, Object>> claimsConsumer) {
claimsConsumer.accept(this.claims);
return this;
}
public OidcClientRegistration build() {
this.claims.computeIfAbsent(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD,
k -> ClientAuthenticationMethod.BASIC.getValue());
// If omitted, the default is that the Client will use only the authorization_code Grant Type.
this.claims.computeIfAbsent(OidcClientMetadataClaimNames.GRANT_TYPES,
k -> Collections.singletonList(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()));
//If omitted, the default is that the Client will use only the code Response Type.
this.claims.computeIfAbsent(OidcClientMetadataClaimNames.RESPONSE_TYPES,
k -> Collections.singletonList(OAuth2AuthorizationResponseType.CODE.getValue()));
validateRedirectUris();
validateReponseTypesClaim();
validateGrantTypesClaim();
return new OidcClientRegistration(this.claims);
}
private void validateRedirectUris() {
// redirect_uris is required
Assert.notNull(this.claims.get(OidcClientMetadataClaimNames.REDIRECT_URIS), "redirect_uris cannot be null");
Assert.isInstanceOf(List.class, this.claims.get(OidcClientMetadataClaimNames.REDIRECT_URIS), "redirect_uris must be of type list");
Assert.notEmpty((List<?>) this.claims.get(OidcClientMetadataClaimNames.REDIRECT_URIS), "redirect_uris must not be empty");
((List<?>) this.claims.get(OidcClientMetadataClaimNames.REDIRECT_URIS)).forEach(
url -> validateURL(url, "redirect_uri must be a valid URL")
);
}
private void validateGrantTypesClaim() {
Assert.isInstanceOf(List.class, this.claims.get(OidcClientMetadataClaimNames.GRANT_TYPES), "grant_types must be of type List");
List<?> grantTypes = (List<?>) this.claims.get(OidcClientMetadataClaimNames.GRANT_TYPES);
// If empty, the default is that the Client will use only the authorization_code Grant Type.
if (grantTypes.isEmpty()) {
this.claims.put(OidcClientMetadataClaimNames.GRANT_TYPES,
Collections.singletonList(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()));
}
}
private void validateReponseTypesClaim() {
Assert.isInstanceOf(List.class, this.claims.get(OidcClientMetadataClaimNames.RESPONSE_TYPES), "response_types must be of type List");
List<?> responseTypes = (List<?>) this.claims.get(OidcClientMetadataClaimNames.RESPONSE_TYPES);
//If empty, the default is that the Client will use only the code Response Type.
if (responseTypes.isEmpty()) {
this.claims.put(OidcClientMetadataClaimNames.RESPONSE_TYPES, Collections.singletonList(OAuth2AuthorizationResponseType.CODE.getValue()));
}
}
private static void validateURL(Object url, String errorMessage) {
if (URL.class.isAssignableFrom(url.getClass())) {
return;
}
try {
new URI(url.toString()).toURL();
} catch (Exception ex) {
throw new IllegalArgumentException(errorMessage, ex);
}
}
@SuppressWarnings("unchecked")
private void addClaimToClaimList(String name, String value) {
Assert.hasText(name, "name cannot be empty");
Assert.notNull(value, "value cannot be null");
this.claims.computeIfAbsent(name, k -> new LinkedList<String>());
((List<String>) this.claims.get(name)).add(value);
}
@SuppressWarnings("unchecked")
private void acceptClaimValues(String name, Consumer<List<String>> valuesConsumer) {
Assert.hasText(name, "name cannot be empty");
Assert.notNull(valuesConsumer, "valuesConsumer cannot be null");
this.claims.computeIfAbsent(name, k -> new LinkedList<String>());
List<String> values = (List<String>) this.claims.get(name);
valuesConsumer.accept(values);
}
}
}

View File

@@ -0,0 +1,157 @@
/*
* 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.core.oidc.http.converter;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.security.oauth2.core.converter.ClaimConversionService;
import org.springframework.security.oauth2.core.converter.ClaimTypeConverter;
import org.springframework.security.oauth2.core.oidc.OidcClientMetadataClaimNames;
import org.springframework.security.oauth2.core.oidc.OidcClientRegistration;
import org.springframework.util.Assert;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
/**
* A {@link HttpMessageConverter} for an {@link OidcClientRegistration OpenID Client Registration Response}.
*
* @author Ovidiu Popa
* @see AbstractHttpMessageConverter
* @see OidcClientRegistration
* @since 0.1.1
*/
public class OidcClientRegistrationHttpMessageConverter extends AbstractHttpMessageConverter<OidcClientRegistration> {
private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP =
new ParameterizedTypeReference<Map<String, Object>>() {
};
private Converter<Map<String, Object>, OidcClientRegistration> clientRegistrationConverter =
new OidcClientRegistrationConverter();
private Converter<OidcClientRegistration, Map<String, Object>> clientRegistrationParametersConverter = OidcClientRegistration::getClaims;
private final GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter();
public OidcClientRegistrationHttpMessageConverter() {
super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
}
@Override
protected boolean supports(Class<?> clazz) {
return OidcClientRegistration.class.isAssignableFrom(clazz);
}
@Override
@SuppressWarnings("unchecked")
protected OidcClientRegistration readInternal(Class<? extends OidcClientRegistration> clazz, HttpInputMessage inputMessage)
throws HttpMessageNotReadableException {
try {
Map<String, Object> clientRegistrationParameters =
(Map<String, Object>) this.jsonMessageConverter.read(STRING_OBJECT_MAP.getType(), null, inputMessage);
return this.clientRegistrationConverter.convert(clientRegistrationParameters);
} catch (Exception ex) {
throw new HttpMessageNotReadableException(
"An error occurred reading the OpenID Client Registration Request: " + ex.getMessage(), ex, inputMessage);
}
}
@Override
protected void writeInternal(OidcClientRegistration oidcClientRegistration, HttpOutputMessage outputMessage)
throws HttpMessageNotWritableException {
try {
Map<String, Object> claims = clientRegistrationParametersConverter.convert(oidcClientRegistration);
this.jsonMessageConverter.write(
claims,
STRING_OBJECT_MAP.getType(),
MediaType.APPLICATION_JSON,
outputMessage
);
} catch (Exception ex) {
throw new HttpMessageNotWritableException(
"An error occurred writing the OpenID Client Registration response: " + ex.getMessage(), ex);
}
}
/**
* Sets the {@link Converter} used for converting the OpenID Client Registration parameters
* to an {@link OidcClientRegistration}.
*
* @param clientRegistrationConverter the {@link Converter} used for converting to an
* {@link OidcClientRegistration}
*/
public void setClientRegistrationConverter(Converter<Map<String, Object>, OidcClientRegistration> clientRegistrationConverter) {
Assert.notNull(clientRegistrationConverter, "clientRegistrationConverter cannot be null");
this.clientRegistrationConverter = clientRegistrationConverter;
}
/**
* Sets the {@link Converter} used for converting the {@link OidcClientRegistration} to a
* {@code Map} representation of the OpenID Client Registration Response.
*
* @param clientRegistrationParametersConverter the {@link Converter} used for converting to a
* {@code Map} representation of the OpenID Client Registration Response
*/
public final void setClientRegistrationParametersConverter(
Converter<OidcClientRegistration, Map<String, Object>> clientRegistrationParametersConverter) {
Assert.notNull(clientRegistrationParametersConverter, "clientRegistrationParametersConverter cannot be null");
this.clientRegistrationParametersConverter = clientRegistrationParametersConverter;
}
private static final class OidcClientRegistrationConverter implements Converter<Map<String, Object>, OidcClientRegistration> {
private static final ClaimConversionService CLAIM_CONVERSION_SERVICE = ClaimConversionService.getSharedInstance();
private static final TypeDescriptor OBJECT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Object.class);
private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
private final ClaimTypeConverter claimTypeConverter;
private OidcClientRegistrationConverter() {
Converter<Object, ?> collectionStringConverter = getConverter(
TypeDescriptor.collection(Collection.class, STRING_TYPE_DESCRIPTOR));
Converter<Object, ?> stringConverter = getConverter(STRING_TYPE_DESCRIPTOR);
Map<String, Converter<Object, ?>> claimConverters = new HashMap<>();
claimConverters.put(OidcClientMetadataClaimNames.REDIRECT_URIS, collectionStringConverter);
claimConverters.put(OidcClientMetadataClaimNames.RESPONSE_TYPES, collectionStringConverter);
claimConverters.put(OidcClientMetadataClaimNames.GRANT_TYPES, collectionStringConverter);
claimConverters.put(OidcClientMetadataClaimNames.CLIENT_NAME, stringConverter);
claimConverters.put(OidcClientMetadataClaimNames.SCOPE, stringConverter);
claimConverters.put(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD, stringConverter);
this.claimTypeConverter = new ClaimTypeConverter(claimConverters);
}
@Override
public OidcClientRegistration convert(Map<String, Object> source) {
Map<String, Object> parsedClaims = this.claimTypeConverter.convert(source);
return OidcClientRegistration.withClaims(parsedClaims).build();
}
private static Converter<Object, ?> getConverter(TypeDescriptor targetDescriptor) {
return source -> CLAIM_CONVERSION_SERVICE.convert(source, OBJECT_TYPE_DESCRIPTOR, targetDescriptor);
}
}
}

View File

@@ -0,0 +1,86 @@
/*
* 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.authentication;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
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.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.util.Assert;
/**
* An {@link AuthenticationProvider} implementation for OpenID Client Registration Endpoint.
*
* @author Ovidiu Popa
* @since 0.1.1
* @see JwtAuthenticationToken
* @see OAuth2AuthorizationService
*/
public class OidcClientRegistrationAuthenticationProvider implements AuthenticationProvider {
private static final String CLIENT_CREATE_SCOPE = "client.create";
private final OAuth2AuthorizationService authorizationService;
/**
* Constructs an {@code OidcClientRegistrationAuthenticationProvider} using the provided parameters.
*
* @param authorizationService the authorization service
*/
public OidcClientRegistrationAuthenticationProvider(OAuth2AuthorizationService authorizationService) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
this.authorizationService = authorizationService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
JwtAuthenticationToken jwtAuthenticationToken =
(JwtAuthenticationToken) authentication;
String tokenValue = jwtAuthenticationToken.getToken().getTokenValue();
OAuth2Authorization authorization = this.authorizationService.findByToken(tokenValue, OAuth2TokenType.ACCESS_TOKEN);
if (authorization == null) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
}
OAuth2Authorization.Token<OAuth2AccessToken> authorizationAccessToken =
authorization.getAccessToken();
if (authorizationAccessToken.isInvalidated()) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
}
OAuth2AccessToken accessToken = authorizationAccessToken.getToken();
if (!accessToken.getScopes().contains(CLIENT_CREATE_SCOPE)) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
}
authorization = OAuth2AuthenticationProviderUtils.invalidate(authorization, accessToken);
this.authorizationService.save(authorization);
return jwtAuthenticationToken;
}
@Override
public boolean supports(Class<?> authentication) {
return JwtAuthenticationToken.class.isAssignableFrom(authentication);
}
}

View File

@@ -88,4 +88,21 @@ public final class InMemoryRegisteredClientRepository implements RegisteredClien
Assert.hasText(clientId, "clientId cannot be empty");
return this.clientIdRegistrationMap.get(clientId);
}
@Override
public void saveClient(RegisteredClient registeredClient) {
Assert.notNull(registeredClient, "registeredClient cannot be null");
String id = registeredClient.getId();
if (idRegistrationMap.containsKey(id)) {
throw new IllegalArgumentException("Registered client must be unique. " +
"Found duplicate identifier: " + id);
}
String clientId = registeredClient.getClientId();
if (clientIdRegistrationMap.containsKey(clientId)) {
throw new IllegalArgumentException("Registered client must be unique. " +
"Found duplicate client identifier: " + clientId);
}
this.idRegistrationMap.put(registeredClient.getId(), registeredClient);
this.clientIdRegistrationMap.put(registeredClient.getClientId(), registeredClient);
}
}

View File

@@ -47,4 +47,11 @@ public interface RegisteredClientRepository {
@Nullable
RegisteredClient findByClientId(String clientId);
/**
* Saves a new registered client
*
* @param registeredClient the {@link RegisteredClient} to be saved
*/
void saveClient(RegisteredClient registeredClient);
}

View File

@@ -33,6 +33,8 @@ public class ProviderSettings extends Settings {
public static final String JWK_SET_ENDPOINT = PROVIDER_SETTING_BASE.concat("jwk-set-endpoint");
public static final String TOKEN_REVOCATION_ENDPOINT = PROVIDER_SETTING_BASE.concat("token-revocation-endpoint");
public static final String TOKEN_INTROSPECTION_ENDPOINT = PROVIDER_SETTING_BASE.concat("token-introspection-endpoint");
public static final String OIDC_CLIENT_REGISTRATION_ENDPOINT = PROVIDER_SETTING_BASE.concat("oidc-client-registration-endpoint");
public static final String ENABLE_OIDC_CLIENT_REGISTRATION_ENDPOINT = PROVIDER_SETTING_BASE.concat("enable-oidc-client-registration-endpoint");
/**
* Constructs a {@code ProviderSettings}.
@@ -164,6 +166,46 @@ public class ProviderSettings extends Settings {
return setting(TOKEN_INTROSPECTION_ENDPOINT, tokenIntrospectionEndpoint);
}
/**
* Returns the Provider's OAuth 2.0 OIDC Client Registration endpoint. The default is {@code /connect/register}.
*
* @return the OIDC Client Registration endpoint
*/
public String oidcClientRegistrationEndpoint() {
return setting(OIDC_CLIENT_REGISTRATION_ENDPOINT);
}
/**
* Sets the Provider's OAuth 2.0 OIDC Client Registration endpoint.
*
* @param oidcClientRegistrationEndpoint the Token Revocation endpoint
* @return the {@link ProviderSettings} for further configuration
*/
public ProviderSettings oidcClientRegistrationEndpoint(String oidcClientRegistrationEndpoint) {
return setting(OIDC_CLIENT_REGISTRATION_ENDPOINT, oidcClientRegistrationEndpoint);
}
/**
* Returns {@code true} if the OIDC Client Registration endpoint is enabled.
* The default is {@code false}.
*
* @return {@code true} if the OIDC Client Registration endpoint is enabled, {@code false} otherwise
*/
public boolean isOidClientRegistrationEndpointEnabled() {
return setting(ENABLE_OIDC_CLIENT_REGISTRATION_ENDPOINT);
}
/**
* Set to {@code true} if the OIDC Client Registration Endpoint should be enabled.
*
* @param oidClientRegistrationEndpointEnabled {@code true} if the OIDC Client Registration endpoint should enabled
* @return the {@link ProviderSettings}
*/
public ProviderSettings isOidClientRegistrationEndpointEnabled(boolean oidClientRegistrationEndpointEnabled) {
setting(ENABLE_OIDC_CLIENT_REGISTRATION_ENDPOINT, oidClientRegistrationEndpointEnabled);
return this;
}
protected static Map<String, Object> defaultSettings() {
Map<String, Object> settings = new HashMap<>();
settings.put(AUTHORIZATION_ENDPOINT, "/oauth2/authorize");
@@ -171,6 +213,8 @@ public class ProviderSettings extends Settings {
settings.put(JWK_SET_ENDPOINT, "/oauth2/jwks");
settings.put(TOKEN_REVOCATION_ENDPOINT, "/oauth2/revoke");
settings.put(TOKEN_INTROSPECTION_ENDPOINT, "/oauth2/introspect");
settings.put(OIDC_CLIENT_REGISTRATION_ENDPOINT, "/connect/register");
settings.put(ENABLE_OIDC_CLIENT_REGISTRATION_ENDPOINT, false);
return settings;
}
}

View File

@@ -0,0 +1,194 @@
/*
* 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.web;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
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.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.Instant;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* A {@code Filter} that processes OpenID Client Registration Requests.
* @author Ovidiu Popa
* @since 0.1.1
* @see OidcClientRegistration
* @see <a href="https://openid.net/specs/openid-connect-registration-1_0.html#ClientRegistration">3.1. Client Registration Request</a>
*/
public class OidcClientRegistrationEndpointFilter extends OncePerRequestFilter {
/**
* The default endpoint {@code URI} for OpenID Client Registration requests.
*/
public static final String DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI = "/connect/register";
private static final String SCOPE_CLAIM_DELIMITER = " ";
private final OidcClientRegistrationHttpMessageConverter clientRegistrationHttpMessageConverter =
new OidcClientRegistrationHttpMessageConverter();
private final RegisteredClientRepository registeredClientRepository;
private final OidcClientRegistrationToRegisteredClientConverter oidcClientToRegisteredClientConverter =
new OidcClientRegistrationToRegisteredClientConverter();
private final RegisteredClientToOidcClientRegistrationConverter registeredClientToOidcClientConverter =
new RegisteredClientToOidcClientRegistrationConverter();
private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter =
new OAuth2ErrorHttpMessageConverter();
private final RequestMatcher requestMatcher;
private final AuthenticationManager authenticationManager;
/**
* Constructs an {@code OidcClientRegistrationEndpointFilter} using the provided parameters.
*
* @param registeredClientRepository the repository of registered clients
* @param authenticationManager the authentication manager
*/
public OidcClientRegistrationEndpointFilter(RegisteredClientRepository registeredClientRepository,
AuthenticationManager authenticationManager) {
this(registeredClientRepository, authenticationManager, DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI);
}
/**
* Constructs an {@code OidcClientRegistrationEndpointFilter} using the provided parameters.
*
* @param registeredClientRepository the repository of registered clients
* @param authenticationManager the authentication manager
* @param oidcClientRegistrationUri the endpoint {@code URI} for OIDC Client Registration requests
*/
public OidcClientRegistrationEndpointFilter(RegisteredClientRepository registeredClientRepository,
AuthenticationManager authenticationManager, String oidcClientRegistrationUri) {
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
Assert.hasText(oidcClientRegistrationUri, "oidcClientRegistrationUri cannot be empty");
this.registeredClientRepository = registeredClientRepository;
this.authenticationManager = authenticationManager;
this.requestMatcher = new AntPathRequestMatcher(
oidcClientRegistrationUri,
HttpMethod.POST.name()
);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!this.requestMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
try {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
authenticationManager.authenticate(authentication);
OidcClientRegistration clientRegistrationRequest =
this.clientRegistrationHttpMessageConverter.read(OidcClientRegistration.class, new ServletServerHttpRequest(request));
RegisteredClient registeredClient = this.oidcClientToRegisteredClientConverter
.convert(clientRegistrationRequest);
this.registeredClientRepository.saveClient(registeredClient);
OidcClientRegistration convert = this.registeredClientToOidcClientConverter
.convert(registeredClient);
final ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
httpResponse.setStatusCode(HttpStatus.CREATED);
this.clientRegistrationHttpMessageConverter.write(
convert, MediaType.APPLICATION_JSON, httpResponse);
} catch (OAuth2AuthenticationException ex) {
SecurityContextHolder.clearContext();
sendErrorResponse(response, ex.getError());
}
}
private void sendErrorResponse(HttpServletResponse response, OAuth2Error error) throws IOException {
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
httpResponse.setStatusCode(HttpStatus.BAD_REQUEST);
this.errorHttpResponseConverter.write(error, null, httpResponse);
}
private static class OidcClientRegistrationToRegisteredClientConverter implements Converter<OidcClientRegistration, RegisteredClient> {
@Override
public RegisteredClient convert(OidcClientRegistration clientRegistration) {
return RegisteredClient.withId(UUID.randomUUID().toString())
.clientId(UUID.randomUUID().toString())
.clientSecret(UUID.randomUUID().toString())
.redirectUris(redirectUris ->
redirectUris.addAll(clientRegistration.getRedirectUris()))
.clientAuthenticationMethod(new ClientAuthenticationMethod(clientRegistration.getTokenEndpointAuthenticationMethod()))
.authorizationGrantTypes(grantTypes ->
grantTypes.addAll(this.grantTypes(clientRegistration)))
.scopes(scopes ->
scopes.addAll(Arrays.asList(clientRegistration.getScope().split(SCOPE_CLAIM_DELIMITER))))
.clientSettings(clientSettings -> clientSettings.requireUserConsent(true))
.build();
}
private List<AuthorizationGrantType> grantTypes(OidcClientRegistration clientRegistration) {
return clientRegistration.getGrantTypes().stream()
.map(AuthorizationGrantType::new)
.collect(Collectors.toList());
}
}
private static class RegisteredClientToOidcClientRegistrationConverter implements Converter<RegisteredClient, OidcClientRegistration> {
@Override
public OidcClientRegistration convert(RegisteredClient source) {
return OidcClientRegistration.builder()
.clientId(source.getClientId())
.redirectUris(uris -> uris.addAll(source.getRedirectUris()))
.clientIdIssuedAt(Instant.now())
.clientSecret(source.getClientSecret())
.clientSecretExpiresAt(Instant.EPOCH)
.responseType(OAuth2AuthorizationResponseType.CODE.getValue())
.grantTypes(grantTypes ->
grantTypes.addAll(source.getAuthorizationGrantTypes().stream().map(AuthorizationGrantType::getValue)
.collect(Collectors.toList()))
)
.scope(String.join(SCOPE_CLAIM_DELIMITER, source.getScopes()))
.tokenEndpointAuthenticationMethod(source.getClientAuthenticationMethods().iterator().next().getValue())
.build();
}
}
}

View File

@@ -22,7 +22,6 @@ import org.junit.Before;
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.context.annotation.Import;

View File

@@ -15,10 +15,6 @@
*/
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
@@ -26,7 +22,6 @@ import org.junit.Before;
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.context.annotation.Import;
@@ -37,16 +32,20 @@ import org.springframework.security.config.test.SpringTestRule;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.jose.TestJwks;
import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
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.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;

View File

@@ -15,14 +15,6 @@
*/
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.util.Base64;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
@@ -30,7 +22,6 @@ import org.junit.Before;
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.context.annotation.Import;
@@ -45,6 +36,7 @@ import org.springframework.security.config.test.SpringTestRule;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2TokenType;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
@@ -52,21 +44,28 @@ import org.springframework.security.oauth2.jose.TestJwks;
import org.springframework.security.oauth2.jose.TestKeys;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
import org.springframework.security.oauth2.core.OAuth2TokenType;
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.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.util.Base64;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.CoreMatchers.containsString;
import static org.mockito.ArgumentMatchers.any;

View File

@@ -15,10 +15,6 @@
*/
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
@@ -27,7 +23,6 @@ import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
@@ -38,12 +33,12 @@ import org.springframework.security.config.test.SpringTestRule;
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.OAuth2TokenType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames2;
import org.springframework.security.oauth2.jose.TestJwks;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
import org.springframework.security.oauth2.core.OAuth2TokenType;
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;
@@ -53,6 +48,10 @@ import org.springframework.test.web.servlet.MockMvc;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;

View File

@@ -0,0 +1,241 @@
/*
* 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.config.annotation.web.configurers.oauth2.server.authorization;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.mock.http.client.MockClientHttpResponse;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.config.test.SpringTestRule;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2TokenType;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
import org.springframework.security.oauth2.core.oidc.OidcClientRegistration;
import org.springframework.security.oauth2.core.oidc.http.converter.OidcClientRegistrationHttpMessageConverter;
import org.springframework.security.oauth2.jose.TestJwks;
import org.springframework.security.oauth2.jose.TestKeys;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
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.client.TestRegisteredClients;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Integration tests for OpenID Connect 1.0 Client Registration Endpoint.
*
* @author Ovidiu Popa
* @since 0.1.1
*/
public class OidcClientRegistrationTests {
private static final OidcClientRegistration.Builder OIDC_CLIENT_REGISTRATION = OidcClientRegistration.builder()
.redirectUri("https://localhost:8080/client")
.responseType(OAuth2AuthorizationResponseType.CODE.getValue())
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.BASIC.getValue())
.scope("test");
private static final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter =
new OAuth2AccessTokenResponseHttpMessageConverter();
private static final OidcClientRegistrationHttpMessageConverter clientRegistrationHttpMessageConverter =
new OidcClientRegistrationHttpMessageConverter();
private static final OAuth2TokenType ACCESS_TOKEN_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.ACCESS_TOKEN);
private static RegisteredClientRepository registeredClientRepository;
private static OAuth2AuthorizationService authorizationService;
private static JWKSource<SecurityContext> jwkSource;
private static NimbusJwtDecoder jwtDecoder;
@Rule
public final SpringTestRule spring = new SpringTestRule();
@Autowired
private MockMvc mvc;
@BeforeClass
public static void init() {
registeredClientRepository = mock(RegisteredClientRepository.class);
authorizationService = mock(OAuth2AuthorizationService.class);
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
jwtDecoder = NimbusJwtDecoder.withPublicKey(TestKeys.DEFAULT_PUBLIC_KEY).build();
}
@Before
public void setup() {
reset(registeredClientRepository);
reset(authorizationService);
}
@Test
public void requestWhenAuthenticatedThenResponseIncludesRegisteredClientDetails() throws Exception {
this.spring.register(AuthorizationServerConfigurationEnabledClientRegistration.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient2()
.scope("client.create").build();
when(registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
.thenReturn(registeredClient);
// get access token
MvcResult mvcResult = this.mvc.perform(post(OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI)
.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.param(OAuth2ParameterNames.SCOPE, "client.create")
.header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth(
registeredClient.getClientId(), registeredClient.getClientSecret())))
.andExpect(status().isOk())
.andExpect(jsonPath("$.access_token").isNotEmpty())
.andExpect(jsonPath("$.scope").value("client.create"))
.andReturn();
//assert get access token
verify(registeredClientRepository).findByClientId(eq(registeredClient.getClientId()));
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
verify(authorizationService).save(authorizationCaptor.capture());
OAuth2Authorization authorization = authorizationCaptor.getValue();
MockHttpServletResponse servletResponse = mvcResult.getResponse();
MockClientHttpResponse httpResponse = new MockClientHttpResponse(
servletResponse.getContentAsByteArray(), HttpStatus.valueOf(servletResponse.getStatus()));
OAuth2AccessTokenResponse accessTokenResponse = accessTokenHttpResponseConverter.read(OAuth2AccessTokenResponse.class, httpResponse);
String tokenValue = accessTokenResponse.getAccessToken().getTokenValue();
// prepare register client request
when(authorizationService.findByToken(
eq(authorization.getToken(OAuth2AccessToken.class).getToken().getTokenValue()),
eq(ACCESS_TOKEN_TOKEN_TYPE)))
.thenReturn(authorization);
doNothing().when(registeredClientRepository).saveClient(any(RegisteredClient.class));
mvcResult = this.mvc.perform(post("/connect/register")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + tokenValue)
.contentType(MediaType.APPLICATION_JSON)
.content(convertToByteArray(OIDC_CLIENT_REGISTRATION.build())))
.andExpect(status().isCreated()).andReturn();
servletResponse = mvcResult.getResponse();
httpResponse = new MockClientHttpResponse(
servletResponse.getContentAsByteArray(), HttpStatus.valueOf(servletResponse.getStatus()));
OidcClientRegistration result = clientRegistrationHttpMessageConverter.read(OidcClientRegistration.class, httpResponse);
assertThat(result).isNotNull();
assertThat(result.getClaimAsString("client_id")).isNotEmpty();
assertThat(result.getClaimAsString("client_id_issued_at")).isNotEmpty();
assertThat(result.getClaimAsString("client_secret")).isNotEmpty();
assertThat(result.getClaimAsString("client_secret_expires_at")).isNotNull().isEqualTo("0.0");
assertThat(result.getRedirectUris()).isNotEmpty().containsExactly("https://localhost:8080/client");
assertThat(result.getResponseTypes()).isNotEmpty().containsExactly(OAuth2AuthorizationResponseType.CODE.getValue());
assertThat(result.getGrantTypes()).isNotEmpty().containsExactly(AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
assertThat(result.getTokenEndpointAuthenticationMethod()).isNotEmpty().isEqualTo(ClientAuthenticationMethod.BASIC.getValue());
assertThat(result.getScope()).isNotEmpty().isEqualTo("test");
}
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());
String credentialsString = clientId + ":" + secret;
byte[] encodedBytes = Base64.getEncoder().encode(credentialsString.getBytes(StandardCharsets.UTF_8));
return new String(encodedBytes, StandardCharsets.UTF_8);
}
private static byte[] convertToByteArray(OidcClientRegistration clientRegistration) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper
.writerFor(Map.class)
.writeValueAsBytes(clientRegistration.getClaims());
}
@EnableWebSecurity
@Import(OAuth2AuthorizationServerConfiguration.class)
static class AuthorizationServerConfiguration {
@Bean
RegisteredClientRepository registeredClientRepository() {
return registeredClientRepository;
}
@Bean
OAuth2AuthorizationService authorizationService() {
return authorizationService;
}
@Bean
JWKSource<SecurityContext> jwkSource() {
return jwkSource;
}
}
@EnableWebSecurity
@Import(OAuth2AuthorizationServerConfiguration.class)
static class AuthorizationServerConfigurationEnabledClientRegistration extends AuthorizationServerConfiguration{
@Bean
JwtDecoder jwtDecoder() {
return jwtDecoder;
}
@Bean
ProviderSettings providerSettings() {
return new ProviderSettings().isOidClientRegistrationEndpointEnabled(true);
}
}
}

View File

@@ -56,6 +56,7 @@ import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames
import org.springframework.security.oauth2.jose.TestJwks;
import org.springframework.security.oauth2.jose.TestKeys;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
@@ -273,6 +274,11 @@ public class OidcTests {
}
};
}
@Bean
JwtDecoder jwtDecoder(){
return jwtDecoder;
}
}
@EnableWebSecurity

View File

@@ -0,0 +1,331 @@
/*
* 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.core.oidc;
import org.junit.Test;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Tests for {@link OidcClientRegistration}
*
* @author Ovidiu Popa
* @since 0.1.1
*/
public class OidcClientRegistrationTests {
private final OidcClientRegistration.Builder clientRegistrationBuilder =
OidcClientRegistration.builder();
@Test
public void buildWhenAllRequiredClaimsAndAdditionalClaimsThenCreated() {
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
.redirectUri("http://client.example.com")
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.responseType(OAuth2AuthorizationResponseType.CODE.getValue())
.scope("test read")
.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.BASIC.getValue())
.build();
assertThat(clientRegistration.getRedirectUris())
.containsOnly("http://client.example.com");
assertThat(clientRegistration.getGrantTypes())
.contains(
AuthorizationGrantType.AUTHORIZATION_CODE.getValue(),
AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()
);
assertThat(clientRegistration.getResponseTypes())
.contains(OAuth2AuthorizationResponseType.CODE.getValue());
assertThat(clientRegistration.getScope())
.isEqualTo("test read");
assertThat(clientRegistration.getTokenEndpointAuthenticationMethod())
.isEqualTo(ClientAuthenticationMethod.BASIC.getValue());
}
@Test
public void buildWhenAllRequiredClaimsThenCreated() {
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
.redirectUri("http://client.example.com")
.build();
assertThat(clientRegistration.getRedirectUris())
.containsOnly("http://client.example.com");
assertThat(clientRegistration.getGrantTypes())
.containsOnly(AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
assertThat(clientRegistration.getResponseTypes())
.containsOnly(OAuth2AuthorizationResponseType.CODE.getValue());
assertThat(clientRegistration.getScope())
.isNull();
assertThat(clientRegistration.getTokenEndpointAuthenticationMethod())
.isEqualTo(ClientAuthenticationMethod.BASIC.getValue());
}
@Test
public void buildWhenAllRequiredClaimsAndAuthorizationGrantTypeButMissingResponseTypeThenCreated() {
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
.redirectUri("http://client.example.com")
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
.build();
assertThat(clientRegistration.getRedirectUris())
.containsOnly("http://client.example.com");
assertThat(clientRegistration.getGrantTypes())
.containsOnly(AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
assertThat(clientRegistration.getResponseTypes())
.containsOnly(OAuth2AuthorizationResponseType.CODE.getValue());
}
@Test
public void buildWhenAllRequiredClaimsAndEmptyGrantTypeListButMissingResponseTypeThenCreated() {
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
.redirectUri("http://client.example.com")
.grantTypes(List::clear)
.build();
assertThat(clientRegistration.getRedirectUris())
.containsOnly("http://client.example.com");
assertThat(clientRegistration.getGrantTypes())
.containsOnly(AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
assertThat(clientRegistration.getResponseTypes())
.containsOnly(OAuth2AuthorizationResponseType.CODE.getValue());
}
@Test
public void buildWhenAllRequiredClaimsAndResponseTypeButMissingAuthorizationGrantTypeThenCreated() {
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
.redirectUri("http://client.example.com")
.responseType(OAuth2AuthorizationResponseType.CODE.getValue())
.build();
assertThat(clientRegistration.getRedirectUris())
.containsOnly("http://client.example.com");
assertThat(clientRegistration.getGrantTypes())
.containsOnly(AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
assertThat(clientRegistration.getResponseTypes())
.containsOnly(OAuth2AuthorizationResponseType.CODE.getValue());
}
@Test
public void buildWhenAllRequiredClaimsAndEmptyResponseTypeListButMissingAuthorizationGrantTypeThenCreated() {
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
.redirectUri("http://client.example.com")
.responseTypes(List::clear)
.build();
assertThat(clientRegistration.getRedirectUris())
.containsOnly("http://client.example.com");
assertThat(clientRegistration.getGrantTypes())
.containsOnly(AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
assertThat(clientRegistration.getResponseTypes())
.containsOnly(OAuth2AuthorizationResponseType.CODE.getValue());
}
@Test
public void buildWhenAllRequiredClaimsAndEmptyScopeThenCreated() {
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
.redirectUri("http://client.example.com")
.build();
assertThat(clientRegistration.getRedirectUris())
.containsOnly("http://client.example.com");
assertThat(clientRegistration.getScope())
.isNull();
}
@Test
public void buildWhenAllRequiredClaimsAndEmptyTokenEndpointAuthMethodThenCreated() {
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
.redirectUri("http://client.example.com")
.build();
assertThat(clientRegistration.getRedirectUris())
.containsOnly("http://client.example.com");
assertThat(clientRegistration.getTokenEndpointAuthenticationMethod())
.isEqualTo(ClientAuthenticationMethod.BASIC.getValue());
}
@Test
public void buildWhenClaimsProvidedThenCreated() {
Map<String, Object> claims = new HashMap<>();
claims.put(OidcClientMetadataClaimNames.REDIRECT_URIS, Collections.singletonList("http://client.example.com"));
claims.put(OidcClientMetadataClaimNames.GRANT_TYPES, Arrays.asList(
AuthorizationGrantType.AUTHORIZATION_CODE.getValue(),
AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()
));
claims.put(OidcClientMetadataClaimNames.RESPONSE_TYPES,
Collections.singletonList(OAuth2AuthorizationResponseType.CODE.getValue()));
claims.put(OidcClientMetadataClaimNames.SCOPE, "test read");
claims.put(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD, ClientAuthenticationMethod.BASIC.getValue());
OidcClientRegistration clientRegistration = OidcClientRegistration.withClaims(claims).build();
assertThat(clientRegistration.getRedirectUris())
.containsOnly("http://client.example.com");
assertThat(clientRegistration.getGrantTypes())
.contains(
AuthorizationGrantType.AUTHORIZATION_CODE.getValue(),
AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()
);
assertThat(clientRegistration.getResponseTypes())
.contains(OAuth2AuthorizationResponseType.CODE.getValue());
assertThat(clientRegistration.getScope())
.isEqualTo("test read");
assertThat(clientRegistration.getTokenEndpointAuthenticationMethod())
.isEqualTo(ClientAuthenticationMethod.BASIC.getValue());
}
@Test
public void buildWhenRedirectUriProvidedWithUrlThenCreated() {
Map<String, Object> claims = new HashMap<>();
claims.put(OidcClientMetadataClaimNames.REDIRECT_URIS, Arrays.asList(
url("http://client.example.com"),
url("http://client.example.com/authorized")
)
);
claims.put(OidcClientMetadataClaimNames.GRANT_TYPES, Arrays.asList(
AuthorizationGrantType.AUTHORIZATION_CODE.getValue(),
AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()
));
claims.put(OidcClientMetadataClaimNames.RESPONSE_TYPES,
Collections.singletonList(OAuth2AuthorizationResponseType.CODE.getValue()));
claims.put(OidcClientMetadataClaimNames.SCOPE, "test read");
claims.put(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD, ClientAuthenticationMethod.BASIC.getValue());
OidcClientRegistration clientRegistration = OidcClientRegistration.withClaims(claims).build();
assertThat(clientRegistration.getRedirectUris())
.contains("http://client.example.com", "http://client.example.com/authorized");
assertThat(clientRegistration.getGrantTypes())
.contains(
AuthorizationGrantType.AUTHORIZATION_CODE.getValue(),
AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()
);
assertThat(clientRegistration.getResponseTypes())
.contains(OAuth2AuthorizationResponseType.CODE.getValue());
assertThat(clientRegistration.getScope())
.isEqualTo("test read");
assertThat(clientRegistration.getTokenEndpointAuthenticationMethod())
.isEqualTo(ClientAuthenticationMethod.BASIC.getValue());
}
@Test
public void withClaimsNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> OidcClientRegistration.withClaims(null))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
public void withClaimsEmptyThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> OidcClientRegistration.withClaims(Collections.emptyMap()))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("claims cannot be empty");
}
@Test
public void buildWhenNullRedirectUriThenThrowIllegalArgumentException() {
OidcClientRegistration.Builder builder = this.clientRegistrationBuilder
.redirectUris((claims) -> claims.remove(OidcClientMetadataClaimNames.REDIRECT_URIS));
assertThatThrownBy(builder::build)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("redirect_uris must not be empty");
}
@Test
public void buildWhenNullRedirectUriClaimThenThrowIllegalArgumentException() {
Map<String, Object> claims = new HashMap<>();
claims.put(OidcClientMetadataClaimNames.REDIRECT_URIS, null);
OidcClientRegistration.Builder builder = OidcClientRegistration.withClaims(claims);
assertThatThrownBy(builder::build)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("redirect_uris cannot be null");
}
@Test
public void buildWhenEmptyRedirectUriListThenThrowIllegalArgumentException() {
OidcClientRegistration.Builder builder = this.clientRegistrationBuilder
.redirectUris(List::clear);
assertThatThrownBy(builder::build)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("redirect_uris must not be empty");
}
@Test
public void buildWhenRedirectUriNotOfTypeListThenThrowIllegalArgumentException() {
OidcClientRegistration.Builder builder = this.clientRegistrationBuilder
.claims(claims -> claims.put(OidcClientMetadataClaimNames.REDIRECT_URIS, "http://client.example.com"));
assertThatThrownBy(builder::build)
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("redirect_uris must be of type list");
}
@Test
public void buildWhenRedirectUriNotUrlThenThrowIllegalArgumentException() {
OidcClientRegistration.Builder builder = this.clientRegistrationBuilder
.redirectUri("not url");
assertThatThrownBy(builder::build)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("redirect_uri must be a valid URL");
}
@Test
public void buildWhenResponseTypesNotOfTypeListThenThrowIllegalArgumentException() {
OidcClientRegistration.Builder builder = this.clientRegistrationBuilder
.redirectUri("http://client.example.com")
.claims(claims -> claims.put(OidcClientMetadataClaimNames.RESPONSE_TYPES, OAuth2AuthorizationResponseType.CODE.getValue()));
assertThatThrownBy(builder::build)
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("response_types must be of type List");
}
@Test
public void buildWhenGrantTypesNotOfTypeListThenThrowIllegalArgumentException() {
OidcClientRegistration.Builder builder = this.clientRegistrationBuilder
.redirectUri("http://client.example.com")
.claims(claims -> claims.put(OidcClientMetadataClaimNames.GRANT_TYPES, AuthorizationGrantType.AUTHORIZATION_CODE.getValue()));
assertThatThrownBy(builder::build)
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("grant_types must be of type List");
}
private static URL url(String urlString) {
try {
return new URL(urlString);
} catch (Exception ex) {
throw new IllegalArgumentException("urlString must be a valid URL and valid URI");
}
}
}

View File

@@ -0,0 +1,197 @@
/*
* 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.core.oidc.http.converter;
import org.junit.Test;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.mock.http.MockHttpOutputMessage;
import org.springframework.mock.http.client.MockClientHttpResponse;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
import org.springframework.security.oauth2.core.oidc.OidcClientRegistration;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* @author Ovidiu Popa
* @since 0.1.1
*/
public class OidcClientRegistrationHttpMessageConverterTest {
private final OidcClientRegistrationHttpMessageConverter messageConverter =
new OidcClientRegistrationHttpMessageConverter();
@Test
public void supportsWhenOidcClientRegistrationThenTrue() {
assertThat(this.messageConverter.supports(OidcClientRegistration.class)).isTrue();
}
@Test
public void setClientRegistrationReadConverterWhenNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.messageConverter.setClientRegistrationConverter(null))
.withMessageContaining("clientRegistrationConverter cannot be null");
}
@Test
public void setClientRegistrationWriteConverterWhenNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.messageConverter.setClientRegistrationParametersConverter(null))
.withMessageContaining("clientRegistrationParametersConverter cannot be null");
}
@Test
public void readInternalWhenRequiredParametersThenSuccess() {
// @formatter:off
String clientRegistrationResponse = "{\n"
+ " \"redirect_uris\": [\n"
+ " \"https://client.example.org/callback\"\n"
+ " ]\n"
+ "}\n";
// @formatter:on
MockClientHttpResponse response = new MockClientHttpResponse(clientRegistrationResponse.getBytes(), HttpStatus.OK);
OidcClientRegistration clientRegistration = this.messageConverter
.readInternal(OidcClientRegistration.class, response);
assertThat(clientRegistration.getRedirectUris())
.containsOnly("https://client.example.org/callback");
assertThat(clientRegistration.getGrantTypes())
.containsOnly(
AuthorizationGrantType.AUTHORIZATION_CODE.getValue()
);
assertThat(clientRegistration.getResponseTypes())
.contains(OAuth2AuthorizationResponseType.CODE.getValue());
assertThat(clientRegistration.getScope())
.isNull();
assertThat(clientRegistration.getTokenEndpointAuthenticationMethod())
.isEqualTo(ClientAuthenticationMethod.BASIC.getValue());
}
@Test
public void readInternalWhenValidParametersThenSuccess() {
// @formatter:off
String clientRegistrationResponse = "{\n"
+" \"redirect_uris\": [\n"
+ " \"https://client.example.org/callback\"\n"
+ " ],\n"
+" \"grant_types\": [\n"
+" \"client_credentials\",\n"
+" \"authorization_code\"\n"
+" ],\n"
+" \"response_types\":[\n"
+" \"code\"\n"
+" ],\n"
+" \"client_name\": \"My Example\",\n"
+" \"scope\": \"read write\",\n"
+" \"token_endpoint_auth_method\": \"basic\"\n"
+"}\n";
// @formatter:on
MockClientHttpResponse response = new MockClientHttpResponse(clientRegistrationResponse.getBytes(), HttpStatus.OK);
OidcClientRegistration clientRegistration = this.messageConverter
.readInternal(OidcClientRegistration.class, response);
assertThat(clientRegistration.getRedirectUris())
.containsOnly("https://client.example.org/callback");
assertThat(clientRegistration.getGrantTypes())
.contains(
AuthorizationGrantType.AUTHORIZATION_CODE.getValue(),
AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()
);
assertThat(clientRegistration.getResponseTypes())
.contains(OAuth2AuthorizationResponseType.CODE.getValue());
assertThat(clientRegistration.getScope())
.isEqualTo("read write");
assertThat(clientRegistration.getTokenEndpointAuthenticationMethod())
.isEqualTo(ClientAuthenticationMethod.BASIC.getValue());
}
@Test
public void readInternalWhenFailingConverterThenThrowException() {
String errorMessage = "this is not a valid converter";
this.messageConverter.setClientRegistrationConverter(source -> {
throw new RuntimeException(errorMessage);
});
MockClientHttpResponse response = new MockClientHttpResponse("{}".getBytes(), HttpStatus.OK);
assertThatExceptionOfType(HttpMessageNotReadableException.class)
.isThrownBy(() -> this.messageConverter.readInternal(OidcClientRegistration.class, response))
.withMessageContaining("An error occurred reading the OpenID Client Registration Request")
.withMessageContaining(errorMessage);
}
@Test
public void readInternalWhenInvalidClientRegistrationThenThrowException() {
String clientRegistrationResponse = "{ \"redirect_uris\": null }";
MockClientHttpResponse response = new MockClientHttpResponse(clientRegistrationResponse.getBytes(), HttpStatus.OK);
assertThatExceptionOfType(HttpMessageNotReadableException.class)
.isThrownBy(() -> this.messageConverter.readInternal(OidcClientRegistration.class, response))
.withMessageContaining("An error occurred reading the OpenID Client Registration Request")
.withMessageContaining("redirect_uris cannot be null");
}
@Test
public void writeInternalWhenClientRegistrationThenSuccess() {
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
.redirectUri("http://client.example.com/callback")
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.responseType(OAuth2AuthorizationResponseType.CODE.getValue())
.scope("test read")
.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.BASIC.getValue())
.build();
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
this.messageConverter.writeInternal(clientRegistration, outputMessage);
String clientRegistrationResponse = outputMessage.getBodyAsString();
assertThat(clientRegistrationResponse).contains("\"redirect_uris\":[\"http://client.example.com/callback\"]");
assertThat(clientRegistrationResponse).contains("\"grant_types\":[\"authorization_code\",\"client_credentials\"]");
assertThat(clientRegistrationResponse).contains("\"response_types\":[\"code\"]");
assertThat(clientRegistrationResponse).contains("\"scope\":\"test read\"");
assertThat(clientRegistrationResponse).contains("\"token_endpoint_auth_method\":\"basic\"");
}
@Test
public void writeInternalWhenWriteFailsThenThrowsException() {
String errorMessage = "this is not a valid converter";
Converter<OidcClientRegistration, Map<String, Object>> failingConverter =
source -> {
throw new RuntimeException(errorMessage);
};
this.messageConverter.setClientRegistrationParametersConverter(failingConverter);
OidcClientRegistration clientRegistration =
OidcClientRegistration.builder()
.redirectUri("http://client.example.com")
.build();
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
assertThatThrownBy(() -> this.messageConverter.writeInternal(clientRegistration, outputMessage))
.isInstanceOf(HttpMessageNotWritableException.class)
.hasMessageContaining("An error occurred writing the OpenID Client Registration response")
.hasMessageContaining(errorMessage);
}
}

View File

@@ -0,0 +1,173 @@
/*
* 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.authentication;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2TokenType;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import java.time.Instant;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* @author Ovidiu Popa
* @since 0.1.1
*/
public class OidcClientRegistrationAuthenticationProviderTests {
private OAuth2AuthorizationService authorizationService;
private OidcClientRegistrationAuthenticationProvider authenticationProvider;
@Before
public void setUp() {
this.authorizationService = mock(OAuth2AuthorizationService.class);
this.authenticationProvider = new OidcClientRegistrationAuthenticationProvider(this.authorizationService);
}
@Test
public void constructorWhenAuthorizationServiceNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new OidcClientRegistrationAuthenticationProvider(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("authorizationService cannot be null");
}
@Test
public void supportsWhenTypeJwtAuthenticationTokenThenReturnTrue() {
assertThat(this.authenticationProvider.supports(JwtAuthenticationToken.class)).isTrue();
}
@Test
public void authenticateWhenAccessTokenNotFoundThenThrowOAuth2AuthenticationException() {
JwtAuthenticationToken authentication = buildJwtAuthenticationToken("client-registration-token", "SCOPE_client.create");
when(authorizationService.findByToken(
eq("client-registration-token"), eq(OAuth2TokenType.ACCESS_TOKEN)))
.thenReturn(null);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.extracting("errorCode")
.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
}
@Test
public void authenticateWhenAccessTokenInvalidatedThenThrowOAuth2AuthenticationException() {
JwtAuthenticationToken authentication = buildJwtAuthenticationToken("client-registration-token", "SCOPE_client.create");
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
"client-registration-token", Instant.now().minusSeconds(120), Instant.now().plusSeconds(1000));
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization()
.token(accessToken, (metadata) -> metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true))
.build();
when(authorizationService.findByToken(
eq("client-registration-token"), eq(OAuth2TokenType.ACCESS_TOKEN)))
.thenReturn(authorization);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.extracting("errorCode")
.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
}
@Test
public void authenticateWhenAccessTokenWithoutClientCreateScopeThenThrowOAuth2AuthenticationException() {
JwtAuthenticationToken authentication = buildJwtAuthenticationToken("client-registration-token", "SCOPE_scope1");
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
"client-registration-token", Instant.now().minusSeconds(120), Instant.now().plusSeconds(1000),
new HashSet<>(Collections.singletonList("scope1")));
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization()
.token(accessToken)
.build();
when(authorizationService.findByToken(
eq("client-registration-token"), eq(OAuth2TokenType.ACCESS_TOKEN)))
.thenReturn(authorization);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.extracting("errorCode")
.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
}
@Test
public void authenticateWhenValidAccessTokenThenInvalidated() {
JwtAuthenticationToken authentication = buildJwtAuthenticationToken("client-registration-token", "SCOPE_client.create");
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
"client-registration-token", Instant.now().minusSeconds(120), Instant.now().plusSeconds(1000),
new HashSet<>(Collections.singletonList("client.create")));
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization()
.token(accessToken)
.build();
when(authorizationService.findByToken(
eq("client-registration-token"), eq(OAuth2TokenType.ACCESS_TOKEN)))
.thenReturn(authorization);
authenticationProvider.authenticate(authentication);
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
verify(authorizationService).save(authorizationCaptor.capture());
OAuth2Authorization capturedAuthorization = authorizationCaptor.getValue();
assertThat(capturedAuthorization.getAccessToken()).isNotNull();
assertThat(capturedAuthorization.getAccessToken().isInvalidated()).isTrue();
}
private static JwtAuthenticationToken buildJwtAuthenticationToken(String tokenValue, String... authorities) {
Jwt jwt = Jwt.withTokenValue(tokenValue)
.header("alg", "none")
.claim("sub", "client")
.build();
List<GrantedAuthority> grantedAuthorities = AuthorityUtils.createAuthorityList(authorities);
JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(jwt, grantedAuthorities);
jwtAuthenticationToken.setAuthenticated(true);
return jwtAuthenticationToken;
}
}

View File

@@ -16,6 +16,8 @@
package org.springframework.security.oauth2.server.authorization.client;
import org.junit.Test;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import java.util.Arrays;
import java.util.Collections;
@@ -112,4 +114,77 @@ public class InMemoryRegisteredClientRepositoryTests {
public void findByClientIdWhenNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> this.clients.findByClientId(null)).isInstanceOf(IllegalArgumentException.class);
}
@Test
public void saveNullRegisteredClientThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> this.clients.saveClient(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("registeredClient cannot be null");
}
@Test
public void saveRegisteredClientThenReturnsSavedRegisteredClientWhenSearchedById() {
RegisteredClient registeredClient = RegisteredClient.withId("new-client")
.clientId("new-client")
.clientSecret("secret")
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
.redirectUri("https://newclient.com")
.scope("scope1").build();
this.clients.saveClient(registeredClient);
RegisteredClient savedClient = this.clients.findById("new-client");
assertThat(savedClient).isNotNull().isEqualTo(registeredClient);
}
@Test
public void saveRegisteredClientThenReturnsSavedRegisteredClientWhenSearchedByClientId() {
RegisteredClient registeredClient = RegisteredClient.withId("id1")
.clientId("new-client-id")
.clientSecret("secret")
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
.redirectUri("https://newclient.com")
.scope("scope1").build();
this.clients.saveClient(registeredClient);
RegisteredClient savedClient = this.clients.findByClientId("new-client-id");
assertThat(savedClient).isNotNull().isEqualTo(registeredClient);
}
@Test
public void saveRegisteredClientWithExistingIdThrowIllegalArgumentException() {
assertThatThrownBy(() -> {
RegisteredClient registeredClient = RegisteredClient.withId("registration-1")
.clientId("new-client")
.clientSecret("secret")
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
.redirectUri("https://newclient.com")
.scope("scope1").build();
this.clients.saveClient(registeredClient);
}).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Registered client must be unique. Found duplicate identifier");
}
@Test
public void saveRegisteredClientWithExistingClientIdThrowIllegalArgumentException() {
assertThatThrownBy(() -> {
RegisteredClient registeredClient = RegisteredClient.withId("new-client")
.clientId("client-1")
.clientSecret("secret")
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
.redirectUri("https://newclient.com")
.scope("scope1").build();
this.clients.saveClient(registeredClient);
}).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Registered client must be unique. Found duplicate client identifier");
}
}

View File

@@ -37,6 +37,8 @@ public class ProviderSettingsTests {
assertThat(providerSettings.jwkSetEndpoint()).isEqualTo("/oauth2/jwks");
assertThat(providerSettings.tokenRevocationEndpoint()).isEqualTo("/oauth2/revoke");
assertThat(providerSettings.tokenIntrospectionEndpoint()).isEqualTo("/oauth2/introspect");
assertThat(providerSettings.oidcClientRegistrationEndpoint()).isEqualTo("/connect/register");
assertThat(providerSettings.isOidClientRegistrationEndpointEnabled()).isFalse();
}
@Test
@@ -47,6 +49,7 @@ public class ProviderSettingsTests {
String tokenRevocationEndpoint = "/oauth2/v1/revoke";
String tokenIntrospectionEndpoint = "/oauth2/v1/introspect";
String issuer = "https://example.com:9000";
String oidcClientRegistrationEndpoint = "/connect/v1/register";
ProviderSettings providerSettings = new ProviderSettings()
.issuer(issuer)
@@ -54,7 +57,10 @@ public class ProviderSettingsTests {
.tokenEndpoint(tokenEndpoint)
.jwkSetEndpoint(jwkSetEndpoint)
.tokenRevocationEndpoint(tokenRevocationEndpoint)
.tokenIntrospectionEndpoint(tokenIntrospectionEndpoint);
.tokenIntrospectionEndpoint(tokenIntrospectionEndpoint)
.tokenRevocationEndpoint(tokenRevocationEndpoint)
.isOidClientRegistrationEndpointEnabled(true)
.oidcClientRegistrationEndpoint(oidcClientRegistrationEndpoint);
assertThat(providerSettings.issuer()).isEqualTo(issuer);
assertThat(providerSettings.authorizationEndpoint()).isEqualTo(authorizationEndpoint);
@@ -62,6 +68,8 @@ public class ProviderSettingsTests {
assertThat(providerSettings.jwkSetEndpoint()).isEqualTo(jwkSetEndpoint);
assertThat(providerSettings.tokenRevocationEndpoint()).isEqualTo(tokenRevocationEndpoint);
assertThat(providerSettings.tokenIntrospectionEndpoint()).isEqualTo(tokenIntrospectionEndpoint);
assertThat(providerSettings.oidcClientRegistrationEndpoint()).isEqualTo(oidcClientRegistrationEndpoint);
assertThat(providerSettings.isOidClientRegistrationEndpointEnabled()).isTrue();
}
@Test
@@ -70,7 +78,7 @@ public class ProviderSettingsTests {
.setting("name1", "value1")
.settings(settings -> settings.put("name2", "value2"));
assertThat(providerSettings.settings()).hasSize(7);
assertThat(providerSettings.settings()).hasSize(9);
assertThat(providerSettings.<String>setting("name1")).isEqualTo("value1");
assertThat(providerSettings.<String>setting("name2")).isEqualTo("value2");
}
@@ -115,6 +123,15 @@ public class ProviderSettingsTests {
.withMessage("value cannot be null");
}
@Test
public void oidcClientRegistrationEndpointWhenNullThenThrowIllegalArgumentException() {
ProviderSettings settings = new ProviderSettings();
assertThatThrownBy(() -> settings.oidcClientRegistrationEndpoint(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("value cannot be null");
}
@Test
public void jwksEndpointWhenNullThenThrowIllegalArgumentException() {
ProviderSettings settings = new ProviderSettings();

View File

@@ -0,0 +1,286 @@
/*
* 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.web;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.AdditionalAnswers;
import org.mockito.ArgumentCaptor;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.mock.http.client.MockClientHttpResponse;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
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.OAuth2AuthorizationResponseType;
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
import org.springframework.security.oauth2.core.oidc.OidcClientMetadataClaimNames;
import org.springframework.security.oauth2.core.oidc.OidcClientRegistration;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
/**
* Tests for {@link OidcClientRegistrationEndpointFilter}
*
* @author Ovidiu Popa
* @since 0.1.1
*/
public class OidcClientRegistrationEndpointFilterTests {
private static final OidcClientRegistration.Builder OIDC_CLIENT_REGISTRATION = OidcClientRegistration.builder()
.redirectUri("https://localhost:8080/client")
.responseType("code")
.grantType("authorization_code")
.tokenEndpointAuthenticationMethod("basic")
.scope("test");
private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter =
new OAuth2ErrorHttpMessageConverter();
private static RegisteredClientRepository registeredClientRepository;
private static AuthenticationManager authenticationManager;
@BeforeClass
public static void init() {
registeredClientRepository = mock(RegisteredClientRepository.class);
authenticationManager = mock(AuthenticationManager.class);
}
@Before
public void setup() {
reset(registeredClientRepository);
reset(authenticationManager);
}
@After
public void tearDown() {
SecurityContextHolder.clearContext();
}
@Test
public void constructorWhenRegisteredClientRepositoryNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new OidcClientRegistrationEndpointFilter(null,
authenticationManager))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("registeredClientRepository cannot be null");
}
@Test
public void constructorWhenAuthenticationManagerNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new OidcClientRegistrationEndpointFilter(registeredClientRepository, null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("authenticationManager cannot be null");
}
@Test
public void constructorWhenOidcClientRegistrationUriNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new OidcClientRegistrationEndpointFilter(registeredClientRepository, authenticationManager, null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("oidcClientRegistrationUri cannot be empty");
}
@Test
public void constructorWhenOidcClientRegistrationUriEmptyThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new OidcClientRegistrationEndpointFilter(registeredClientRepository, authenticationManager, ""))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("oidcClientRegistrationUri cannot be empty");
}
@Test
public void doFilterWhenNotClientRegistrationRequestThenNotProcessed() throws Exception {
OidcClientRegistrationEndpointFilter filter =
new OidcClientRegistrationEndpointFilter(registeredClientRepository, authenticationManager);
String requestUri = "/path";
MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
request.setServletPath(requestUri);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
filter.doFilter(request, response, filterChain);
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
}
@Test
public void doFilterWhenClientRegistrationRequestGetThenNotProcessed() throws Exception {
OidcClientRegistrationEndpointFilter filter =
new OidcClientRegistrationEndpointFilter(registeredClientRepository, authenticationManager);
String requestUri = OidcClientRegistrationEndpointFilter.DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI;
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
filter.doFilter(request, response, filterChain);
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
}
@Test
public void doFilterWhenAuthenticationManagerThrowsOAuth2AuthenticationExceptionThenBadRequest() throws Exception {
setSecurityContext("client-registration-token", true, "SCOPE_client.create");
when(authenticationManager.authenticate(any(JwtAuthenticationToken.class)))
.thenThrow(new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT)));
OidcClientRegistrationEndpointFilter filter =
new OidcClientRegistrationEndpointFilter(registeredClientRepository, authenticationManager);
String requestUri = OidcClientRegistrationEndpointFilter.DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI;
MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
request.setServletPath(requestUri);
request.setContent(convertToByteArray(OIDC_CLIENT_REGISTRATION.build()));
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
filter.doFilter(request, response, filterChain);
verifyNoInteractions(filterChain);
assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
OAuth2Error error = readError(response);
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
}
@Test
@SuppressWarnings("unchecked")
public void doFilterWhenClientRegistrationRequestThenClientRegistrationResponse() throws Exception {
doNothing().when(registeredClientRepository).saveClient(any(RegisteredClient.class));
when(authenticationManager.authenticate(any(JwtAuthenticationToken.class))).then(AdditionalAnswers.returnsFirstArg());
setSecurityContext("client-registration-token", true, "SCOPE_client.create");
OidcClientRegistrationEndpointFilter filter =
new OidcClientRegistrationEndpointFilter(registeredClientRepository, authenticationManager);
String requestUri = OidcClientRegistrationEndpointFilter.DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI;
MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
request.setServletPath(requestUri);
request.setContent(convertToByteArray(OIDC_CLIENT_REGISTRATION.build()));
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
filter.doFilter(request, response, filterChain);
verifyNoInteractions(filterChain);
verify(authenticationManager).authenticate(any());
ArgumentCaptor<RegisteredClient> registeredClientCaptor = ArgumentCaptor.forClass(RegisteredClient.class);
verify(registeredClientRepository).saveClient(registeredClientCaptor.capture());
RegisteredClient registeredClient = registeredClientCaptor.getValue();
assertThat(response.getStatus()).isEqualTo(HttpStatus.CREATED.value());
assertThat(response.getContentType()).isEqualTo(MediaType.APPLICATION_JSON_VALUE);
ObjectMapper objectMapper = new ObjectMapper();
Map<String, Object> clientRegistrationResponse = objectMapper.readerFor(Map.class)
.readValue(response.getContentAsString());
assertThat(clientRegistrationResponse.get(OidcClientMetadataClaimNames.CLIENT_ID))
.isEqualTo(registeredClient.getClientId());
assertThat((String) clientRegistrationResponse.get(OidcClientMetadataClaimNames.CLIENT_SECRET))
.isEqualTo(registeredClient.getClientSecret());
assertThat((List<String>) clientRegistrationResponse.get(OidcClientMetadataClaimNames.REDIRECT_URIS))
.containsAll(registeredClient.getRedirectUris());
assertThat(clientRegistrationResponse.get(OidcClientMetadataClaimNames.CLIENT_ID_ISSUED_AT))
.isNotNull();
assertThat(clientRegistrationResponse.get(OidcClientMetadataClaimNames.CLIENT_SECRET_EXPIRES_AT))
.isEqualTo(0.0);
assertThat((List<String>) clientRegistrationResponse.get(OidcClientMetadataClaimNames.RESPONSE_TYPES))
.contains(OAuth2AuthorizationResponseType.CODE.getValue());
assertThat((List<String>) clientRegistrationResponse.get(OidcClientMetadataClaimNames.GRANT_TYPES))
.containsAll(grantTypes(registeredClient));
assertThat(clientRegistrationResponse.get(OidcClientMetadataClaimNames.SCOPE))
.isEqualTo(String.join(" ", registeredClient.getScopes()));
assertThat(clientRegistrationResponse.get(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD))
.isEqualTo(registeredClient.getClientAuthenticationMethods().iterator().next().getValue());
}
private List<String> grantTypes(RegisteredClient registeredClient) {
return registeredClient.getAuthorizationGrantTypes().stream()
.map(AuthorizationGrantType::getValue)
.collect(Collectors.toList());
}
private static void setSecurityContext(String tokenValue, boolean authenticated, String... authorities) {
Jwt jwt = Jwt.withTokenValue(tokenValue)
.header("alg", "none")
.claim("sub", "client")
.build();
List<GrantedAuthority> grantedAuthorities = AuthorityUtils.createAuthorityList(authorities);
JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(jwt, grantedAuthorities);
jwtAuthenticationToken.setAuthenticated(authenticated);
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(jwtAuthenticationToken);
SecurityContextHolder.setContext(securityContext);
}
private static byte[] convertToByteArray(OidcClientRegistration clientRegistration) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper
.writerFor(Map.class)
.writeValueAsBytes(clientRegistration.getClaims());
}
private OAuth2Error readError(MockHttpServletResponse response) throws Exception {
MockClientHttpResponse httpResponse = new MockClientHttpResponse(
response.getContentAsByteArray(), HttpStatus.valueOf(response.getStatus()));
return this.errorHttpResponseConverter.read(OAuth2Error.class, httpResponse);
}
}

View File

@@ -21,6 +21,11 @@ import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtValidators;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import sample.jose.Jwks;
import org.springframework.context.annotation.Bean;
@@ -85,4 +90,12 @@ public class AuthorizationServerConfig {
public ProviderSettings providerSettings() {
return new ProviderSettings().issuer("http://auth-server:9000");
}
@Bean
public JwtDecoder jwtDecoder(ProviderSettings providerSettings){
OAuth2TokenValidator<Jwt> jwtValidator = JwtValidators.createDefaultWithIssuer(providerSettings.issuer());
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri("http://auth-server:9000"+providerSettings.jwkSetEndpoint()).build();
jwtDecoder.setJwtValidator(jwtValidator);
return jwtDecoder;
}
}