Implement OAuth 2.0 Server Metadata (RFC 8414)

See See https://tools.ietf.org/html/rfc8414

Closes gh-54
This commit is contained in:
Daniel Garnier-Moiroux
2020-11-10 11:59:00 +01:00
committed by Daniel Garnier-Moiroux
parent a30a1692b2
commit 0a4775423b
24 changed files with 2123 additions and 417 deletions

View File

@@ -49,6 +49,7 @@ import org.springframework.security.oauth2.server.authorization.config.ProviderS
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;
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationServerConfigurationEndpointFilter;
import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientAuthenticationFilter;
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenIntrospectionEndpointFilter;
@@ -89,13 +90,15 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
private RequestMatcher tokenRevocationEndpointMatcher;
private RequestMatcher jwkSetEndpointMatcher;
private RequestMatcher oidcProviderConfigurationEndpointMatcher;
private RequestMatcher oauth2ServerConfigurationEndpointMatcher;
private final RequestMatcher endpointsMatcher = (request) ->
this.authorizationEndpointMatcher.matches(request) ||
this.tokenEndpointMatcher.matches(request) ||
this.tokenIntrospectionEndpointMatcher.matches(request) ||
this.tokenRevocationEndpointMatcher.matches(request) ||
this.jwkSetEndpointMatcher.matches(request) ||
this.oidcProviderConfigurationEndpointMatcher.matches(request);
this.oidcProviderConfigurationEndpointMatcher.matches(request) ||
this.oauth2ServerConfigurationEndpointMatcher.matches(request);
/**
* Sets the repository of registered clients.
@@ -214,6 +217,10 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
OidcProviderConfigurationEndpointFilter oidcProviderConfigurationEndpointFilter =
new OidcProviderConfigurationEndpointFilter(providerSettings);
builder.addFilterBefore(postProcess(oidcProviderConfigurationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
OAuth2AuthorizationServerConfigurationEndpointFilter authorizationServerConfigurationFilter
= new OAuth2AuthorizationServerConfigurationEndpointFilter(providerSettings);
builder.addFilterBefore(postProcess(authorizationServerConfigurationFilter), AbstractPreAuthenticatedProcessingFilter.class);
}
JWKSource<SecurityContext> jwkSource = getJwkSource(builder);
@@ -277,6 +284,8 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
providerSettings.jwkSetEndpoint(), HttpMethod.GET.name());
this.oidcProviderConfigurationEndpointMatcher = new AntPathRequestMatcher(
OidcProviderConfigurationEndpointFilter.DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI, HttpMethod.GET.name());
this.oauth2ServerConfigurationEndpointMatcher = new AntPathRequestMatcher(
OAuth2AuthorizationServerConfigurationEndpointFilter.DEFAULT_OAUTH2_AUTHORIZATION_SERVER_CONFIGURATION_ENDPOINT_URI, HttpMethod.GET.name());
}
private static void validateProviderSettings(ProviderSettings providerSettings) {

View File

@@ -0,0 +1,370 @@
/*
* Copyright 2020 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;
import org.springframework.util.Assert;
import java.io.Serializable;
import java.net.URI;
import java.net.URL;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
/**
* A base representation of a Provider Configuration response, returned by an endpoint defined
* either in OpenID Connect Discovery 1.0 or OAuth 2.0 Authorization Server Metadata.
* It contains a set of claims about the Provider's configuration.
*
* @author Daniel Garnier-Moiroux
* @see OAuth2AuthorizationServerMetadataClaimAccessor
* @since 0.1.1
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse">4.2. OpenID Provider Configuration Response</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc8414#section-3.2">3.2. Authorization Server Metadata Response</a>
*/
public abstract class AbstractOAuth2AuthorizationServerConfiguration implements OAuth2AuthorizationServerMetadataClaimAccessor, Serializable {
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
protected final Map<String, Object> claims;
protected AbstractOAuth2AuthorizationServerConfiguration(Map<String, Object> claims) {
Assert.notEmpty(claims, "claims cannot be empty");
this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims));
}
/**
* Returns the Authorization Server metadata.
*
* @return a {@code Map} of the metadata values
*/
@Override
public Map<String, Object> getClaims() {
return this.claims;
}
/**
* An abstract builder for subclasses of {@link AbstractOAuth2AuthorizationServerConfiguration}.
*/
protected static abstract class AbstractBuilder<T extends AbstractOAuth2AuthorizationServerConfiguration, B extends AbstractBuilder<T, B>> {
protected final Map<String, Object> claims = new LinkedHashMap<>();
protected AbstractBuilder() {
}
@SuppressWarnings("unchecked")
protected B getThis() { return (B) this; }; // avoid unchecked casts in subclasses by using "getThis()" instead of "(B) this"
/**
* Use this {@code issuer} in the resulting {@link AbstractOAuth2AuthorizationServerConfiguration}, REQUIRED.
*
* @param issuer the {@code URL} of the Authorization Server's Issuer Identifier
* @return the {@link AbstractBuilder} for further configuration
*/
public B issuer(String issuer) {
return claim(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, issuer);
}
/**
* Use this {@code authorization_endpoint} in the resulting {@link AbstractOAuth2AuthorizationServerConfiguration}, REQUIRED.
*
* @param authorizationEndpoint the {@code URL} of the Authorization Server's OAuth 2.0 Authorization Endpoint
* @return the {@link AbstractBuilder} for further configuration
*/
public B authorizationEndpoint(String authorizationEndpoint) {
return claim(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, authorizationEndpoint);
}
/**
* Use this {@code token_endpoint} in the resulting {@link AbstractOAuth2AuthorizationServerConfiguration}, REQUIRED.
*
* @param tokenEndpoint the {@code URL} of the Authorization Server's OAuth 2.0 Token Endpoint
* @return the {@link AbstractBuilder} for further configuration
*/
public B tokenEndpoint(String tokenEndpoint) {
return claim(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, tokenEndpoint);
}
/**
* Add this Authentication Method to the collection of {@code token_endpoint_auth_methods_supported}
* in the resulting {@link AbstractOAuth2AuthorizationServerConfiguration}, OPTIONAL.
*
* @param authenticationMethod the OAuth 2.0 Authentication Method supported by the Token Endpoint
* @return the {@link AbstractBuilder} for further configuration
*/
public B tokenEndpointAuthenticationMethod(String authenticationMethod) {
addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, authenticationMethod);
return getThis();
}
/**
* A {@code Consumer} of the Token Endpoint Authentication Method(s) allowing the ability to add, replace, or remove.
*
* @param authenticationMethodsConsumer a {@code Consumer} of the Token Endpoint Authentication Method(s)
* @return the {@link AbstractBuilder} for further configuration
*/
public B tokenEndpointAuthenticationMethods(Consumer<List<String>> authenticationMethodsConsumer) {
acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, authenticationMethodsConsumer);
return getThis();
}
/**
* Use this {@code jwks_uri} in the resulting {@link AbstractOAuth2AuthorizationServerConfiguration}, REQUIRED.
*
* @param jwkSetUri the {@code URL} of the Authorization Server's JSON Web Key Set document
* @return the {@link AbstractBuilder} for further configuration
*/
public B jwkSetUri(String jwkSetUri) {
return claim(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI, jwkSetUri);
}
/**
* Add this Response Type to the collection of {@code response_types_supported} in the resulting
* {@link AbstractOAuth2AuthorizationServerConfiguration}.
*
* @param responseType the OAuth 2.0 {@code response_type} value that the Authorization Server supports
* @return the {@link AbstractBuilder} for further configuration
*/
public B responseType(String responseType) {
addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, responseType);
return getThis();
}
/**
* A {@code Consumer} of the Response Type(s) allowing the ability to add, replace, or remove.
*
* @param responseTypesConsumer a {@code Consumer} of the Response Type(s)
* @return the {@link AbstractBuilder} for further configuration
*/
public B responseTypes(Consumer<List<String>> responseTypesConsumer) {
acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, responseTypesConsumer);
return getThis();
}
/**
* Add this Grant Type to the collection of {@code grant_types_supported} in the resulting
* {@link AbstractOAuth2AuthorizationServerConfiguration}, OPTIONAL.
*
* @param grantType the OAuth 2.0 {@code grant_type} value that the Authorization Server supports
* @return the {@link AbstractBuilder} for further configuration
*/
public B grantType(String grantType) {
addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, grantType);
return getThis();
}
/**
* 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 AbstractBuilder} for further configuration
*/
public B grantTypes(Consumer<List<String>> grantTypesConsumer) {
acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, grantTypesConsumer);
return getThis();
}
/**
* Add this Scope to the collection of {@code scopes_supported} in the resulting
* {@link AbstractOAuth2AuthorizationServerConfiguration}, RECOMMENDED.
*
* @param scope the OAuth 2.0 {@code scope} value that the Authorization Server supports
* @return the {@link AbstractBuilder} for further configuration
*/
public B scope(String scope) {
addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, scope);
return getThis();
}
/**
* A {@code Consumer} of the Scopes(s) allowing the ability to add, replace, or remove.
*
* @param scopesConsumer a {@code Consumer} of the Scopes(s)
* @return the {@link AbstractBuilder} for further configuration
*/
public B scopes(Consumer<List<String>> scopesConsumer) {
acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, scopesConsumer);
return getThis();
}
/**
* Use this claim in the resulting {@link AbstractOAuth2AuthorizationServerConfiguration}
*
* @param name the claim name
* @param value the claim value
* @return the {@link AbstractBuilder} for further configuration
*/
public B 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 getThis();
}
/**
* 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 AbstractBuilder} for further configurations
*/
public B claims(Consumer<Map<String, Object>> claimsConsumer) {
claimsConsumer.accept(this.claims);
return getThis();
}
/**
* Use this {@code revocation_endpoint} in the resulting {@link AbstractOAuth2AuthorizationServerConfiguration}, OPTIONAL.
*
* @param tokenRevocationEndpoint the {@code URL} of the OAuth 2.0 Authorization Server's Token Revocation Endpoint
* @return the {@link AbstractBuilder} for further configuration
*/
public B tokenRevocationEndpoint(String tokenRevocationEndpoint) {
return claim(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT, tokenRevocationEndpoint);
}
/**
* Add this Authentication Method to the collection of {@code revocation_endpoint_auth_methods_supported}
* in the resulting {@link AbstractOAuth2AuthorizationServerConfiguration}, OPTIONAL.
*
* @param authenticationMethod the OAuth 2.0 Authentication Method supported by the Revocation Endpoint
* @return the {@link AbstractBuilder} for further configuration
*/
public B tokenRevocationEndpointAuthenticationMethod(String authenticationMethod) {
addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED, authenticationMethod);
return getThis();
}
/**
* A {@code Consumer} of the Token Revocation Endpoint Authentication Method(s) allowing the ability to add,
* replace, or remove.
*
* @param authenticationMethodsConsumer a {@code Consumer} of the OAuth 2.0 Token Revocation Endpoint Authentication Method(s)
* @return the {@link AbstractBuilder} for further configuration
*/
public B tokenRevocationEndpointAuthenticationMethods(Consumer<List<String>> authenticationMethodsConsumer) {
acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED, authenticationMethodsConsumer);
return getThis();
}
/**
* Add this Proof Key for Code Exchange (PKCE) Code Challenge Method to the collection of
* {@code code_challenge_methods_supported} in the resulting {@link AbstractOAuth2AuthorizationServerConfiguration}, OPTIONAL.
*
* @param codeChallengeMethod the Proof Key for Code Exchange (PKCE) Code Challenge Method
* supported by the Authorization Server
* @return the {@link AbstractBuilder} for further configuration
*/
public B codeChallengeMethod(String codeChallengeMethod) {
addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED, codeChallengeMethod);
return getThis();
}
/**
* A {@code Consumer} of the Proof Key for Code Exchange (PKCE) Code Challenge Method(s) allowing
* the ability to add, replace, or remove.
*
* @param codeChallengeMethodsConsumer a {@code Consumer} of the Proof Key for Code Exchange (PKCE)
* Code Challenge Method(s)
* @return the {@link AbstractBuilder} for further configuration
*/
public B codeChallengeMethods(Consumer<List<String>> codeChallengeMethodsConsumer) {
acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED, codeChallengeMethodsConsumer);
return getThis();
}
/**
* Creates the {@link AbstractOAuth2AuthorizationServerConfiguration}.
*
* @return the {@link AbstractOAuth2AuthorizationServerConfiguration}
*/
public abstract T build();
protected void validateCommonClaims() {
Assert.notNull(this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.ISSUER), "issuer cannot be null");
validateURL(this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.ISSUER), "issuer must be a valid URL");
Assert.notNull(this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT), "authorizationEndpoint cannot be null");
validateURL(this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT), "authorizationEndpoint must be a valid URL");
Assert.notNull(this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT), "tokenEndpoint cannot be null");
validateURL(this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT), "tokenEndpoint must be a valid URL");
Assert.notNull(this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI), "jwksUri cannot be null");
validateURL(this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI), "jwksUri must be a valid URL");
Assert.notNull(this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED), "responseTypes cannot be null");
Assert.isInstanceOf(List.class, this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED), "responseTypes must be of type List");
Assert.notEmpty((List<?>) this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED), "responseTypes cannot be empty");
if (this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT) != null) {
validateURL(this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT), "tokenRevocationEndpoint must be a valid URL");
}
}
/**
* Remove claims of type Collection that have a size of zero.
* <p>
* Both <a target="_blank" href="https://tools.ietf.org/html/rfc8414#section-3.2">3.2. Authorization Server Metadata Response</a>
* and <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse">4.2. OpenID Provider Configuration Response</a>
* state "Claims with zero elements MUST be omitted from the response."
*/
protected void removeEmptyClaims() {
Set<String> claimsToRemove = this.claims.entrySet()
.stream()
.filter(entry -> entry.getValue() != null)
.filter(entry -> Collection.class.isAssignableFrom(entry.getValue().getClass()))
.filter(entry -> ((Collection<?>) entry.getValue()).size() == 0)
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
for (String claimToRemove : claimsToRemove) {
this.claims.remove(claimToRemove);
}
}
protected 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")
protected 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")
protected 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,134 @@
/*
* Copyright 2020 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;
import java.net.URL;
import java.util.List;
/**
* A base {@link ClaimAccessor} for the "claims" the Authorization Server can make about
* its configuration, used either in OpenID Connect Discovery 1.0 or OAuth 2.0 Authorization
* Server Metadata.
*
* @author Daniel Garnier-Moiroux
* @since 0.1.1
* @see ClaimAccessor
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc8414#section-2">2. Authorization Server Metadata</a>
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata">3. OpenID Provider Metadata</a>
*/
public interface OAuth2AuthorizationServerMetadataClaimAccessor extends ClaimAccessor {
/**
* Returns the {@code URL} the Authorization Server asserts as its Issuer Identifier {@code (issuer)}.
*
* @return the {@code URL} the Authorization Server asserts as its Issuer Identifier
*/
default URL getIssuer() {
return getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.ISSUER);
}
/**
* Returns the {@code URL} of the OAuth 2.0 Authorization Endpoint {@code (authorization_endpoint)}.
*
* @return the {@code URL} of the OAuth 2.0 Authorization Endpoint
*/
default URL getAuthorizationEndpoint() {
return getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT);
}
/**
* Returns the {@code URL} of the OAuth 2.0 Token Endpoint {@code (token_endpoint)}.
*
* @return the {@code URL} of the OAuth 2.0 Token Endpoint
*/
default URL getTokenEndpoint() {
return getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT);
}
/**
* Returns the client authentication methods supported by the OAuth 2.0 Token Endpoint {@code (token_endpoint_auth_methods_supported)}.
*
* @return the client authentication methods supported by the OAuth 2.0 Token Endpoint
*/
default List<String> getTokenEndpointAuthenticationMethods() {
return getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED);
}
/**
* Returns the {@code URL} of the JSON Web Key Set {@code (jwks_uri)}.
*
* @return the {@code URL} of the JSON Web Key Set
*/
default URL getJwkSetUri() {
return getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI);
}
/**
* Returns the OAuth 2.0 {@code response_type} values supported {@code (response_types_supported)}.
*
* @return the OAuth 2.0 {@code response_type} values supported
*/
default List<String> getResponseTypes() {
return getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED);
}
/**
* Returns the OAuth 2.0 {@code grant_type} values supported {@code (grant_types_supported)}.
*
* @return the OAuth 2.0 {@code grant_type} values supported
*/
default List<String> getGrantTypes() {
return getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED);
}
/**
* Returns the OAuth 2.0 {@code scope} values supported {@code (scopes_supported)}.
*
* @return the OAuth 2.0 {@code scope} values supported
*/
default List<String> getScopes() {
return this.getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED);
}
/**
* Returns the {@code URL} of the OAuth 2.0 Token Revocation Endpoint {@code (revocation_endpoint)}.
*
* @return the {@code URL} of the OAuth 2.0 Token Revocation Endpoint
*/
default URL getTokenRevocationEndpoint() {
return this.getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT);
}
/**
* Returns the client authentication methods supported by the OAuth 2.0 Token Revocation Endpoint {@code (revocation_endpoint_auth_methods_supported)}.
*
* @return the client authentication methods supported by the OAuth 2.0 Token Revocation Endpoint
*/
default List<String> getTokenRevocationEndpointAuthenticationMethods() {
return this.getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED);
}
/**
* Returns the Proof Key for Code Exchange (PKCE) code challenge methods supported by the
* OAuth 2.0 Authorization Server {@code (code_challenge_methods_supported)}.
*
* @return the code challenge methods supported by the OAuth 2.0 Authorization Server
*/
default List<String> getCodeChallengeMethods() {
return this.getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED);
}
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2020 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;
import org.springframework.security.oauth2.core.oidc.OidcProviderMetadataClaimNames;
/**
* The names of the "claims" an Authorization Server can make about its configuration,
* used either in OpenID Connect Discovery 1.0 or OAuth 2.0 Authorization Server Metadata.
*
* @author Daniel Garnier-Moiroux
* @since 0.1.1
* @see OidcProviderMetadataClaimNames
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc8414#section-2">2. Authorization Server Metadata</a>
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata">3. OpenID Provider Metadata</a>
*/
public interface OAuth2AuthorizationServerMetadataClaimNames {
/**
* {@code issuer} - the {@code URL} the Authorization Server asserts as its Issuer Identifier
*/
String ISSUER = "issuer";
/**
* {@code authorization_endpoint} - the {@code URL} of the OAuth 2.0 Authorization Endpoint
*/
String AUTHORIZATION_ENDPOINT = "authorization_endpoint";
/**
* {@code token_endpoint} - the {@code URL} of the OAuth 2.0 Token Endpoint
*/
String TOKEN_ENDPOINT = "token_endpoint";
/**
* {@code token_endpoint_auth_methods_supported} - the client authentication methods supported by the OAuth 2.0 Token Endpoint
*/
String TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED = "token_endpoint_auth_methods_supported";
/**
* {@code jwks_uri} - the {@code URL} of the JSON Web Key Set
*/
String JWKS_URI = "jwks_uri";
/**
* {@code response_types_supported} - the OAuth 2.0 {@code response_type} values supported
*/
String RESPONSE_TYPES_SUPPORTED = "response_types_supported";
/**
* {@code grant_types_supported} - the OAuth 2.0 {@code grant_type} values supported
*/
String GRANT_TYPES_SUPPORTED = "grant_types_supported";
/**
* {@code scopes_supported} - the OAuth 2.0 {@code scope} values supported
*/
String SCOPES_SUPPORTED = "scopes_supported";
/**
* {@code revocation_endpoint} - the {@code URL} of the OAuth 2.0 Token Revocation Endpoint
*/
String REVOCATION_ENDPOINT = "revocation_endpoint";
/**
* {@code token_endpoint_auth_methods_supported} - the client authentication methods supported by the OAuth 2.0 Token Revocation Endpoint
*/
String REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED = "revocation_endpoint_auth_methods_supported";
/**
* {@code code_challenge_methods_supported} - the Proof Key for Code Exchange (PKCE) code challenge methods
* supported by the OAuth 2.0 Authorization Server
*/
String CODE_CHALLENGE_METHODS_SUPPORTED = "code_challenge_methods_supported";
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2020 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.endpoint;
import org.springframework.security.oauth2.core.AbstractOAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.core.OAuth2AuthorizationServerMetadataClaimAccessor;
import org.springframework.security.oauth2.core.Version;
import org.springframework.util.Assert;
import java.io.Serializable;
import java.util.Map;
/**
* A representation of an OAuth 2.0 Authorization Server Configuration response,
* which is returned form an OAuth 2.0 Authorization Server's Configuration Endpoint,
* and contains a set of claims about the Authorization Server's configuration.
* The claims are defined by the OAuth 2.0 Authorization Server Metadata
* specification (RFC 8414).
*
* @author Daniel Garnier-Moiroux
* @since 0.1.1
* @see AbstractOAuth2AuthorizationServerConfiguration
* @see OAuth2AuthorizationServerMetadataClaimAccessor
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc8414#section-3.2">3.2. Authorization Server Metadata Response</a>
*/
public final class OAuth2AuthorizationServerConfiguration extends AbstractOAuth2AuthorizationServerConfiguration
implements OAuth2AuthorizationServerMetadataClaimAccessor, Serializable {
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
private OAuth2AuthorizationServerConfiguration(Map<String, Object> claims) {
super(claims);
}
/**
* Constructs a new {@link Builder} with empty claims.
*
* @return the {@link 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
* @return the {@link Builder}
*/
public static Builder withClaims(Map<String, Object> claims) {
Assert.notEmpty(claims, "claims cannot be empty");
return new Builder()
.claims(c -> c.putAll(claims));
}
/**
* Helps configure an {@link OAuth2AuthorizationServerConfiguration}.
*/
public static class Builder
extends AbstractOAuth2AuthorizationServerConfiguration.AbstractBuilder<OAuth2AuthorizationServerConfiguration, Builder> {
private Builder() {
}
/**
* Validate the claims and build the {@link OAuth2AuthorizationServerConfiguration}.
* <p>
* The following claims are REQUIRED:
* {@code issuer}, {@code authorization_endpoint}, {@code token_endpoint},
* {@code jwks_uri} and {@code response_types_supported}.
*
* @return the {@link OAuth2AuthorizationServerConfiguration}
*/
public OAuth2AuthorizationServerConfiguration build() {
validateCommonClaims();
removeEmptyClaims();
return new OAuth2AuthorizationServerConfiguration(this.claims);
}
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2020 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.endpoint;
import org.springframework.security.oauth2.core.Version;
import org.springframework.util.Assert;
import java.io.Serializable;
/**
* TODO
* This class is temporary and will be removed after upgrading to Spring Security 5.5.0 GA.
*
* The {@code code_challenge_method} is consumed by the authorization endpoint. The client sets
* the {@code code_challenge_method} parameter and a {@code code_challenge} derived from the
* {@code code_verifier} using the {@code code_challenge_method}.
*
* <p>
* The {@code code_challenge_method} parameter value may be one of &quot;S256&quot; or
* &quot;plain&quot;. If the client is capable of using &quot;S256&quot;, it MUST use
* &quot;S256&quot;, as it is mandatory to implement on the server.
*
* @author Daniel Garnier-Moiroux
* @since 0.1.1
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-6.2">6.2. PKCE Code Challenge Method Registry</a>
*/
public final class PkceCodeChallengeMethod2 implements Serializable {
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
public static final PkceCodeChallengeMethod2 S256 = new PkceCodeChallengeMethod2("S256");
public static final PkceCodeChallengeMethod2 PLAIN = new PkceCodeChallengeMethod2("plain");
private final String value;
private PkceCodeChallengeMethod2(String value) {
Assert.hasText(value, "value cannot be empty");
this.value = value;
}
/**
* Returns the value of the authorization response type.
*
* @return the value of the authorization response type
*/
public String getValue() {
return this.value;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || this.getClass() != obj.getClass()) {
return false;
}
PkceCodeChallengeMethod2 that = (PkceCodeChallengeMethod2) obj;
return this.getValue().equals(that.getValue());
}
@Override
public int hashCode() {
return this.getValue().hashCode();
}
}

View File

@@ -0,0 +1,162 @@
/*
* Copyright 2020 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.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.OAuth2AuthorizationServerMetadataClaimNames;
import org.springframework.security.oauth2.core.converter.ClaimConversionService;
import org.springframework.security.oauth2.core.converter.ClaimTypeConverter;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationServerConfiguration;
import org.springframework.util.Assert;
import java.net.URL;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
/**
* A {@link HttpMessageConverter} for an {@link OAuth2AuthorizationServerConfiguration OAuth 2.0 Authorization Server Configuration Response}.
*
* @author Daniel Garnier-Moiroux
* @since 0.1.1
* @see AbstractHttpMessageConverter
* @see OAuth2AuthorizationServerConfiguration
*/
public class OAuth2AuthorizationServerConfigurationHttpMessageConverter
extends AbstractHttpMessageConverter<OAuth2AuthorizationServerConfiguration> {
private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP =
new ParameterizedTypeReference<Map<String, Object>>() {};
private final GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter();
private Converter<Map<String, Object>, OAuth2AuthorizationServerConfiguration> authorizationServerConfigurationConverter = new OAuth2AuthorizationServerConfigurationConverter();
private Converter<OAuth2AuthorizationServerConfiguration, Map<String, Object>> authorizationServerConfigurationParametersConverter = OAuth2AuthorizationServerConfiguration::getClaims;
public OAuth2AuthorizationServerConfigurationHttpMessageConverter() {
super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
}
@Override
protected boolean supports(Class<?> clazz) {
return OAuth2AuthorizationServerConfiguration.class.isAssignableFrom(clazz);
}
@Override
@SuppressWarnings("unchecked")
protected OAuth2AuthorizationServerConfiguration readInternal(Class<? extends OAuth2AuthorizationServerConfiguration> clazz, HttpInputMessage inputMessage)
throws HttpMessageNotReadableException {
try {
Map<String, Object> serverConfigurationParameters =
(Map<String, Object>) this.jsonMessageConverter.read(STRING_OBJECT_MAP.getType(), null, inputMessage);
return this.authorizationServerConfigurationConverter.convert(serverConfigurationParameters);
} catch (Exception ex) {
throw new HttpMessageNotReadableException(
"An error occurred reading the OAuth 2.0 Authorization Server Configuration: " + ex.getMessage(), ex, inputMessage);
}
}
@Override
protected void writeInternal(OAuth2AuthorizationServerConfiguration providerConfiguration, HttpOutputMessage outputMessage)
throws HttpMessageNotWritableException {
try {
Map<String, Object> providerConfigurationResponseParameters =
this.authorizationServerConfigurationParametersConverter.convert(providerConfiguration);
this.jsonMessageConverter.write(
providerConfigurationResponseParameters,
STRING_OBJECT_MAP.getType(),
MediaType.APPLICATION_JSON,
outputMessage
);
} catch (Exception ex) {
throw new HttpMessageNotWritableException(
"An error occurred writing the OAuth 2.0 Authorization Server Configuration: " + ex.getMessage(), ex);
}
}
/**
* Sets the {@link Converter} used for converting the OAuth 2.0 Authorization Server Configuration
* parameters to an {@link OAuth2AuthorizationServerConfiguration}.
*
* @param authorizationServerConfigurationConverter the {@link Converter} used for converting to
* an {@link OAuth2AuthorizationServerConfiguration}.
*/
public void setAuthorizationServerConfigurationConverter(Converter<Map<String, Object>, OAuth2AuthorizationServerConfiguration> authorizationServerConfigurationConverter) {
Assert.notNull(authorizationServerConfigurationConverter, "authorizationServerConfigurationConverter cannot be null");
this.authorizationServerConfigurationConverter = authorizationServerConfigurationConverter;
}
/**
* Sets the {@link Converter} used for converting the {@link OAuth2AuthorizationServerConfiguration} to a
* {@code Map} representation of the OAuth 2.0 Authorization Server Configuration.
*
* @param authorizationServerConfigurationParametersConverter the {@link Converter} used for converting to a
* {@code Map} representation of the OAuth 2.0 Authorization Server Configuration.
*/
public void setAuthorizationServerConfigurationParametersConverter(Converter<OAuth2AuthorizationServerConfiguration, Map<String, Object>> authorizationServerConfigurationParametersConverter) {
Assert.notNull(authorizationServerConfigurationParametersConverter, "authorizationServerConfigurationParametersConverter cannot be null");
this.authorizationServerConfigurationParametersConverter = authorizationServerConfigurationParametersConverter;
}
private static final class OAuth2AuthorizationServerConfigurationConverter implements Converter<Map<String, Object>, OAuth2AuthorizationServerConfiguration> {
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 static final TypeDescriptor URL_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(URL.class);
private final ClaimTypeConverter claimTypeConverter;
private OAuth2AuthorizationServerConfigurationConverter() {
Map<String, Converter<Object, ?>> claimNameToConverter = new HashMap<>();
Converter<Object, ?> collectionStringConverter = getConverter(
TypeDescriptor.collection(Collection.class, STRING_TYPE_DESCRIPTOR));
Converter<Object, ?> urlConverter = getConverter(URL_TYPE_DESCRIPTOR);
claimNameToConverter.put(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, urlConverter);
claimNameToConverter.put(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, urlConverter);
claimNameToConverter.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, urlConverter);
claimNameToConverter.put(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT, urlConverter);
claimNameToConverter.put(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI, urlConverter);
claimNameToConverter.put(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, collectionStringConverter);
claimNameToConverter.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, collectionStringConverter);
claimNameToConverter.put(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED, collectionStringConverter);
claimNameToConverter.put(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, collectionStringConverter);
claimNameToConverter.put(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, collectionStringConverter);
claimNameToConverter.put(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED, collectionStringConverter);
this.claimTypeConverter = new ClaimTypeConverter(claimNameToConverter);
}
@Override
public OAuth2AuthorizationServerConfiguration convert(Map<String, Object> source) {
Map<String, Object> parsedClaims = this.claimTypeConverter.convert(source);
return OAuth2AuthorizationServerConfiguration.withClaims(parsedClaims).build();
}
private static Converter<Object, ?> getConverter(TypeDescriptor targetDescriptor) {
return (source) -> CLAIM_CONVERSION_SERVICE.convert(source, OBJECT_TYPE_DESCRIPTOR, targetDescriptor);
}
}
}

View File

@@ -15,16 +15,12 @@
*/
package org.springframework.security.oauth2.core.oidc;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
import org.springframework.security.oauth2.core.AbstractOAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.core.Version;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
import org.springframework.util.Assert;
import java.io.Serializable;
import java.net.URI;
import java.net.URL;
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;
@@ -38,24 +34,15 @@ import java.util.function.Consumer;
* @author Daniel Garnier-Moiroux
* @since 0.1.0
* @see OidcProviderMetadataClaimAccessor
* @see AbstractOAuth2AuthorizationServerConfiguration
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse">4.2. OpenID Provider Configuration Response</a>
*/
public final class OidcProviderConfiguration implements OidcProviderMetadataClaimAccessor, Serializable {
public final class OidcProviderConfiguration extends AbstractOAuth2AuthorizationServerConfiguration
implements OidcProviderMetadataClaimAccessor, Serializable {
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
private final Map<String, Object> claims;
private OidcProviderConfiguration(Map<String, Object> claims) {
this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims));
}
/**
* Returns the OpenID Provider Configuration metadata.
*
* @return a {@code Map} of the metadata values
*/
@Override
public Map<String, Object> getClaims() {
return this.claims;
super(claims);
}
/**
@@ -81,121 +68,10 @@ public final class OidcProviderConfiguration implements OidcProviderMetadataClai
/**
* Helps configure an {@link OidcProviderConfiguration}
*/
public static class Builder {
private final Map<String, Object> claims = new LinkedHashMap<>();
public static class Builder extends AbstractOAuth2AuthorizationServerConfiguration.AbstractBuilder<OidcProviderConfiguration, Builder> {
private Builder() {
}
/**
* Use this {@code issuer} in the resulting {@link OidcProviderConfiguration}, REQUIRED.
*
* @param issuer the URL of the OpenID Provider's Issuer Identifier
* @return the {@link Builder} for further configuration
*/
public Builder issuer(String issuer) {
return claim(OidcProviderMetadataClaimNames.ISSUER, issuer);
}
/**
* Use this {@code authorization_endpoint} in the resulting {@link OidcProviderConfiguration}, REQUIRED.
*
* @param authorizationEndpoint the URL of the OpenID Provider's OAuth 2.0 Authorization Endpoint
* @return the {@link Builder} for further configuration
*/
public Builder authorizationEndpoint(String authorizationEndpoint) {
return claim(OidcProviderMetadataClaimNames.AUTHORIZATION_ENDPOINT, authorizationEndpoint);
}
/**
* Use this {@code token_endpoint} in the resulting {@link OidcProviderConfiguration}, REQUIRED.
*
* @param tokenEndpoint the URL of the OpenID Provider's OAuth 2.0 Token Endpoint
* @return the {@link Builder} for further configuration
*/
public Builder tokenEndpoint(String tokenEndpoint) {
return claim(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT, tokenEndpoint);
}
/**
* Add this Authentication Method to the collection of {@code token_endpoint_auth_methods_supported}
* in the resulting {@link OidcProviderConfiguration}, OPTIONAL.
*
* @param authenticationMethod the OAuth 2.0 Authentication Method supported by the Token endpoint
* @return the {@link Builder} for further configuration
*/
public Builder tokenEndpointAuthenticationMethod(String authenticationMethod) {
addClaimToClaimList(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, authenticationMethod);
return this;
}
/**
* A {@code Consumer} of the Token Endpoint Authentication Method(s) allowing the ability to add, replace, or remove.
*
* @param authenticationMethodsConsumer a {@code Consumer} of the Token Endpoint Authentication Method(s)
* @return the {@link Builder} for further configuration
*/
public Builder tokenEndpointAuthenticationMethods(Consumer<List<String>> authenticationMethodsConsumer) {
acceptClaimValues(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, authenticationMethodsConsumer);
return this;
}
/**
* Use this {@code jwks_uri} in the resulting {@link OidcProviderConfiguration}, REQUIRED.
*
* @param jwkSetUri the URL of the OpenID Provider's JSON Web Key Set document
* @return the {@link Builder} for further configuration
*/
public Builder jwkSetUri(String jwkSetUri) {
return claim(OidcProviderMetadataClaimNames.JWKS_URI, jwkSetUri);
}
/**
* Add this Response Type to the collection of {@code response_types_supported} in the resulting
* {@link OidcProviderConfiguration}, REQUIRED.
*
* @param responseType the OAuth 2.0 {@code response_type} value that the OpenID Provider supports
* @return the {@link Builder} for further configuration
*/
public Builder responseType(String responseType) {
addClaimToClaimList(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, responseType);
return this;
}
/**
* A {@code Consumer} of the Response Type(s) allowing the ability to add, replace, or remove.
*
* @param responseTypesConsumer a {@code Consumer} of the Response Type(s)
* @return the {@link Builder} for further configuration
*/
public Builder responseTypes(Consumer<List<String>> responseTypesConsumer) {
acceptClaimValues(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, responseTypesConsumer);
return this;
}
/**
* Add this Grant Type to the collection of {@code grant_types_supported} in the resulting
* {@link OidcProviderConfiguration}, OPTIONAL.
*
* @param grantType the OAuth 2.0 {@code grant_type} value that the OpenID Provider supports
* @return the {@link Builder} for further configuration
*/
public Builder grantType(String grantType) {
addClaimToClaimList(OidcProviderMetadataClaimNames.GRANT_TYPES_SUPPORTED, 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(OidcProviderMetadataClaimNames.GRANT_TYPES_SUPPORTED, grantTypesConsumer);
return this;
}
/**
* Add this Subject Type to the collection of {@code subject_types_supported} in the resulting
* {@link OidcProviderConfiguration}, REQUIRED.
@@ -219,29 +95,6 @@ public final class OidcProviderConfiguration implements OidcProviderMetadataClai
return this;
}
/**
* Add this Scope to the collection of {@code scopes_supported} in the resulting
* {@link OidcProviderConfiguration}, RECOMMENDED.
*
* @param scope the OAuth 2.0 {@code scope} value that the OpenID Provider supports
* @return the {@link Builder} for further configuration
*/
public Builder scope(String scope) {
addClaimToClaimList(OidcProviderMetadataClaimNames.SCOPES_SUPPORTED, scope);
return this;
}
/**
* A {@code Consumer} of the Scopes(s) allowing the ability to add, replace, or remove.
*
* @param scopesConsumer a {@code Consumer} of the Scopes(s)
* @return the {@link Builder} for further configuration
*/
public Builder scopes(Consumer<List<String>> scopesConsumer) {
acceptClaimValues(OidcProviderMetadataClaimNames.SCOPES_SUPPORTED, scopesConsumer);
return this;
}
/**
* Add this {@link JwsAlgorithm JWS} signing algorithm to the collection of {@code id_token_signing_alg_values_supported}
* in the resulting {@link OidcProviderConfiguration}, REQUIRED.
@@ -297,27 +150,20 @@ public final class OidcProviderConfiguration implements OidcProviderMetadataClai
* <p>
* The following claims are REQUIRED:
* {@code issuer}, {@code authorization_endpoint}, {@code token_endpoint}, {@code jwks_uri},
* {@code response_types_supported} and {@code subject_types_supported}.
* {@code response_types_supported}, {@code subject_types_supported} and
* {@code id_token_signing_alg_values_supported}.
*
* @return the {@link OidcProviderConfiguration}
*/
@Override
public OidcProviderConfiguration build() {
validateClaims();
validateCommonClaims();
validateOidcSpecificClaims();
removeEmptyClaims();
return new OidcProviderConfiguration(this.claims);
}
private void validateClaims() {
Assert.notNull(this.claims.get(OidcProviderMetadataClaimNames.ISSUER), "issuer cannot be null");
validateURL(this.claims.get(OidcProviderMetadataClaimNames.ISSUER), "issuer must be a valid URL");
Assert.notNull(this.claims.get(OidcProviderMetadataClaimNames.AUTHORIZATION_ENDPOINT), "authorizationEndpoint cannot be null");
validateURL(this.claims.get(OidcProviderMetadataClaimNames.AUTHORIZATION_ENDPOINT), "authorizationEndpoint must be a valid URL");
Assert.notNull(this.claims.get(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT), "tokenEndpoint cannot be null");
validateURL(this.claims.get(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT), "tokenEndpoint must be a valid URL");
Assert.notNull(this.claims.get(OidcProviderMetadataClaimNames.JWKS_URI), "jwksUri cannot be null");
validateURL(this.claims.get(OidcProviderMetadataClaimNames.JWKS_URI), "jwksUri must be a valid URL");
Assert.notNull(this.claims.get(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED), "responseTypes cannot be null");
Assert.isInstanceOf(List.class, this.claims.get(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED), "responseTypes must be of type List");
Assert.notEmpty((List<?>) this.claims.get(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED), "responseTypes cannot be empty");
private void validateOidcSpecificClaims() {
Assert.notNull(this.claims.get(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED), "subjectTypes cannot be null");
Assert.isInstanceOf(List.class, this.claims.get(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED), "subjectTypes must be of type List");
Assert.notEmpty((List<?>) this.claims.get(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED), "subjectTypes cannot be empty");
@@ -325,34 +171,5 @@ public final class OidcProviderConfiguration implements OidcProviderMetadataClai
Assert.isInstanceOf(List.class, this.claims.get(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED), "idTokenSigningAlgorithms must be of type List");
Assert.notEmpty((List<?>) this.claims.get(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED), "idTokenSigningAlgorithms cannot be empty");
}
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

@@ -16,11 +16,12 @@
package org.springframework.security.oauth2.core.oidc;
import org.springframework.security.oauth2.core.OAuth2AuthorizationServerMetadataClaimAccessor;
import org.springframework.security.oauth2.core.ClaimAccessor;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
import java.net.URL;
import java.util.List;
/**
@@ -30,74 +31,12 @@ import java.util.List;
* @author Daniel Garnier-Moiroux
* @since 0.1.0
* @see ClaimAccessor
* @see OAuth2AuthorizationServerMetadataClaimAccessor
* @see OidcProviderMetadataClaimNames
* @see OidcProviderConfiguration
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata">3. OpenID Provider Metadata</a>
*/
public interface OidcProviderMetadataClaimAccessor extends ClaimAccessor {
/**
* Returns the {@code URL} the OpenID Provider asserts as its Issuer Identifier {@code (issuer)}.
*
* @return the {@code URL} the OpenID Provider asserts as its Issuer Identifier
*/
default URL getIssuer() {
return getClaimAsURL(OidcProviderMetadataClaimNames.ISSUER);
}
/**
* Returns the {@code URL} of the OAuth 2.0 Authorization Endpoint {@code (authorization_endpoint)}.
*
* @return the {@code URL} of the OAuth 2.0 Authorization Endpoint
*/
default URL getAuthorizationEndpoint() {
return getClaimAsURL(OidcProviderMetadataClaimNames.AUTHORIZATION_ENDPOINT);
}
/**
* Returns the {@code URL} of the OAuth 2.0 Token Endpoint {@code (token_endpoint)}.
*
* @return the {@code URL} of the OAuth 2.0 Token Endpoint
*/
default URL getTokenEndpoint() {
return getClaimAsURL(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT);
}
/**
* Returns the client authentication methods supported by the OAuth 2.0 Token Endpoint {@code (token_endpoint_auth_methods_supported)}.
*
* @return the client authentication methods supported by the OAuth 2.0 Token Endpoint
*/
default List<String> getTokenEndpointAuthenticationMethods() {
return getClaimAsStringList(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED);
}
/**
* Returns the {@code URL} of the JSON Web Key Set {@code (jwks_uri)}.
*
* @return the {@code URL} of the JSON Web Key Set
*/
default URL getJwkSetUri() {
return getClaimAsURL(OidcProviderMetadataClaimNames.JWKS_URI);
}
/**
* Returns the OAuth 2.0 {@code response_type} values supported {@code (response_types_supported)}.
*
* @return the OAuth 2.0 {@code response_type} values supported
*/
default List<String> getResponseTypes() {
return getClaimAsStringList(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED);
}
/**
* Returns the OAuth 2.0 {@code grant_type} values supported {@code (grant_types_supported)}.
*
* @return the OAuth 2.0 {@code grant_type} values supported
*/
default List<String> getGrantTypes() {
return getClaimAsStringList(OidcProviderMetadataClaimNames.GRANT_TYPES_SUPPORTED);
}
public interface OidcProviderMetadataClaimAccessor extends OAuth2AuthorizationServerMetadataClaimAccessor {
/**
* Returns the Subject Identifier types supported {@code (subject_types_supported)}.
@@ -108,15 +47,6 @@ public interface OidcProviderMetadataClaimAccessor extends ClaimAccessor {
return getClaimAsStringList(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED);
}
/**
* Returns the OAuth 2.0 {@code scope} values supported {@code (scopes_supported)}.
*
* @return the OAuth 2.0 {@code scope} values supported
*/
default List<String> getScopes() {
return getClaimAsStringList(OidcProviderMetadataClaimNames.SCOPES_SUPPORTED);
}
/**
* Returns the {@link JwsAlgorithm JWS} signing algorithms supported for the {@link OidcIdToken ID Token}
* to encode the claims in a {@link Jwt} {@code (id_token_signing_alg_values_supported)}.

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.security.oauth2.core.oidc;
import org.springframework.security.oauth2.core.OAuth2AuthorizationServerMetadataClaimNames;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
/**
@@ -25,53 +26,13 @@ import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
* @since 0.1.0
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata">3. OpenID Provider Metadata</a>
*/
public interface OidcProviderMetadataClaimNames {
/**
* {@code issuer} - the {@code URL} the OpenID Provider asserts as its Issuer Identifier
*/
String ISSUER = "issuer";
/**
* {@code authorization_endpoint} - the {@code URL} of the OAuth 2.0 Authorization Endpoint
*/
String AUTHORIZATION_ENDPOINT = "authorization_endpoint";
/**
* {@code token_endpoint} - the {@code URL} of the OAuth 2.0 Token Endpoint
*/
String TOKEN_ENDPOINT = "token_endpoint";
/**
* {@code token_endpoint_auth_methods_supported} - the client authentication methods supported by the OAuth 2.0 Token Endpoint
*/
String TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED = "token_endpoint_auth_methods_supported";
/**
* {@code jwks_uri} - the {@code URL} of the JSON Web Key Set
*/
String JWKS_URI = "jwks_uri";
/**
* {@code response_types_supported} - the OAuth 2.0 {@code response_type} values supported
*/
String RESPONSE_TYPES_SUPPORTED = "response_types_supported";
/**
* {@code grant_types_supported} - the OAuth 2.0 {@code grant_type} values supported
*/
String GRANT_TYPES_SUPPORTED = "grant_types_supported";
public interface OidcProviderMetadataClaimNames extends OAuth2AuthorizationServerMetadataClaimNames {
/**
* {@code subject_types_supported} - the Subject Identifier types supported
*/
String SUBJECT_TYPES_SUPPORTED = "subject_types_supported";
/**
* {@code scopes_supported} - the OAuth 2.0 {@code scope} values supported
*/
String SCOPES_SUPPORTED = "scopes_supported";
/**
* {@code id_token_signing_alg_values_supported} - the {@link JwsAlgorithm JWS} signing algorithms supported for the {@link OidcIdToken ID Token}
*/

View File

@@ -31,6 +31,7 @@ import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2TokenType;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.PkceCodeChallengeMethod2;
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
@@ -156,9 +157,9 @@ public class OAuth2ClientAuthenticationProvider implements AuthenticationProvide
private static boolean codeVerifierValid(String codeVerifier, String codeChallenge, String codeChallengeMethod) {
if (!StringUtils.hasText(codeVerifier)) {
return false;
} else if (!StringUtils.hasText(codeChallengeMethod) || "plain".equals(codeChallengeMethod)) {
} else if (!StringUtils.hasText(codeChallengeMethod) || PkceCodeChallengeMethod2.PLAIN.getValue().equals(codeChallengeMethod)) {
return codeVerifier.equals(codeChallenge);
} else if ("S256".equals(codeChallengeMethod)) {
} else if (PkceCodeChallengeMethod2.S256.getValue().equals(codeChallengeMethod)) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));

View File

@@ -70,7 +70,8 @@ public class ProviderSettings extends Settings {
}
/**
* Returns the Provider's OAuth 2.0 Authorization endpoint. The default is {@code /oauth2/authorize}.
* Returns the Provider's OAuth 2.0 Authorization endpoint.
* The default is {@code /oauth2/authorize}.
*
* @return the Authorization endpoint
*/
@@ -89,7 +90,8 @@ public class ProviderSettings extends Settings {
}
/**
* Returns the Provider's OAuth 2.0 Token endpoint. The default is {@code /oauth2/token}.
* Returns the Provider's OAuth 2.0 Token endpoint.
* The default is {@code /oauth2/token}.
*
* @return the Token endpoint
*/

View File

@@ -75,20 +75,21 @@ public class OidcProviderConfigurationEndpointFilter extends OncePerRequestFilte
return;
}
OidcProviderConfiguration providerConfiguration = OidcProviderConfiguration.builder()
OidcProviderConfiguration providerConfiguration = OidcProviderConfiguration
.builder()
.issuer(this.providerSettings.issuer())
.authorizationEndpoint(asUrl(this.providerSettings.issuer(), this.providerSettings.authorizationEndpoint()))
.tokenEndpoint(asUrl(this.providerSettings.issuer(), this.providerSettings.tokenEndpoint()))
.tokenEndpointAuthenticationMethod("client_secret_basic") // TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_BASIC in Spring Security 5.5.0
.tokenEndpointAuthenticationMethod("client_secret_post") // TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_POST in Spring Security 5.5.0
.tokenEndpointAuthenticationMethod("client_secret_basic") // TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_BASIC in Spring Security 5.5.0
.tokenEndpointAuthenticationMethod("client_secret_post") // TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_POST in Spring Security 5.5.0
.jwkSetUri(asUrl(this.providerSettings.issuer(), this.providerSettings.jwkSetEndpoint()))
.responseType(OAuth2AuthorizationResponseType.CODE.getValue())
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.grantType(AuthorizationGrantType.REFRESH_TOKEN.getValue())
.responseType(OAuth2AuthorizationResponseType.CODE.getValue())
.scope(OidcScopes.OPENID)
.subjectType("public")
.idTokenSigningAlgorithm(SignatureAlgorithm.RS256.getName())
.scope(OidcScopes.OPENID)
.build();
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);

View File

@@ -48,6 +48,7 @@ import org.springframework.security.oauth2.core.OAuth2TokenType;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.PkceCodeChallengeMethod2;
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
@@ -370,7 +371,7 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {
String codeChallengeMethod = authorizationRequestContext.getParameters().getFirst(PkceParameterNames.CODE_CHALLENGE_METHOD);
if (StringUtils.hasText(codeChallengeMethod)) {
if (authorizationRequestContext.getParameters().get(PkceParameterNames.CODE_CHALLENGE_METHOD).size() != 1 ||
(!"S256".equals(codeChallengeMethod) && !"plain".equals(codeChallengeMethod))) {
(!PkceCodeChallengeMethod2.S256.getValue().equals(codeChallengeMethod) && !PkceCodeChallengeMethod2.PLAIN.getValue().equals(codeChallengeMethod))) {
authorizationRequestContext.setError(
createError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, PKCE_ERROR_URI));
return;

View File

@@ -0,0 +1,104 @@
/*
* Copyright 2020 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.web;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.core.endpoint.PkceCodeChallengeMethod2;
import org.springframework.security.oauth2.core.http.converter.OAuth2AuthorizationServerConfigurationHttpMessageConverter;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
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 org.springframework.web.util.UriComponentsBuilder;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* A {@code Filter} that processes OAuth 2.0 Authorization Server Configuration Requests.
*
* @author Daniel Garnier-Moiroux
* @since 0.1.1
* @see ProviderSettings
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc8414#section-3">3. Obtaining Authorization Server Metadata</a>
*/
public class OAuth2AuthorizationServerConfigurationEndpointFilter extends OncePerRequestFilter {
/**
* The default endpoint {@code URI} for OAuth 2.0 Authorization Server Configuration requests.
*/
public static final String DEFAULT_OAUTH2_AUTHORIZATION_SERVER_CONFIGURATION_ENDPOINT_URI = "/.well-known/oauth-authorization-server";
private final RequestMatcher requestMatcher;
private final ProviderSettings providerSettings;
private final OAuth2AuthorizationServerConfigurationHttpMessageConverter authorizationServerConfigurationHttpMessageConverter
= new OAuth2AuthorizationServerConfigurationHttpMessageConverter();
public OAuth2AuthorizationServerConfigurationEndpointFilter(ProviderSettings providerSettings) {
Assert.notNull(providerSettings, "providerSettings cannot be null");
this.providerSettings = providerSettings;
this.requestMatcher = new AntPathRequestMatcher(
DEFAULT_OAUTH2_AUTHORIZATION_SERVER_CONFIGURATION_ENDPOINT_URI,
HttpMethod.GET.name()
);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!this.requestMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
OAuth2AuthorizationServerConfiguration authorizationServerConfiguration = OAuth2AuthorizationServerConfiguration
.builder()
.issuer(this.providerSettings.issuer())
.authorizationEndpoint(asUrl(this.providerSettings.issuer(), this.providerSettings.authorizationEndpoint()))
.tokenEndpoint(asUrl(this.providerSettings.issuer(), this.providerSettings.tokenEndpoint()))
.tokenEndpointAuthenticationMethod("client_secret_basic") // TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_BASIC in Spring Security 5.5.0
.tokenEndpointAuthenticationMethod("client_secret_post") // TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_POST in Spring Security 5.5.0
.tokenRevocationEndpoint(asUrl(this.providerSettings.issuer(), this.providerSettings.tokenRevocationEndpoint()))
.tokenRevocationEndpointAuthenticationMethod("client_secret_basic") // TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_BASIC in Spring Security 5.5.0
.tokenRevocationEndpointAuthenticationMethod("client_secret_post") // TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_POST in Spring Security 5.5.0
.jwkSetUri(asUrl(this.providerSettings.issuer(), this.providerSettings.jwkSetEndpoint()))
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.grantType(AuthorizationGrantType.REFRESH_TOKEN.getValue())
.responseType(OAuth2AuthorizationResponseType.CODE.getValue())
.scope(OidcScopes.OPENID)
.codeChallengeMethod(PkceCodeChallengeMethod2.PLAIN.getValue())
.codeChallengeMethod(PkceCodeChallengeMethod2.S256.getValue())
.build();
ServletServerHttpResponse resp = new ServletServerHttpResponse(response);
this.authorizationServerConfigurationHttpMessageConverter.write(
authorizationServerConfiguration, MediaType.APPLICATION_JSON, resp);
}
private static String asUrl(String issuer, String endpoint) {
return UriComponentsBuilder.fromUriString(issuer).path(endpoint).toUriString();
}
}

View File

@@ -0,0 +1,91 @@
/*
* Copyright 2020 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.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
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;
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.jose.TestJwks;
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.web.OAuth2AuthorizationServerConfigurationEndpointFilter;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.Mockito.mock;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Integration tests for OAuth 2.0 Authorization Server Configuration.
*
* @author Daniel Garnier-Moiroux
*/
public class OAuth2AuthorizationServerConfigurationTests {
private static final String issuerUrl = "https://example.com/issuer1";
private static JWKSource<SecurityContext> jwkSource;
@Rule
public final SpringTestRule spring = new SpringTestRule();
@Autowired
private MockMvc mvc;
@BeforeClass
public static void setupClass() {
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
@Test
public void requestWhenServerConfigurationRequestAndIssuerSetThenReturnServerConfigurationResponse() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
this.mvc.perform(get(OAuth2AuthorizationServerConfigurationEndpointFilter.DEFAULT_OAUTH2_AUTHORIZATION_SERVER_CONFIGURATION_ENDPOINT_URI))
.andExpect(status().is2xxSuccessful())
.andExpect(jsonPath("issuer").value(issuerUrl))
.andReturn();
}
@EnableWebSecurity
@Import(OAuth2AuthorizationServerConfiguration.class)
static class AuthorizationServerConfiguration {
@Bean
RegisteredClientRepository registeredClientRepository() {
return mock(RegisteredClientRepository.class);
}
@Bean
JWKSource<SecurityContext> jwkSource() {
return jwkSource;
}
@Bean
ProviderSettings providerSettings() {
return new ProviderSettings().issuer(issuerUrl);
}
}
}

View File

@@ -0,0 +1,451 @@
/*
* Copyright 2020 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.endpoint;
import org.junit.Test;
import org.springframework.security.oauth2.core.OAuth2AuthorizationServerMetadataClaimNames;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationServerConfiguration.Builder;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link OAuth2AuthorizationServerConfiguration}.
*
* @author Daniel Garnier-Moiroux
*/
public class OAuth2AuthorizationServerConfigurationTests {
private final Builder minimalConfigurationBuilder =
OAuth2AuthorizationServerConfiguration.builder()
.issuer("https://example.com/issuer1")
.authorizationEndpoint("https://example.com/issuer1/oauth2/authorize")
.tokenEndpoint("https://example.com/issuer1/oauth2/token")
.jwkSetUri("https://example.com/issuer1/oauth2/jwks")
.scope("openid")
.responseType("code");
@Test
public void buildWhenAllRequiredClaimsAndAdditionalClaimsThenCreated() {
OAuth2AuthorizationServerConfiguration authorizationServerConfiguration = OAuth2AuthorizationServerConfiguration.builder()
.issuer("https://example.com/issuer1")
.authorizationEndpoint("https://example.com/issuer1/oauth2/authorize")
.tokenEndpoint("https://example.com/issuer1/oauth2/token")
.tokenRevocationEndpoint("https://example.com/issuer1/oauth2/revoke")
.jwkSetUri("https://example.com/issuer1/oauth2/jwks")
.scope("openid")
.responseType("code")
.grantType("authorization_code")
.grantType("client_credentials")
.tokenEndpointAuthenticationMethod("client_secret_basic")
.tokenRevocationEndpointAuthenticationMethod("client_secret_basic")
.codeChallengeMethod("plain")
.codeChallengeMethod("S256")
.claim("a-claim", "a-value")
.build();
assertThat(authorizationServerConfiguration.getIssuer()).isEqualTo(url("https://example.com/issuer1"));
assertThat(authorizationServerConfiguration.getAuthorizationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/authorize"));
assertThat(authorizationServerConfiguration.getTokenEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/token"));
assertThat(authorizationServerConfiguration.getTokenRevocationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/revoke"));
assertThat(authorizationServerConfiguration.getJwkSetUri()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks"));
assertThat(authorizationServerConfiguration.getScopes()).containsExactly("openid");
assertThat(authorizationServerConfiguration.getResponseTypes()).containsExactly("code");
assertThat(authorizationServerConfiguration.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "client_credentials");
assertThat(authorizationServerConfiguration.getTokenEndpointAuthenticationMethods()).containsExactly("client_secret_basic");
assertThat(authorizationServerConfiguration.getTokenRevocationEndpointAuthenticationMethods()).containsExactly("client_secret_basic");
assertThat(authorizationServerConfiguration.getCodeChallengeMethods()).containsExactlyInAnyOrder("plain", "S256");
assertThat(authorizationServerConfiguration.getClaimAsString("a-claim")).isEqualTo("a-value");
}
@Test
public void buildWhenOnlyRequiredClaimsThenCreated() {
OAuth2AuthorizationServerConfiguration authorizationServerConfiguration = OAuth2AuthorizationServerConfiguration.builder()
.issuer("https://example.com/issuer1")
.authorizationEndpoint("https://example.com/issuer1/oauth2/authorize")
.tokenEndpoint("https://example.com/issuer1/oauth2/token")
.jwkSetUri("https://example.com/issuer1/oauth2/jwks")
.scope("openid")
.responseType("code")
.build();
assertThat(authorizationServerConfiguration.getIssuer()).isEqualTo(url("https://example.com/issuer1"));
assertThat(authorizationServerConfiguration.getAuthorizationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/authorize"));
assertThat(authorizationServerConfiguration.getTokenEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/token"));
assertThat(authorizationServerConfiguration.getJwkSetUri()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks"));
assertThat(authorizationServerConfiguration.getScopes()).containsExactly("openid");
assertThat(authorizationServerConfiguration.getResponseTypes()).containsExactly("code");
assertThat(authorizationServerConfiguration.getGrantTypes()).isNull();
assertThat(authorizationServerConfiguration.getTokenEndpointAuthenticationMethods()).isNull();
assertThat(authorizationServerConfiguration.getTokenRevocationEndpoint()).isNull();
assertThat(authorizationServerConfiguration.getTokenRevocationEndpointAuthenticationMethods()).isNull();
assertThat(authorizationServerConfiguration.getCodeChallengeMethods()).isNull();
}
@Test
public void buildFromClaimsThenCreated() {
HashMap<String, Object> claims = new HashMap<>();
claims.put(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, "https://example.com/issuer1");
claims.put(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, "https://example.com/issuer1/oauth2/authorize");
claims.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, "https://example.com/issuer1/oauth2/token");
claims.put(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI, "https://example.com/issuer1/oauth2/jwks");
claims.put(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, Collections.singletonList("openid"));
claims.put(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, Collections.singletonList("code"));
claims.put("some-claim", "some-value");
OAuth2AuthorizationServerConfiguration authorizationServerConfiguration = OAuth2AuthorizationServerConfiguration.withClaims(claims).build();
assertThat(authorizationServerConfiguration.getIssuer()).isEqualTo(url("https://example.com/issuer1"));
assertThat(authorizationServerConfiguration.getAuthorizationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/authorize"));
assertThat(authorizationServerConfiguration.getTokenEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/token"));
assertThat(authorizationServerConfiguration.getJwkSetUri()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks"));
assertThat(authorizationServerConfiguration.getScopes()).containsExactly("openid");
assertThat(authorizationServerConfiguration.getResponseTypes()).containsExactly("code");
assertThat(authorizationServerConfiguration.getGrantTypes()).isNull();
assertThat(authorizationServerConfiguration.getTokenEndpointAuthenticationMethods()).isNull();
assertThat(authorizationServerConfiguration.getTokenRevocationEndpoint()).isNull();
assertThat(authorizationServerConfiguration.getTokenRevocationEndpointAuthenticationMethods()).isNull();
assertThat(authorizationServerConfiguration.getCodeChallengeMethods()).isNull();
assertThat(authorizationServerConfiguration.getClaimAsString("some-claim")).isEqualTo("some-value");
}
@Test
public void buildFromClaimsWhenUsingUrlsThenCreated() {
HashMap<String, Object> claims = new HashMap<>();
claims.put(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, url("https://example.com/issuer1"));
claims.put(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, url("https://example.com/issuer1/oauth2/authorize"));
claims.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, url("https://example.com/issuer1/oauth2/token"));
claims.put(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT, url("https://example.com/issuer1/oauth2/revoke"));
claims.put(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI, url("https://example.com/issuer1/oauth2/jwks"));
claims.put(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, Collections.singletonList("openid"));
claims.put(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, Collections.singletonList("code"));
claims.put("some-claim", "some-value");
OAuth2AuthorizationServerConfiguration authorizationServerConfiguration = OAuth2AuthorizationServerConfiguration.withClaims(claims).build();
assertThat(authorizationServerConfiguration.getIssuer()).isEqualTo(url("https://example.com/issuer1"));
assertThat(authorizationServerConfiguration.getAuthorizationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/authorize"));
assertThat(authorizationServerConfiguration.getTokenEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/token"));
assertThat(authorizationServerConfiguration.getTokenRevocationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/revoke"));
assertThat(authorizationServerConfiguration.getJwkSetUri()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks"));
assertThat(authorizationServerConfiguration.getScopes()).containsExactly("openid");
assertThat(authorizationServerConfiguration.getResponseTypes()).containsExactly("code");
assertThat(authorizationServerConfiguration.getGrantTypes()).isNull();
assertThat(authorizationServerConfiguration.getTokenEndpointAuthenticationMethods()).isNull();
assertThat(authorizationServerConfiguration.getTokenRevocationEndpointAuthenticationMethods()).isNull();
assertThat(authorizationServerConfiguration.getCodeChallengeMethods()).isNull();
assertThat(authorizationServerConfiguration.getClaimAsString("some-claim")).isEqualTo("some-value");
}
@Test
public void withClaimsWhenNullThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> OAuth2AuthorizationServerConfiguration.withClaims(null))
.withMessage("claims cannot be empty");
}
@Test
public void withClaimsWhenMissingRequiredClaimsThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> OAuth2AuthorizationServerConfiguration.withClaims(Collections.emptyMap()))
.withMessage("claims cannot be empty");
}
@Test
public void buildWhenCalledTwiceThenGeneratesTwoConfigurations() {
OAuth2AuthorizationServerConfiguration first = this.minimalConfigurationBuilder
.grantType("client_credentials")
.build();
OAuth2AuthorizationServerConfiguration second = this.minimalConfigurationBuilder
.claims((claims) ->
{
LinkedHashSet<String> newGrantTypes = new LinkedHashSet<>();
newGrantTypes.add("authorization_code");
newGrantTypes.add("custom_grant");
claims.put(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, newGrantTypes);
}
)
.build();
assertThat(first.getGrantTypes()).containsExactly("client_credentials");
assertThat(second.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "custom_grant");
}
@Test
public void buildWhenEmptyClaimsThenOmitted() {
OAuth2AuthorizationServerConfiguration authorizationServerConfiguration = this.minimalConfigurationBuilder
.claim("some-claim", Collections.emptyList())
.claims(claims -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, Collections.emptyList()))
.build();
assertThat(authorizationServerConfiguration.getClaimAsStringList("some-claim")).isNull();
assertThat(authorizationServerConfiguration.getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED)).isNull();
}
@Test
public void buildWhenMissingIssuerThenThrowsIllegalArgumentException() {
Builder builder = this.minimalConfigurationBuilder
.claims((claims) -> claims.remove(OAuth2AuthorizationServerMetadataClaimNames.ISSUER));
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessage("issuer cannot be null");
}
@Test
public void buildWhenIssuerIsNotAnUrlThenThrowsIllegalArgumentException() {
Builder builder = this.minimalConfigurationBuilder
.claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, "not an url"));
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessageStartingWith("issuer must be a valid URL");
}
@Test
public void buildWhenMissingAuthorizationEndpointThenThrowsIllegalArgumentException() {
Builder builder = this.minimalConfigurationBuilder
.claims((claims) -> claims.remove(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT));
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessage("authorizationEndpoint cannot be null");
}
@Test
public void buildWhenAuthorizationEndpointIsNotAnUrlThenThrowsIllegalArgumentException() {
Builder builder = this.minimalConfigurationBuilder
.claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, "not an url"));
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessageStartingWith("authorizationEndpoint must be a valid URL");
}
@Test
public void buildWhenMissingTokenEndpointThenThrowsIllegalArgumentException() {
Builder builder = this.minimalConfigurationBuilder
.claims((claims) -> claims.remove(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT));
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessage("tokenEndpoint cannot be null");
}
@Test
public void buildWhenTokenEndpointIsNotAnUrlThenThrowsIllegalArgumentException() {
Builder builder = this.minimalConfigurationBuilder
.claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, "not an url"));
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessageStartingWith("tokenEndpoint must be a valid URL");
}
@Test
public void buildWhenMissingJwksUriThenThrowsIllegalArgumentException() {
Builder builder = this.minimalConfigurationBuilder
.claims((claims) -> claims.remove(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI));
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessage("jwksUri cannot be null");
}
@Test
public void buildWhenJwksUriIsNotAnUrlThenThrowsIllegalArgumentException() {
Builder builder = this.minimalConfigurationBuilder
.claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI, "not an url"));
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessageStartingWith("jwksUri must be a valid URL");
}
@Test
public void buildWhenMissingResponseTypesThenThrowsIllegalArgumentException() {
Builder builder = this.minimalConfigurationBuilder
.claims((claims) -> claims.remove(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED));
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessage("responseTypes cannot be null");
}
@Test
public void buildWhenResponseTypesNotListThenThrowIllegalArgumentException() {
Builder builder = this.minimalConfigurationBuilder
.claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, "not-a-list"));
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessageStartingWith("responseTypes must be of type List");
}
@Test
public void buildWhenResponseTypesEmptyListThenThrowIllegalArgumentException() {
Builder builder = this.minimalConfigurationBuilder
.claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, Collections.emptyList()));
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessage("responseTypes cannot be empty");
}
@Test
public void buildWhenInvalidTokenRevocationEndpointThenThrowsIllegalArgumentException() {
Builder builder = this.minimalConfigurationBuilder
.tokenRevocationEndpoint("not a valid URL");
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessage("tokenRevocationEndpoint must be a valid URL");
}
@Test
public void responseTypesWhenAddingOrRemovingThenCorrectValues() {
OAuth2AuthorizationServerConfiguration configuration = this.minimalConfigurationBuilder
.responseType("should-be-removed")
.responseTypes(responseTypes -> {
responseTypes.clear();
responseTypes.add("some-response-type");
})
.build();
assertThat(configuration.getResponseTypes()).containsExactly("some-response-type");
}
@Test
public void responseTypesWhenNotPresentAndAddingThenCorrectValues() {
OAuth2AuthorizationServerConfiguration configuration = this.minimalConfigurationBuilder
.claims(claims -> claims.remove(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED))
.responseTypes(responseTypes -> responseTypes.add("some-response-type"))
.build();
assertThat(configuration.getResponseTypes()).containsExactly("some-response-type");
}
@Test
public void scopesWhenAddingOrRemovingThenCorrectValues() {
OAuth2AuthorizationServerConfiguration configuration = this.minimalConfigurationBuilder
.scope("should-be-removed")
.scopes(scopes -> {
scopes.clear();
scopes.add("some-scope");
})
.build();
assertThat(configuration.getScopes()).containsExactly("some-scope");
}
@Test
public void grantTypesWhenAddingOrRemovingThenCorrectValues() {
OAuth2AuthorizationServerConfiguration configuration = this.minimalConfigurationBuilder
.grantType("should-be-removed")
.grantTypes(grantTypes -> {
grantTypes.clear();
grantTypes.add("some-grant-type");
})
.build();
assertThat(configuration.getGrantTypes()).containsExactly("some-grant-type");
}
@Test
public void tokenEndpointAuthenticationMethodsWhenAddingOrRemovingThenCorrectValues() {
OAuth2AuthorizationServerConfiguration configuration = this.minimalConfigurationBuilder
.tokenEndpointAuthenticationMethod("should-be-removed")
.tokenEndpointAuthenticationMethods(authMethods -> {
authMethods.clear();
authMethods.add("some-authentication-method");
})
.build();
assertThat(configuration.getTokenEndpointAuthenticationMethods()).containsExactly("some-authentication-method");
}
@Test
public void tokenRevocationEndpointAuthenticationMethodsWhenAddingOrRemovingThenCorrectValues() {
OAuth2AuthorizationServerConfiguration configuration = this.minimalConfigurationBuilder
.tokenRevocationEndpointAuthenticationMethod("should-be-removed")
.tokenRevocationEndpointAuthenticationMethods(authMethods -> {
authMethods.clear();
authMethods.add("some-authentication-method");
})
.build();
assertThat(configuration.getTokenRevocationEndpointAuthenticationMethods()).containsExactly("some-authentication-method");
}
@Test
public void codeChallengeMethodsMethodsWhenAddingOrRemovingThenCorrectValues() {
OAuth2AuthorizationServerConfiguration configuration = this.minimalConfigurationBuilder
.codeChallengeMethod("should-be-removed")
.codeChallengeMethods(codeChallengeMethods -> {
codeChallengeMethods.clear();
codeChallengeMethods.add("some-authentication-method");
})
.build();
assertThat(configuration.getCodeChallengeMethods()).containsExactly("some-authentication-method");
}
@Test
public void claimWhenNameIsNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> OAuth2AuthorizationServerConfiguration.builder().claim(null, "value"))
.withMessage("name cannot be empty");
}
@Test
public void claimWhenValueIsNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> OAuth2AuthorizationServerConfiguration.builder().claim("claim-name", null))
.withMessage("value cannot be null");
}
@Test
public void claimsWhenRemovingClaimThenNotPresent() {
OAuth2AuthorizationServerConfiguration configuration =
this.minimalConfigurationBuilder
.grantType("some-grant-type")
.claims((claims) -> claims.remove(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED))
.build();
assertThat(configuration.getGrantTypes()).isNull();
}
@Test
public void claimsWhenAddingClaimThenPresent() {
OAuth2AuthorizationServerConfiguration configuration =
this.minimalConfigurationBuilder
.claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, "authorization_code"))
.build();
assertThat(configuration.getGrantTypes()).containsExactly("authorization_code");
}
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,41 @@
/*
* Copyright 2020 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.endpoint;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* TODO
* This class is temporary and will be removed after upgrading to Spring Security 5.5.0 GA.
*
* Tests for {@link PkceCodeChallengeMethod2}.
*
* @author Daniel Garnier-Moiroux
*/
public class PkceCodeChallengeMethod2Test {
@Test
public void getValueWhenCodeChallengeMethodPlainThenReturnPlain() {
assertThat(PkceCodeChallengeMethod2.PLAIN.getValue()).isEqualTo("plain");
}
@Test
public void getValueWhenCodeChallengeMethodS256ThenReturnS256() {
assertThat(PkceCodeChallengeMethod2.S256.getValue()).isEqualTo("S256");
}
}

View File

@@ -0,0 +1,218 @@
/*
* Copyright 2020 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.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.endpoint.OAuth2AuthorizationServerConfiguration;
import java.net.URL;
import java.util.Arrays;
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;
/**
* Tests for {@link OAuth2AuthorizationServerConfigurationHttpMessageConverter}
*
* @author Daniel Garnier-Moiroux
*/
public class OAuth2AuthorizationServerConfigurationHttpMessageConverterTests {
private final OAuth2AuthorizationServerConfigurationHttpMessageConverter messageConverter = new OAuth2AuthorizationServerConfigurationHttpMessageConverter();
@Test
public void supportsWhenOAuth2AuthorizationServerConfigurationThenTrue() {
assertThat(this.messageConverter.supports(OAuth2AuthorizationServerConfiguration.class)).isTrue();
}
@Test
public void setAuthorizationServerConfigurationParametersConverterWhenConverterIsNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.messageConverter.setAuthorizationServerConfigurationParametersConverter(null));
}
@Test
public void setAuthorizationServerConfigurationConverterWhenConverterIsNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.messageConverter.setAuthorizationServerConfigurationConverter(null));
}
@Test
public void readInternalWhenRequiredParametersThenSuccess() throws Exception {
// @formatter:off
String serverConfigurationResponse = "{\n"
+ " \"issuer\": \"https://example.com/issuer1\",\n"
+ " \"authorization_endpoint\": \"https://example.com/issuer1/oauth2/authorize\",\n"
+ " \"token_endpoint\": \"https://example.com/issuer1/oauth2/token\",\n"
+ " \"jwks_uri\": \"https://example.com/issuer1/oauth2/jwks\",\n"
+ " \"response_types_supported\": [\"code\"]\n"
+ "}\n";
// @formatter:on
MockClientHttpResponse response = new MockClientHttpResponse(serverConfigurationResponse.getBytes(), HttpStatus.OK);
OAuth2AuthorizationServerConfiguration serverConfiguration = this.messageConverter
.readInternal(OAuth2AuthorizationServerConfiguration.class, response);
assertThat(serverConfiguration.getIssuer()).isEqualTo(new URL("https://example.com/issuer1"));
assertThat(serverConfiguration.getAuthorizationEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/authorize"));
assertThat(serverConfiguration.getTokenEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/token"));
assertThat(serverConfiguration.getJwkSetUri()).isEqualTo(new URL("https://example.com/issuer1/oauth2/jwks"));
assertThat(serverConfiguration.getResponseTypes()).containsExactly("code");
assertThat(serverConfiguration.getScopes()).isNull();
assertThat(serverConfiguration.getGrantTypes()).isNull();
assertThat(serverConfiguration.getTokenEndpointAuthenticationMethods()).isNull();
assertThat(serverConfiguration.getCodeChallengeMethods()).isNull();
assertThat(serverConfiguration.getTokenRevocationEndpoint()).isNull();
assertThat(serverConfiguration.getTokenRevocationEndpointAuthenticationMethods()).isNull();
}
@Test
public void readInternalWhenValidParametersThenSuccess() throws Exception {
// @formatter:off
String serverConfigurationResponse = "{\n"
+ " \"issuer\": \"https://example.com/issuer1\",\n"
+ " \"authorization_endpoint\": \"https://example.com/issuer1/oauth2/authorize\",\n"
+ " \"token_endpoint\": \"https://example.com/issuer1/oauth2/token\",\n"
+ " \"revocation_endpoint\": \"https://example.com/issuer1/oauth2/revoke\",\n"
+ " \"jwks_uri\": \"https://example.com/issuer1/oauth2/jwks\",\n"
+ " \"response_types_supported\": [\"code\"],\n"
+ " \"grant_types_supported\": [\"authorization_code\", \"client_credentials\"],\n"
+ " \"scopes_supported\": [\"openid\"],\n"
+ " \"token_endpoint_auth_methods_supported\": [\"client_secret_basic\"],\n"
+ " \"revocation_endpoint_auth_methods_supported\": [\"client_secret_basic\"],\n"
+ " \"code_challenge_methods_supported\": [\"plain\",\"S256\"],\n"
+ " \"custom_claim\": \"value\",\n"
+ " \"custom_collection_claim\": [\"value1\", \"value2\"]\n"
+ "}\n";
// @formatter:on
MockClientHttpResponse response = new MockClientHttpResponse(serverConfigurationResponse.getBytes(), HttpStatus.OK);
OAuth2AuthorizationServerConfiguration serverConfiguration = this.messageConverter
.readInternal(OAuth2AuthorizationServerConfiguration.class, response);
assertThat(serverConfiguration.getClaims()).hasSize(13);
assertThat(serverConfiguration.getIssuer()).isEqualTo(new URL("https://example.com/issuer1"));
assertThat(serverConfiguration.getAuthorizationEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/authorize"));
assertThat(serverConfiguration.getTokenEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/token"));
assertThat(serverConfiguration.getTokenRevocationEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/revoke"));
assertThat(serverConfiguration.getJwkSetUri()).isEqualTo(new URL("https://example.com/issuer1/oauth2/jwks"));
assertThat(serverConfiguration.getResponseTypes()).containsExactly("code");
assertThat(serverConfiguration.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "client_credentials");
assertThat(serverConfiguration.getScopes()).containsExactly("openid");
assertThat(serverConfiguration.getTokenEndpointAuthenticationMethods()).containsExactly("client_secret_basic");
assertThat(serverConfiguration.getTokenRevocationEndpointAuthenticationMethods()).containsExactly("client_secret_basic");
assertThat(serverConfiguration.getCodeChallengeMethods()).containsExactlyInAnyOrder("plain", "S256");
assertThat(serverConfiguration.getClaimAsString("custom_claim")).isEqualTo("value");
assertThat(serverConfiguration.getClaimAsStringList("custom_collection_claim")).containsExactlyInAnyOrder("value1", "value2");
}
@Test
public void readInternalWhenFailingConverterThenThrowException() {
String errorMessage = "this is not a valid converter";
this.messageConverter.setAuthorizationServerConfigurationConverter(source -> {
throw new RuntimeException(errorMessage);
});
MockClientHttpResponse response = new MockClientHttpResponse("{}".getBytes(), HttpStatus.OK);
assertThatExceptionOfType(HttpMessageNotReadableException.class)
.isThrownBy(() -> this.messageConverter.readInternal(OAuth2AuthorizationServerConfiguration.class, response))
.withMessageContaining("An error occurred reading the OAuth 2.0 Authorization Server Configuration")
.withMessageContaining(errorMessage);
}
@Test
public void readInternalWhenInvalidOAuth2AuthorizationServerConfigurationThenThrowException() {
String providerConfigurationResponse = "{ \"issuer\": null }";
MockClientHttpResponse response = new MockClientHttpResponse(providerConfigurationResponse.getBytes(), HttpStatus.OK);
assertThatExceptionOfType(HttpMessageNotReadableException.class)
.isThrownBy(() -> this.messageConverter.readInternal(OAuth2AuthorizationServerConfiguration.class, response))
.withMessageContaining("An error occurred reading the OAuth 2.0 Authorization Server Configuration")
.withMessageContaining("issuer cannot be null");
}
@Test
public void writeInternalWhenOAuth2AuthorizationServerConfigurationThenSuccess() {
OAuth2AuthorizationServerConfiguration serverConfiguration =
OAuth2AuthorizationServerConfiguration
.builder()
.issuer("https://example.com/issuer1")
.authorizationEndpoint("https://example.com/issuer1/oauth2/authorize")
.tokenEndpoint("https://example.com/issuer1/oauth2/token")
.tokenRevocationEndpoint("https://example.com/issuer1/oauth2/revoke")
.jwkSetUri("https://example.com/issuer1/oauth2/jwks")
.scope("openid")
.responseType("code")
.grantType("authorization_code")
.grantType("client_credentials")
.tokenEndpointAuthenticationMethod("client_secret_basic")
.tokenRevocationEndpointAuthenticationMethod("client_secret_basic")
.codeChallengeMethod("plain")
.codeChallengeMethod("S256")
.claim("custom_claim", "value")
.claim("custom_collection_claim", Arrays.asList("value1", "value2"))
.build();
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
this.messageConverter.writeInternal(serverConfiguration, outputMessage);
String serverConfigurationResponse = outputMessage.getBodyAsString();
assertThat(serverConfigurationResponse).contains("\"issuer\":\"https://example.com/issuer1\"");
assertThat(serverConfigurationResponse).contains("\"authorization_endpoint\":\"https://example.com/issuer1/oauth2/authorize\"");
assertThat(serverConfigurationResponse).contains("\"token_endpoint\":\"https://example.com/issuer1/oauth2/token\"");
assertThat(serverConfigurationResponse).contains("\"revocation_endpoint\":\"https://example.com/issuer1/oauth2/revoke\"");
assertThat(serverConfigurationResponse).contains("\"jwks_uri\":\"https://example.com/issuer1/oauth2/jwks\"");
assertThat(serverConfigurationResponse).contains("\"scopes_supported\":[\"openid\"]");
assertThat(serverConfigurationResponse).contains("\"response_types_supported\":[\"code\"]");
assertThat(serverConfigurationResponse).contains("\"grant_types_supported\":[\"authorization_code\",\"client_credentials\"]");
assertThat(serverConfigurationResponse).contains("\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\"]");
assertThat(serverConfigurationResponse).contains("\"revocation_endpoint_auth_methods_supported\":[\"client_secret_basic\"]");
assertThat(serverConfigurationResponse).contains("\"code_challenge_methods_supported\":[\"plain\",\"S256\"]");
assertThat(serverConfigurationResponse).contains("\"custom_claim\":\"value\"");
assertThat(serverConfigurationResponse).contains("\"custom_collection_claim\":[\"value1\",\"value2\"]");
}
@Test
public void writeInternalWhenWriteFailsThenThrowsException() {
String errorMessage = "this is not a valid converter";
Converter<OAuth2AuthorizationServerConfiguration, Map<String, Object>> failingConverter =
source -> {
throw new RuntimeException(errorMessage);
};
this.messageConverter.setAuthorizationServerConfigurationParametersConverter(failingConverter);
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
OAuth2AuthorizationServerConfiguration serverConfiguration =
OAuth2AuthorizationServerConfiguration
.builder()
.issuer("https://example.com/issuer1")
.authorizationEndpoint("https://example.com/issuer1/oauth2/authorize")
.tokenEndpoint("https://example.com/issuer1/oauth2/token")
.jwkSetUri("https://example.com/issuer1/oauth2/jwks")
.responseType("code")
.build();
assertThatExceptionOfType(HttpMessageNotWritableException.class)
.isThrownBy(() -> this.messageConverter.writeInternal(serverConfiguration, outputMessage))
.withMessageContaining("An error occurred writing the OAuth 2.0 Authorization Server Configuration")
.withMessageContaining(errorMessage);
}
}

View File

@@ -25,7 +25,7 @@ import java.util.Map;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link OidcProviderConfiguration}.
@@ -157,15 +157,16 @@ public class OidcProviderConfigurationTests {
@Test
public void withClaimsWhenNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> OidcProviderConfiguration.withClaims(null))
.isInstanceOf(IllegalArgumentException.class);
assertThatIllegalArgumentException().isThrownBy(() -> OidcProviderConfiguration.withClaims(null))
.isInstanceOf(IllegalArgumentException.class)
.withMessage("claims cannot be empty");
}
@Test
public void withClaimsWhenMissingRequiredClaimsThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> OidcProviderConfiguration.withClaims(Collections.emptyMap()))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("claims cannot be empty");
assertThatIllegalArgumentException()
.isThrownBy(() -> OidcProviderConfiguration.withClaims(Collections.emptyMap()))
.withMessage("claims cannot be empty");
}
@Test
@@ -189,14 +190,25 @@ public class OidcProviderConfigurationTests {
assertThat(second.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "custom_grant");
}
@Test
public void buildWhenEmptyClaimsThenOmitted() {
OidcProviderConfiguration providerConfiguration = this.minimalConfigurationBuilder
.claim("some-claim", Collections.emptyList())
.claims(claims -> claims.put(OidcProviderMetadataClaimNames.GRANT_TYPES_SUPPORTED, Collections.emptyList()))
.build();
assertThat(providerConfiguration.getClaimAsStringList("some-claim")).isNull();
assertThat(providerConfiguration.getClaimAsStringList(OidcProviderMetadataClaimNames.GRANT_TYPES_SUPPORTED)).isNull();
}
@Test
public void buildWhenMissingIssuerThenThrowIllegalArgumentException() {
OidcProviderConfiguration.Builder builder = this.minimalConfigurationBuilder
.claims((claims) -> claims.remove(OidcProviderMetadataClaimNames.ISSUER));
assertThatThrownBy(builder::build)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("issuer cannot be null");
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessage("issuer cannot be null");
}
@Test
@@ -204,9 +216,9 @@ public class OidcProviderConfigurationTests {
OidcProviderConfiguration.Builder builder = this.minimalConfigurationBuilder
.claims((claims) -> claims.put(OidcProviderMetadataClaimNames.ISSUER, "not an url"));
assertThatThrownBy(builder::build)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("issuer must be a valid URL");
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessage("issuer must be a valid URL");
}
@Test
@@ -214,9 +226,9 @@ public class OidcProviderConfigurationTests {
OidcProviderConfiguration.Builder builder = this.minimalConfigurationBuilder
.claims((claims) -> claims.remove(OidcProviderMetadataClaimNames.AUTHORIZATION_ENDPOINT));
assertThatThrownBy(builder::build)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("authorizationEndpoint cannot be null");
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessage("authorizationEndpoint cannot be null");
}
@Test
@@ -224,9 +236,9 @@ public class OidcProviderConfigurationTests {
OidcProviderConfiguration.Builder builder = this.minimalConfigurationBuilder
.claims((claims) -> claims.put(OidcProviderMetadataClaimNames.AUTHORIZATION_ENDPOINT, "not an url"));
assertThatThrownBy(builder::build)
.isInstanceOf(IllegalArgumentException.class)
.hasMessageStartingWith("authorizationEndpoint must be a valid URL");
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessageStartingWith("authorizationEndpoint must be a valid URL");
}
@Test
@@ -234,9 +246,9 @@ public class OidcProviderConfigurationTests {
OidcProviderConfiguration.Builder builder = this.minimalConfigurationBuilder
.claims((claims) -> claims.remove(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT));
assertThatThrownBy(builder::build)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("tokenEndpoint cannot be null");
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessage("tokenEndpoint cannot be null");
}
@Test
@@ -244,9 +256,9 @@ public class OidcProviderConfigurationTests {
OidcProviderConfiguration.Builder builder = this.minimalConfigurationBuilder
.claims((claims) -> claims.put(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT, "not an url"));
assertThatThrownBy(builder::build)
.isInstanceOf(IllegalArgumentException.class)
.hasMessageStartingWith("tokenEndpoint must be a valid URL");
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessageStartingWith("tokenEndpoint must be a valid URL");
}
@Test
@@ -254,9 +266,9 @@ public class OidcProviderConfigurationTests {
OidcProviderConfiguration.Builder builder = this.minimalConfigurationBuilder
.claims((claims) -> claims.remove(OidcProviderMetadataClaimNames.JWKS_URI));
assertThatThrownBy(builder::build)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("jwksUri cannot be null");
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessage("jwksUri cannot be null");
}
@Test
@@ -264,9 +276,9 @@ public class OidcProviderConfigurationTests {
OidcProviderConfiguration.Builder builder = this.minimalConfigurationBuilder
.claims((claims) -> claims.put(OidcProviderMetadataClaimNames.JWKS_URI, "not an url"));
assertThatThrownBy(builder::build)
.isInstanceOf(IllegalArgumentException.class)
.hasMessageStartingWith("jwksUri must be a valid URL");
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessageStartingWith("jwksUri must be a valid URL");
}
@Test
@@ -274,9 +286,9 @@ public class OidcProviderConfigurationTests {
OidcProviderConfiguration.Builder builder = this.minimalConfigurationBuilder
.claims((claims) -> claims.remove(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED));
assertThatThrownBy(builder::build)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("responseTypes cannot be null");
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessage("responseTypes cannot be null");
}
@Test
@@ -287,9 +299,9 @@ public class OidcProviderConfigurationTests {
claims.put(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, "code");
});
assertThatThrownBy(builder::build)
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("responseTypes must be of type List");
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessageContaining("responseTypes must be of type List");
}
@Test
@@ -300,9 +312,9 @@ public class OidcProviderConfigurationTests {
claims.put(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, Collections.emptyList());
});
assertThatThrownBy(builder::build)
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("responseTypes cannot be empty");
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessageContaining("responseTypes cannot be empty");
}
@Test
@@ -310,9 +322,9 @@ public class OidcProviderConfigurationTests {
OidcProviderConfiguration.Builder builder = this.minimalConfigurationBuilder
.claims((claims) -> claims.remove(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED));
assertThatThrownBy(builder::build)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("subjectTypes cannot be null");
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessage("subjectTypes cannot be null");
}
@Test
@@ -323,9 +335,9 @@ public class OidcProviderConfigurationTests {
claims.put(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED, "public");
});
assertThatThrownBy(builder::build)
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("subjectTypes must be of type List");
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessageContaining("subjectTypes must be of type List");
}
@Test
@@ -336,9 +348,9 @@ public class OidcProviderConfigurationTests {
claims.put(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED, Collections.emptyList());
});
assertThatThrownBy(builder::build)
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("subjectTypes cannot be empty");
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessageContaining("subjectTypes cannot be empty");
}
@Test
@@ -346,9 +358,9 @@ public class OidcProviderConfigurationTests {
OidcProviderConfiguration.Builder builder = this.minimalConfigurationBuilder
.claims((claims) -> claims.remove(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED));
assertThatThrownBy(builder::build)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("idTokenSigningAlgorithms cannot be null");
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessage("idTokenSigningAlgorithms cannot be null");
}
@Test
@@ -359,9 +371,9 @@ public class OidcProviderConfigurationTests {
claims.put(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED, "RS256");
});
assertThatThrownBy(builder::build)
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("idTokenSigningAlgorithms must be of type List");
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessageContaining("idTokenSigningAlgorithms must be of type List");
}
@Test
@@ -372,9 +384,9 @@ public class OidcProviderConfigurationTests {
claims.put(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED, Collections.emptyList());
});
assertThatThrownBy(builder::build)
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("idTokenSigningAlgorithms cannot be empty");
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessageContaining("idTokenSigningAlgorithms cannot be empty");
}
@Test
@@ -467,16 +479,16 @@ public class OidcProviderConfigurationTests {
@Test
public void claimWhenNameIsNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> OidcProviderConfiguration.builder().claim(null, "value"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("name cannot be empty");
assertThatIllegalArgumentException()
.isThrownBy(() -> OidcProviderConfiguration.builder().claim(null, "value"))
.withMessage("name cannot be empty");
}
@Test
public void claimWhenValueIsNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> OidcProviderConfiguration.builder().claim("claim-name", null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("value cannot be null");
assertThatIllegalArgumentException()
.isThrownBy(() -> OidcProviderConfiguration.builder().claim("claim-name", null))
.withMessage("value cannot be null");
}
@Test

View File

@@ -31,7 +31,6 @@ 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;
/**
* Tests for {@link OidcProviderConfigurationHttpMessageConverter}
@@ -205,9 +204,9 @@ public class OidcProviderConfigurationHttpMessageConverterTests {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
assertThatThrownBy(() -> this.messageConverter.writeInternal(providerConfiguration, outputMessage))
.isInstanceOf(HttpMessageNotWritableException.class)
.hasMessageContaining("An error occurred writing the OpenID Provider Configuration")
.hasMessageContaining(errorMessage);
assertThatExceptionOfType(HttpMessageNotWritableException.class)
.isThrownBy(() -> this.messageConverter.writeInternal(providerConfiguration, outputMessage))
.withMessageContaining("An error occurred writing the OpenID Provider Configuration")
.withMessageContaining(errorMessage);
}
}

View File

@@ -18,7 +18,7 @@ package org.springframework.security.oauth2.server.authorization.config;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link ProviderSettings}.
@@ -78,48 +78,48 @@ public class ProviderSettingsTests {
@Test
public void issuerWhenNullThenThrowIllegalArgumentException() {
ProviderSettings settings = new ProviderSettings();
assertThatThrownBy(() -> settings.issuer(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("value cannot be null");
assertThatIllegalArgumentException()
.isThrownBy(() -> settings.issuer(null))
.withMessage("value cannot be null");
}
@Test
public void authorizationEndpointWhenNullThenThrowIllegalArgumentException() {
ProviderSettings settings = new ProviderSettings();
assertThatThrownBy(() -> settings.authorizationEndpoint(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("value cannot be null");
assertThatIllegalArgumentException()
.isThrownBy(() -> settings.authorizationEndpoint(null))
.withMessage("value cannot be null");
}
@Test
public void tokenEndpointWhenNullThenThrowIllegalArgumentException() {
ProviderSettings settings = new ProviderSettings();
assertThatThrownBy(() -> settings.tokenEndpoint(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("value cannot be null");
assertThatIllegalArgumentException()
.isThrownBy(() -> settings.tokenEndpoint(null))
.withMessage("value cannot be null");
}
@Test
public void tokenRevocationEndpointWhenNullThenThrowIllegalArgumentException() {
ProviderSettings settings = new ProviderSettings();
assertThatThrownBy(() -> settings.tokenRevocationEndpoint(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("value cannot be null");
assertThatIllegalArgumentException()
.isThrownBy(() -> settings.tokenRevocationEndpoint(null))
.withMessage("value cannot be null");
}
@Test
public void tokenIntrospectionEndpointWhenNullThenThrowIllegalArgumentException() {
ProviderSettings settings = new ProviderSettings();
assertThatThrownBy(() -> settings.tokenIntrospectionEndpoint(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("value cannot be null");
assertThatIllegalArgumentException()
.isThrownBy(() -> settings.tokenIntrospectionEndpoint(null))
.withMessage("value cannot be null");
}
@Test
public void jwksEndpointWhenNullThenThrowIllegalArgumentException() {
ProviderSettings settings = new ProviderSettings();
assertThatThrownBy(() -> settings.jwkSetEndpoint(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("value cannot be null");
assertThatIllegalArgumentException()
.isThrownBy(() -> settings.jwkSetEndpoint(null))
.withMessage("value cannot be null");
}
}

View File

@@ -26,7 +26,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@@ -41,9 +41,9 @@ public class OidcProviderConfigurationEndpointFilterTests {
@Test
public void constructorWhenProviderSettingsNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new OidcProviderConfigurationEndpointFilter(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("providerSettings cannot be null");
assertThatIllegalArgumentException()
.isThrownBy(() -> new OidcProviderConfigurationEndpointFilter(null))
.withMessage("providerSettings cannot be null");
}
@Test
@@ -129,8 +129,8 @@ public class OidcProviderConfigurationEndpointFilterTests {
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
assertThatThrownBy(() -> filter.doFilter(request, response, filterChain))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("issuer must be a valid URL");
assertThatIllegalArgumentException()
.isThrownBy(() -> filter.doFilter(request, response, filterChain))
.withMessage("issuer must be a valid URL");
}
}

View File

@@ -0,0 +1,141 @@
/*
* Copyright 2020 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.web;
import org.junit.Test;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
/**
* Tests for {@link OAuth2AuthorizationServerConfigurationEndpointFilter}.
*
* @author Daniel Garnier-Moiroux
*/
public class OAuth2AuthorizationServerConfigurationEndpointFilterTests {
@Test
public void constructorWhenProviderSettingsNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new OAuth2AuthorizationServerConfigurationEndpointFilter(null))
.withMessage("providerSettings cannot be null");
}
@Test
public void doFilterWhenNotAuthorizationServerConfigurationRequestThenNotProcessed() throws Exception {
OAuth2AuthorizationServerConfigurationEndpointFilter filter =
new OAuth2AuthorizationServerConfigurationEndpointFilter(new ProviderSettings().issuer("https://example.com"));
String requestUri = "/path";
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 doFilterWhenAuthorizationServerConfigurationRequestPostThenNotProcessed() throws Exception {
OAuth2AuthorizationServerConfigurationEndpointFilter filter =
new OAuth2AuthorizationServerConfigurationEndpointFilter(new ProviderSettings().issuer("https://example.com"));
String requestUri = OAuth2AuthorizationServerConfigurationEndpointFilter.DEFAULT_OAUTH2_AUTHORIZATION_SERVER_CONFIGURATION_ENDPOINT_URI;
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 doFilterWhenAuthorizationServerConfigurationRequestThenAuthorizationServerConfigurationResponse() throws Exception {
String authorizationEndpoint = "/oauth2/v1/authorize";
String tokenEndpoint = "/oauth2/v1/token";
String tokenRevocationEndpoint = "/oauth2/v1/revoke";
String jwkSetEndpoint = "/oauth2/v1/jwks";
ProviderSettings providerSettings = new ProviderSettings()
.issuer("https://example.com/issuer1")
.authorizationEndpoint(authorizationEndpoint)
.tokenEndpoint(tokenEndpoint)
.tokenRevocationEndpoint(tokenRevocationEndpoint)
.jwkSetEndpoint(jwkSetEndpoint);
OAuth2AuthorizationServerConfigurationEndpointFilter filter =
new OAuth2AuthorizationServerConfigurationEndpointFilter(providerSettings);
String requestUri = OAuth2AuthorizationServerConfigurationEndpointFilter.DEFAULT_OAUTH2_AUTHORIZATION_SERVER_CONFIGURATION_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);
verifyNoInteractions(filterChain);
assertThat(response.getContentType()).isEqualTo(MediaType.APPLICATION_JSON_VALUE);
String serverConfigurationResponse = response.getContentAsString();
assertThat(serverConfigurationResponse).contains("\"issuer\":\"https://example.com/issuer1\"");
assertThat(serverConfigurationResponse).contains("\"authorization_endpoint\":\"https://example.com/issuer1/oauth2/v1/authorize\"");
assertThat(serverConfigurationResponse).contains("\"token_endpoint\":\"https://example.com/issuer1/oauth2/v1/token\"");
assertThat(serverConfigurationResponse).contains("\"revocation_endpoint\":\"https://example.com/issuer1/oauth2/v1/revoke\"");
assertThat(serverConfigurationResponse).contains("\"jwks_uri\":\"https://example.com/issuer1/oauth2/v1/jwks\"");
assertThat(serverConfigurationResponse).contains("\"scopes_supported\":[\"openid\"]");
assertThat(serverConfigurationResponse).contains("\"response_types_supported\":[\"code\"]");
assertThat(serverConfigurationResponse).contains("\"grant_types_supported\":[\"authorization_code\",\"client_credentials\",\"refresh_token\"]");
assertThat(serverConfigurationResponse).contains("\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\"]");
assertThat(serverConfigurationResponse).contains("\"revocation_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\"]");
assertThat(serverConfigurationResponse).contains("\"code_challenge_methods_supported\":[\"plain\",\"S256\"]");
}
@Test
public void doFilterWhenProviderSettingsWithInvalidIssuerThenThrowIllegalArgumentException() {
ProviderSettings providerSettings = new ProviderSettings()
.issuer("https://this is an invalid URL");
OAuth2AuthorizationServerConfigurationEndpointFilter filter =
new OAuth2AuthorizationServerConfigurationEndpointFilter(providerSettings);
String requestUri = OAuth2AuthorizationServerConfigurationEndpointFilter.DEFAULT_OAUTH2_AUTHORIZATION_SERVER_CONFIGURATION_ENDPOINT_URI;
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
assertThatIllegalArgumentException()
.isThrownBy(() -> filter.doFilter(request, response, filterChain))
.withMessage("issuer must be a valid URL");
}
}