Remove CryptoKeySource

Closes gh-196
This commit is contained in:
Joe Grandja 2021-01-15 05:57:15 -05:00
parent 4b37606807
commit 12f4001c9d
34 changed files with 1089 additions and 1280 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,18 +15,28 @@
*/
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
import java.net.URI;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer;
import org.springframework.security.crypto.key.CryptoKeySource;
import org.springframework.security.oauth2.jose.jws.NimbusJwsEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwsEncoder;
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationProvider;
@ -36,12 +46,12 @@ import org.springframework.security.oauth2.server.authorization.authentication.O
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationProvider;
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.JwkSetEndpointFilter;
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.OAuth2ClientAuthenticationFilter;
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenRevocationEndpointFilter;
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcProviderConfigurationEndpointFilter;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint;
@ -55,12 +65,6 @@ import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import java.net.URI;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* An {@link AbstractHttpConfigurer} for OAuth 2.0 Authorization Server support.
*
@ -73,7 +77,7 @@ import java.util.Map;
* @see OAuth2AuthorizationEndpointFilter
* @see OAuth2TokenEndpointFilter
* @see OAuth2TokenRevocationEndpointFilter
* @see JwkSetEndpointFilter
* @see NimbusJwkSetEndpointFilter
* @see OidcProviderConfigurationEndpointFilter
* @see OAuth2ClientAuthenticationFilter
*/
@ -92,7 +96,7 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
private final RequestMatcher tokenRevocationEndpointMatcher = new AntPathRequestMatcher(
OAuth2TokenRevocationEndpointFilter.DEFAULT_TOKEN_REVOCATION_ENDPOINT_URI, HttpMethod.POST.name());
private final RequestMatcher jwkSetEndpointMatcher = new AntPathRequestMatcher(
JwkSetEndpointFilter.DEFAULT_JWK_SET_ENDPOINT_URI, HttpMethod.GET.name());
NimbusJwkSetEndpointFilter.DEFAULT_JWK_SET_ENDPOINT_URI, HttpMethod.GET.name());
private final RequestMatcher oidcProviderConfigurationEndpointMatcher = new AntPathRequestMatcher(
OidcProviderConfigurationEndpointFilter.DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI, HttpMethod.GET.name());
@ -120,18 +124,6 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
return this;
}
/**
* Sets the source for cryptographic keys.
*
* @param keySource the source for cryptographic keys
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
*/
public OAuth2AuthorizationServerConfigurer<B> keySource(CryptoKeySource keySource) {
Assert.notNull(keySource, "keySource cannot be null");
this.getBuilder().setSharedObject(CryptoKeySource.class, keySource);
return this;
}
/**
* Sets the provider settings.
*
@ -219,8 +211,9 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
builder.addFilterBefore(postProcess(oidcProviderConfigurationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
}
JwkSetEndpointFilter jwkSetEndpointFilter = new JwkSetEndpointFilter(
getKeySource(builder),
JWKSource<SecurityContext> jwkSource = getJwkSource(builder);
NimbusJwkSetEndpointFilter jwkSetEndpointFilter = new NimbusJwkSetEndpointFilter(
jwkSource,
providerSettings.jwkSetEndpoint());
builder.addFilterBefore(postProcess(jwkSetEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
@ -284,21 +277,27 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
}
private static <B extends HttpSecurityBuilder<B>> JwtEncoder getJwtEncoder(B builder) {
JwtEncoder jwtEncoder = getOptionalBean(builder, JwtEncoder.class);
JwtEncoder jwtEncoder = builder.getSharedObject(JwtEncoder.class);
if (jwtEncoder == null) {
CryptoKeySource keySource = getKeySource(builder);
jwtEncoder = new NimbusJwsEncoder(keySource);
jwtEncoder = getOptionalBean(builder, JwtEncoder.class);
if (jwtEncoder == null) {
JWKSource<SecurityContext> jwkSource = getJwkSource(builder);
jwtEncoder = new NimbusJwsEncoder(jwkSource);
}
builder.setSharedObject(JwtEncoder.class, jwtEncoder);
}
return jwtEncoder;
}
private static <B extends HttpSecurityBuilder<B>> CryptoKeySource getKeySource(B builder) {
CryptoKeySource keySource = builder.getSharedObject(CryptoKeySource.class);
if (keySource == null) {
keySource = getBean(builder, CryptoKeySource.class);
builder.setSharedObject(CryptoKeySource.class, keySource);
@SuppressWarnings("unchecked")
private static <B extends HttpSecurityBuilder<B>> JWKSource<SecurityContext> getJwkSource(B builder) {
JWKSource<SecurityContext> jwkSource = builder.getSharedObject(JWKSource.class);
if (jwkSource == null) {
ResolvableType type = ResolvableType.forClassWithGenerics(JWKSource.class, SecurityContext.class);
jwkSource = getBean(builder, type);
builder.setSharedObject(JWKSource.class, jwkSource);
}
return keySource;
return jwkSource;
}
private static <B extends HttpSecurityBuilder<B>> ProviderSettings getProviderSettings(B builder) {
@ -317,6 +316,19 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
return builder.getSharedObject(ApplicationContext.class).getBean(type);
}
@SuppressWarnings("unchecked")
private static <B extends HttpSecurityBuilder<B>, T> T getBean(B builder, ResolvableType type) {
ApplicationContext context = builder.getSharedObject(ApplicationContext.class);
String[] names = context.getBeanNamesForType(type);
if (names.length == 1) {
return (T) context.getBean(names[0]);
}
if (names.length > 1) {
throw new NoUniqueBeanDefinitionException(type, names);
}
throw new NoSuchBeanDefinitionException(type);
}
private static <B extends HttpSecurityBuilder<B>, T> T getOptionalBean(B builder, Class<T> type) {
Map<String, T> beansMap = BeanFactoryUtils.beansOfTypeIncludingAncestors(
builder.getSharedObject(ApplicationContext.class), type);

View File

@ -1,78 +0,0 @@
/*
* 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.crypto.key;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Map;
import java.util.UUID;
/**
* A {@link CryptoKey} that holds a {@code java.security.PrivateKey}
* and {@code java.security.PublicKey} used for asymmetric algorithm's.
*
* @author Joe Grandja
* @since 0.1.0
* @see CryptoKey
* @see PrivateKey
* @see PublicKey
*/
public final class AsymmetricKey extends CryptoKey<PrivateKey> {
private final PublicKey publicKey;
private AsymmetricKey(PrivateKey privateKey, PublicKey publicKey, String id, Map<String, Object> metadata) {
super(privateKey, id, metadata);
this.publicKey = publicKey;
}
/**
* Returns the {@code java.security.PublicKey}.
*
* @return the {@code java.security.PublicKey}
*/
public PublicKey getPublicKey() {
return this.publicKey;
}
/**
* A builder for {@link AsymmetricKey}.
*/
public static class Builder extends AbstractBuilder<AsymmetricKey, Builder> {
private PublicKey publicKey;
Builder(PrivateKey privateKey, PublicKey publicKey) {
super(privateKey);
Assert.notNull(publicKey, "publicKey cannot be null");
this.publicKey = publicKey;
}
/**
* Creates the {@link AsymmetricKey}.
*
* @return the {@link AsymmetricKey}
*/
@Override
public AsymmetricKey build() {
if (!StringUtils.hasText(this.id)) {
this.id = UUID.randomUUID().toString();
}
return new AsymmetricKey((PrivateKey) this.key, this.publicKey, this.id, this.metadata);
}
}
}

View File

@ -1,231 +0,0 @@
/*
* 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.crypto.key;
import org.springframework.security.oauth2.server.authorization.Version;
import org.springframework.util.Assert;
import javax.crypto.SecretKey;
import java.io.Serializable;
import java.security.Key;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
/**
* A holder of a {@code java.security.Key} used for cryptographic operations.
*
* @param <K> the type of {@code java.security.Key}
* @author Joe Grandja
* @since 0.1.0
* @see CryptoKeySource
*/
public class CryptoKey<K extends Key> implements Serializable {
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
private final K key;
private final String id;
private final Map<String, Object> metadata;
/**
* Constructs a {@code CryptoKey} using the provided parameters.
*
* @param key the {@code java.security.Key}
* @param id the logical identifier for the {@code key}
*/
protected CryptoKey(K key, String id) {
this(key, id, Collections.emptyMap());
}
/**
* Constructs a {@code CryptoKey} using the provided parameters.
*
* @param key the {@code java.security.Key}
* @param id the logical identifier for the {@code key}
* @param metadata the metadata describing the {@code key}
*/
protected CryptoKey(K key, String id, Map<String, Object> metadata) {
Assert.notNull(key, "key cannot be null");
Assert.hasText(id, "id cannot be empty");
Assert.notNull(metadata, "metadata cannot be null");
this.key = key;
this.id = id;
this.metadata = Collections.unmodifiableMap(new LinkedHashMap<>(metadata));
}
/**
* Returns a type of {@code java.security.Key},
* e.g. {@code javax.crypto.SecretKey} or {@code java.security.PrivateKey}.
*
* @return the type of {@code java.security.Key}
*/
public final K getKey() {
return this.key;
}
/**
* Returns the logical identifier for this key.
*
* @return the logical identifier for this key
*/
public final String getId() {
return this.id;
}
/**
* Returns the metadata value associated to this key.
*
* @param name the name of the metadata
* @param <T> the type of the metadata
* @return the metadata value, or {@code null} if not available
*/
@SuppressWarnings("unchecked")
public final <T> T getMetadata(String name) {
Assert.hasText(name, "name cannot be empty");
return (T) this.metadata.get(name);
}
/**
* Returns the metadata associated to this key.
*
* @return a {@code Map} of the metadata
*/
public final Map<String, Object> getMetadata() {
return this.metadata;
}
/**
* Returns the algorithm for this key.
*
* @return the algorithm for this key
*/
public final String getAlgorithm() {
return getKey().getAlgorithm();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
CryptoKey<?> that = (CryptoKey<?>) obj;
return Objects.equals(this.id, that.id);
}
@Override
public int hashCode() {
return Objects.hash(this.id);
}
/**
* Create a {@link SymmetricKey} via {@link SymmetricKey.Builder}.
*
* @param secretKey the {@code javax.crypto.SecretKey}
* @return the {@link SymmetricKey.Builder}
*/
public static SymmetricKey.Builder symmetric(SecretKey secretKey) {
return new SymmetricKey.Builder(secretKey);
}
/**
* Create a {@link AsymmetricKey} via {@link AsymmetricKey.Builder}.
*
* @param privateKey the {@code java.security.PrivateKey}
* @param publicKey the {@code java.security.PublicKey}
* @return the {@link AsymmetricKey.Builder}
*/
public static AsymmetricKey.Builder asymmetric(PrivateKey privateKey, PublicKey publicKey) {
return new AsymmetricKey.Builder(privateKey, publicKey);
}
/**
* Base builder for {@link CryptoKey}.
*
* @param <T> the type of {@link CryptoKey}
* @param <B> the type of {@link AbstractBuilder}
*/
protected abstract static class AbstractBuilder<T extends CryptoKey<?>, B extends AbstractBuilder<T, B>> implements Serializable {
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
protected Key key;
protected String id;
protected Map<String, Object> metadata = new HashMap<>();
/**
* Sub-class constructor.
*
* @param key the {@code java.security.Key}
*/
protected AbstractBuilder(Key key) {
Assert.notNull(key, "key cannot be null");
this.key = key;
}
/**
* Sets the logical identifier for this key.
*
* @param id the logical identifier for this key
* @return the type of {@link AbstractBuilder}
*/
@SuppressWarnings("unchecked")
public B id(String id) {
this.id = id;
return (B) this;
}
/**
* Adds metadata for this key.
*
* @param name the name of the metadata
* @param value the value of the metadata
* @return the type of {@link AbstractBuilder}
*/
@SuppressWarnings("unchecked")
public B metadata(String name, Object value) {
Assert.hasText(name, "name cannot be empty");
Assert.notNull(value, "value cannot be null");
this.metadata.put(name, value);
return (B) this;
}
/**
* A {@code Consumer} of the metadata {@code Map}
* allowing the ability to add, replace, or remove.
*
* @param metadataConsumer a {@link Consumer} of the metadata {@code Map}
* @return the type of {@link AbstractBuilder}
*/
@SuppressWarnings("unchecked")
public B metadata(Consumer<Map<String, Object>> metadataConsumer) {
metadataConsumer.accept(this.metadata);
return (B) this;
}
/**
* Creates the {@link CryptoKey}.
*
* @return the {@link CryptoKey}
*/
protected abstract T build();
}
}

View File

@ -1,38 +0,0 @@
/*
* 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.crypto.key;
import java.util.Set;
/**
* A source for {@link CryptoKey}'s.
*
* @author Joe Grandja
* @since 0.1.0
* @see CryptoKey
* @see SymmetricKey
* @see AsymmetricKey
*/
public interface CryptoKeySource {
/**
* Returns a {@code Set} of {@link CryptoKey}'s.
*
* @return a {@code Set} of {@link CryptoKey}'s
*/
Set<CryptoKey<?>> getKeys();
}

View File

@ -1,62 +0,0 @@
/*
* 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.crypto.key;
import javax.crypto.SecretKey;
import java.security.KeyPair;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.springframework.security.crypto.key.KeyGeneratorUtils.generateRsaKey;
import static org.springframework.security.crypto.key.KeyGeneratorUtils.generateSecretKey;
/**
* An implementation of a {@link CryptoKeySource} that generates the {@link CryptoKey}'s when constructed.
*
* <p>
* <b>NOTE:</b> This implementation should ONLY be used during development/testing.
*
* @author Joe Grandja
* @since 0.1.0
* @see CryptoKeySource
*/
public final class StaticKeyGeneratingCryptoKeySource implements CryptoKeySource {
private final Map<String, CryptoKey<?>> keys;
public StaticKeyGeneratingCryptoKeySource() {
this.keys = Collections.unmodifiableMap(generateKeys());
}
@Override
public Set<CryptoKey<?>> getKeys() {
return new HashSet<>(this.keys.values());
}
private static Map<String, CryptoKey<?>> generateKeys() {
KeyPair rsaKeyPair = generateRsaKey();
AsymmetricKey asymmetricKey = CryptoKey.asymmetric(rsaKeyPair.getPrivate(), rsaKeyPair.getPublic()).build();
SecretKey hmacKey = generateSecretKey();
SymmetricKey symmetricKey = CryptoKey.symmetric(hmacKey).build();
return Stream.of(asymmetricKey, symmetricKey)
.collect(Collectors.toMap(CryptoKey::getId, v -> v));
}
}

View File

@ -1,61 +0,0 @@
/*
* 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.crypto.key;
import org.springframework.util.StringUtils;
import javax.crypto.SecretKey;
import java.util.Map;
import java.util.UUID;
/**
* A {@link CryptoKey} that holds a {@code javax.crypto.SecretKey}
* used for symmetric algorithm's.
*
* @author Joe Grandja
* @since 0.1.0
* @see CryptoKey
* @see SecretKey
*/
public final class SymmetricKey extends CryptoKey<SecretKey> {
private SymmetricKey(SecretKey key, String id, Map<String, Object> metadata) {
super(key, id, metadata);
}
/**
* A builder for {@link SymmetricKey}.
*/
public static class Builder extends AbstractBuilder<SymmetricKey, Builder> {
Builder(SecretKey secretKey) {
super(secretKey);
}
/**
* Creates the {@link SymmetricKey}.
*
* @return the {@link SymmetricKey}
*/
@Override
public SymmetricKey build() {
if (!StringUtils.hasText(this.id)) {
this.id = UUID.randomUUID().toString();
}
return new SymmetricKey((SecretKey) this.key, this.id, this.metadata);
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -13,30 +13,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.jose;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.util.Assert;
package org.springframework.security.oauth2.jwt;
import java.net.URL;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import static org.springframework.security.oauth2.jose.JoseHeaderNames.ALG;
import static org.springframework.security.oauth2.jose.JoseHeaderNames.CRIT;
import static org.springframework.security.oauth2.jose.JoseHeaderNames.CTY;
import static org.springframework.security.oauth2.jose.JoseHeaderNames.JKU;
import static org.springframework.security.oauth2.jose.JoseHeaderNames.JWK;
import static org.springframework.security.oauth2.jose.JoseHeaderNames.KID;
import static org.springframework.security.oauth2.jose.JoseHeaderNames.TYP;
import static org.springframework.security.oauth2.jose.JoseHeaderNames.X5C;
import static org.springframework.security.oauth2.jose.JoseHeaderNames.X5T;
import static org.springframework.security.oauth2.jose.JoseHeaderNames.X5T_S256;
import static org.springframework.security.oauth2.jose.JoseHeaderNames.X5U;
import org.springframework.security.oauth2.core.converter.ClaimConversionService;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
import org.springframework.util.Assert;
/**
* The JOSE header is a JSON object representing the header parameters of a JSON Web Token,
@ -55,16 +44,16 @@ public final class JoseHeader {
private final Map<String, Object> headers;
private JoseHeader(Map<String, Object> headers) {
this.headers = Collections.unmodifiableMap(new LinkedHashMap<>(headers));
this.headers = Collections.unmodifiableMap(new HashMap<>(headers));
}
/**
* Returns the JWS algorithm used to digitally sign the JWS.
* Returns the {@link JwsAlgorithm JWS algorithm} used to digitally sign the JWS.
*
* @return the JWS algorithm
*/
public JwsAlgorithm getJwsAlgorithm() {
return getHeader(ALG);
return getHeader(JoseHeaderNames.ALG);
}
/**
@ -73,8 +62,8 @@ public final class JoseHeader {
*
* @return the JWK Set URL
*/
public String getJwkSetUri() {
return getHeader(JKU);
public URL getJwkSetUri() {
return getHeader(JoseHeaderNames.JKU);
}
/**
@ -84,7 +73,7 @@ public final class JoseHeader {
* @return the JSON Web Key
*/
public Map<String, Object> getJwk() {
return getHeader(JWK);
return getHeader(JoseHeaderNames.JWK);
}
/**
@ -93,7 +82,7 @@ public final class JoseHeader {
* @return the key ID
*/
public String getKeyId() {
return getHeader(KID);
return getHeader(JoseHeaderNames.KID);
}
/**
@ -102,8 +91,8 @@ public final class JoseHeader {
*
* @return the X.509 URL
*/
public String getX509Uri() {
return getHeader(X5U);
public URL getX509Uri() {
return getHeader(JoseHeaderNames.X5U);
}
/**
@ -113,7 +102,7 @@ public final class JoseHeader {
* @return the X.509 certificate chain
*/
public List<String> getX509CertificateChain() {
return getHeader(X5C);
return getHeader(JoseHeaderNames.X5C);
}
/**
@ -123,7 +112,7 @@ public final class JoseHeader {
* @return the X.509 certificate SHA-1 thumbprint
*/
public String getX509SHA1Thumbprint() {
return getHeader(X5T);
return getHeader(JoseHeaderNames.X5T);
}
/**
@ -133,7 +122,7 @@ public final class JoseHeader {
* @return the X.509 certificate SHA-256 thumbprint
*/
public String getX509SHA256Thumbprint() {
return getHeader(X5T_S256);
return getHeader(JoseHeaderNames.X5T_S256);
}
/**
@ -143,7 +132,7 @@ public final class JoseHeader {
* @return the critical headers
*/
public Set<String> getCritical() {
return getHeader(CRIT);
return getHeader(JoseHeaderNames.CRIT);
}
/**
@ -152,7 +141,7 @@ public final class JoseHeader {
* @return the type header
*/
public String getType() {
return getHeader(TYP);
return getHeader(JoseHeaderNames.TYP);
}
/**
@ -161,7 +150,7 @@ public final class JoseHeader {
* @return the content type header
*/
public String getContentType() {
return getHeader(CTY);
return getHeader(JoseHeaderNames.CTY);
}
/**
@ -209,12 +198,12 @@ public final class JoseHeader {
/**
* A builder for {@link JoseHeader}.
*/
public static class Builder {
private final Map<String, Object> headers = new LinkedHashMap<>();
public static final class Builder {
private final Map<String, Object> headers = new HashMap<>();
private Builder(JwsAlgorithm jwsAlgorithm) {
Assert.notNull(jwsAlgorithm, "jwsAlgorithm cannot be null");
header(ALG, jwsAlgorithm);
header(JoseHeaderNames.ALG, jwsAlgorithm);
}
private Builder(JoseHeader headers) {
@ -230,7 +219,7 @@ public final class JoseHeader {
* @return the {@link Builder}
*/
public Builder jwkSetUri(String jwkSetUri) {
return header(JKU, jwkSetUri);
return header(JoseHeaderNames.JKU, jwkSetUri);
}
/**
@ -241,7 +230,7 @@ public final class JoseHeader {
* @return the {@link Builder}
*/
public Builder jwk(Map<String, Object> jwk) {
return header(JWK, jwk);
return header(JoseHeaderNames.JWK, jwk);
}
/**
@ -251,7 +240,7 @@ public final class JoseHeader {
* @return the {@link Builder}
*/
public Builder keyId(String keyId) {
return header(KID, keyId);
return header(JoseHeaderNames.KID, keyId);
}
/**
@ -262,7 +251,7 @@ public final class JoseHeader {
* @return the {@link Builder}
*/
public Builder x509Uri(String x509Uri) {
return header(X5U, x509Uri);
return header(JoseHeaderNames.X5U, x509Uri);
}
/**
@ -273,7 +262,7 @@ public final class JoseHeader {
* @return the {@link Builder}
*/
public Builder x509CertificateChain(List<String> x509CertificateChain) {
return header(X5C, x509CertificateChain);
return header(JoseHeaderNames.X5C, x509CertificateChain);
}
/**
@ -284,7 +273,7 @@ public final class JoseHeader {
* @return the {@link Builder}
*/
public Builder x509SHA1Thumbprint(String x509SHA1Thumbprint) {
return header(X5T, x509SHA1Thumbprint);
return header(JoseHeaderNames.X5T, x509SHA1Thumbprint);
}
/**
@ -295,7 +284,7 @@ public final class JoseHeader {
* @return the {@link Builder}
*/
public Builder x509SHA256Thumbprint(String x509SHA256Thumbprint) {
return header(X5T_S256, x509SHA256Thumbprint);
return header(JoseHeaderNames.X5T_S256, x509SHA256Thumbprint);
}
/**
@ -306,7 +295,7 @@ public final class JoseHeader {
* @return the {@link Builder}
*/
public Builder critical(Set<String> headerNames) {
return header(CRIT, headerNames);
return header(JoseHeaderNames.CRIT, headerNames);
}
/**
@ -316,7 +305,7 @@ public final class JoseHeader {
* @return the {@link Builder}
*/
public Builder type(String type) {
return header(TYP, type);
return header(JoseHeaderNames.TYP, type);
}
/**
@ -326,7 +315,7 @@ public final class JoseHeader {
* @return the {@link Builder}
*/
public Builder contentType(String contentType) {
return header(CTY, contentType);
return header(JoseHeaderNames.CTY, contentType);
}
/**
@ -362,7 +351,19 @@ public final class JoseHeader {
*/
public JoseHeader build() {
Assert.notEmpty(this.headers, "headers cannot be empty");
convertAsURL(JoseHeaderNames.JKU);
convertAsURL(JoseHeaderNames.X5U);
return new JoseHeader(this.headers);
}
private void convertAsURL(String header) {
Object value = this.headers.get(header);
if (value != null) {
URL convertedValue = ClaimConversionService.getSharedInstance().convert(value, URL.class);
Assert.isTrue(convertedValue != null,
() -> "Unable to convert header '" + header + "' of type '" + value.getClass() + "' to URL.");
this.headers.put(header, convertedValue);
}
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.jose;
package org.springframework.security.oauth2.jwt;
/**
* The Registered Header Parameter Names defined by the JSON Web Token (JWT),
@ -28,69 +28,72 @@ package org.springframework.security.oauth2.jose;
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515#section-4">JWS JOSE Header</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7516#section-4">JWE JOSE Header</a>
*/
public interface JoseHeaderNames {
public final class JoseHeaderNames {
/**
* {@code alg} - the algorithm header identifies the cryptographic algorithm used to secure a JWS or JWE
*/
String ALG = "alg";
public static final String ALG = "alg";
/**
* {@code jku} - the JWK Set URL header is a URI that refers to a resource for a set of JSON-encoded public keys,
* one of which corresponds to the key used to digitally sign a JWS or encrypt a JWE
*/
String JKU = "jku";
public static final String JKU = "jku";
/**
* {@code jwk} - the JSON Web Key header is the public key that corresponds to the key
* used to digitally sign a JWS or encrypt a JWE
*/
String JWK = "jwk";
public static final String JWK = "jwk";
/**
* {@code kid} - the key ID header is a hint indicating which key was used to secure a JWS or JWE
*/
String KID = "kid";
public static final String KID = "kid";
/**
* {@code x5u} - the X.509 URL header is a URI that refers to a resource for the X.509 public key certificate
* or certificate chain corresponding to the key used to digitally sign a JWS or encrypt a JWE
*/
String X5U = "x5u";
public static final String X5U = "x5u";
/**
* {@code x5c} - the X.509 certificate chain header contains the X.509 public key certificate
* or certificate chain corresponding to the key used to digitally sign a JWS or encrypt a JWE
*/
String X5C = "x5c";
public static final String X5C = "x5c";
/**
* {@code x5t} - the X.509 certificate SHA-1 thumbprint header is a base64url-encoded SHA-1 thumbprint (a.k.a. digest)
* of the DER encoding of the X.509 certificate corresponding to the key used to digitally sign a JWS or encrypt a JWE
*/
String X5T = "x5t";
public static final String X5T = "x5t";
/**
* {@code x5t#S256} - the X.509 certificate SHA-256 thumbprint header is a base64url-encoded SHA-256 thumbprint (a.k.a. digest)
* of the DER encoding of the X.509 certificate corresponding to the key used to digitally sign a JWS or encrypt a JWE
*/
String X5T_S256 = "x5t#S256";
public static final String X5T_S256 = "x5t#S256";
/**
* {@code typ} - the type header is used by JWS/JWE applications to declare the media type of a JWS/JWE
*/
String TYP = "typ";
public static final String TYP = "typ";
/**
* {@code cty} - the content type header is used by JWS/JWE applications to declare the media type
* of the secured content (the payload)
*/
String CTY = "cty";
public static final String CTY = "cty";
/**
* {@code crit} - the critical header indicates that extensions to the JWS/JWE/JWA specifications
* are being used that MUST be understood and processed
*/
String CRIT = "crit";
public static final String CRIT = "crit";
private JoseHeaderNames() {
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,22 +15,14 @@
*/
package org.springframework.security.oauth2.jwt;
import org.springframework.util.Assert;
import java.time.Instant;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import static org.springframework.security.oauth2.jwt.JwtClaimNames.AUD;
import static org.springframework.security.oauth2.jwt.JwtClaimNames.EXP;
import static org.springframework.security.oauth2.jwt.JwtClaimNames.IAT;
import static org.springframework.security.oauth2.jwt.JwtClaimNames.ISS;
import static org.springframework.security.oauth2.jwt.JwtClaimNames.JTI;
import static org.springframework.security.oauth2.jwt.JwtClaimNames.NBF;
import static org.springframework.security.oauth2.jwt.JwtClaimNames.SUB;
import org.springframework.util.Assert;
/**
* The {@link Jwt JWT} Claims Set is a JSON object representing the claims conveyed by a JSON Web Token.
@ -46,7 +38,7 @@ public final class JwtClaimsSet implements JwtClaimAccessor {
private final Map<String, Object> claims;
private JwtClaimsSet(Map<String, Object> claims) {
this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims));
this.claims = Collections.unmodifiableMap(new HashMap<>(claims));
}
@Override
@ -76,8 +68,8 @@ public final class JwtClaimsSet implements JwtClaimAccessor {
/**
* A builder for {@link JwtClaimsSet}.
*/
public static class Builder {
private final Map<String, Object> claims = new LinkedHashMap<>();
public static final class Builder {
private final Map<String, Object> claims = new HashMap<>();
private Builder() {
}
@ -94,7 +86,7 @@ public final class JwtClaimsSet implements JwtClaimAccessor {
* @return the {@link Builder}
*/
public Builder issuer(String issuer) {
return claim(ISS, issuer);
return claim(JwtClaimNames.ISS, issuer);
}
/**
@ -104,7 +96,7 @@ public final class JwtClaimsSet implements JwtClaimAccessor {
* @return the {@link Builder}
*/
public Builder subject(String subject) {
return claim(SUB, subject);
return claim(JwtClaimNames.SUB, subject);
}
/**
@ -114,7 +106,7 @@ public final class JwtClaimsSet implements JwtClaimAccessor {
* @return the {@link Builder}
*/
public Builder audience(List<String> audience) {
return claim(AUD, audience);
return claim(JwtClaimNames.AUD, audience);
}
/**
@ -125,7 +117,7 @@ public final class JwtClaimsSet implements JwtClaimAccessor {
* @return the {@link Builder}
*/
public Builder expiresAt(Instant expiresAt) {
return claim(EXP, expiresAt);
return claim(JwtClaimNames.EXP, expiresAt);
}
/**
@ -136,7 +128,7 @@ public final class JwtClaimsSet implements JwtClaimAccessor {
* @return the {@link Builder}
*/
public Builder notBefore(Instant notBefore) {
return claim(NBF, notBefore);
return claim(JwtClaimNames.NBF, notBefore);
}
/**
@ -146,7 +138,7 @@ public final class JwtClaimsSet implements JwtClaimAccessor {
* @return the {@link Builder}
*/
public Builder issuedAt(Instant issuedAt) {
return claim(IAT, issuedAt);
return claim(JwtClaimNames.IAT, issuedAt);
}
/**
@ -156,7 +148,7 @@ public final class JwtClaimsSet implements JwtClaimAccessor {
* @return the {@link Builder}
*/
public Builder id(String jti) {
return claim(JTI, jti);
return claim(JwtClaimNames.JTI, jti);
}
/**

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,8 +15,6 @@
*/
package org.springframework.security.oauth2.jwt;
import org.springframework.security.oauth2.jose.JoseHeader;
/**
* Implementations of this interface are responsible for encoding
* a JSON Web Token (JWT) to it's compact claims representation format.

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -13,54 +13,49 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.jose.jws;
package org.springframework.security.oauth2.jwt;
import java.net.URL;
import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JOSEObjectType;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.KeyLengthException;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jose.KeySourceException;
import com.nimbusds.jose.crypto.factories.DefaultJWSSignerFactory;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKMatcher;
import com.nimbusds.jose.jwk.JWKSelector;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jose.produce.JWSSignerFactory;
import com.nimbusds.jose.util.Base64;
import com.nimbusds.jose.util.Base64URL;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.crypto.key.AsymmetricKey;
import org.springframework.security.crypto.key.CryptoKey;
import org.springframework.security.crypto.key.CryptoKeySource;
import org.springframework.security.oauth2.jose.JoseHeader;
import org.springframework.security.oauth2.jose.JoseHeaderNames;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncodingException;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import javax.crypto.SecretKey;
import java.net.URI;
import java.net.URL;
import java.security.PrivateKey;
import java.time.Instant;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
/**
* An implementation of a {@link JwtEncoder} that encodes a JSON Web Token (JWT)
* using the JSON Web Signature (JWS) Compact Serialization format.
* The private/secret key used for signing the JWS is obtained
* from the {@link CryptoKeySource} supplied via the constructor.
* An implementation of a {@link JwtEncoder} that encodes a JSON Web Token (JWT) using the
* JSON Web Signature (JWS) Compact Serialization format. The private/secret key used for
* signing the JWS is supplied by the {@code com.nimbusds.jose.jwk.source.JWKSource}
* provided via the constructor.
*
* <p>
* <b>NOTE:</b> This implementation uses the Nimbus JOSE + JWT SDK.
@ -68,50 +63,46 @@ import java.util.stream.Collectors;
* @author Joe Grandja
* @since 0.0.1
* @see JwtEncoder
* @see CryptoKeySource
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519">JSON Web Token (JWT)</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515">JSON Web Signature (JWS)</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515#section-3.1">JWS Compact Serialization</a>
* @see <a target="_blank" href="https://connect2id.com/products/nimbus-jose-jwt">Nimbus JOSE + JWT SDK</a>
* @see com.nimbusds.jose.jwk.source.JWKSource
* @see com.nimbusds.jose.jwk.JWK
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519">JSON Web Token
* (JWT)</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515">JSON Web Signature
* (JWS)</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515#section-3.1">JWS
* Compact Serialization</a>
* @see <a target="_blank" href="https://connect2id.com/products/nimbus-jose-jwt">Nimbus
* JOSE + JWT SDK</a>
*/
public final class NimbusJwsEncoder implements JwtEncoder {
private static final String ENCODING_ERROR_MESSAGE_TEMPLATE =
"An error occurred while attempting to encode the Jwt: %s";
private static final String RSA_KEY_TYPE = "RSA";
private static final String EC_KEY_TYPE = "EC";
private static final Map<JwsAlgorithm, String> jcaKeyAlgorithmMappings = new HashMap<JwsAlgorithm, String>() {
{
put(MacAlgorithm.HS256, "HmacSHA256");
put(MacAlgorithm.HS384, "HmacSHA384");
put(MacAlgorithm.HS512, "HmacSHA512");
put(SignatureAlgorithm.RS256, RSA_KEY_TYPE);
put(SignatureAlgorithm.RS384, RSA_KEY_TYPE);
put(SignatureAlgorithm.RS512, RSA_KEY_TYPE);
put(SignatureAlgorithm.ES256, EC_KEY_TYPE);
put(SignatureAlgorithm.ES384, EC_KEY_TYPE);
put(SignatureAlgorithm.ES512, EC_KEY_TYPE);
}
private static final String ENCODING_ERROR_MESSAGE_TEMPLATE = "An error occurred while attempting to encode the Jwt: %s";
private static final Converter<JoseHeader, JWSHeader> JWS_HEADER_CONVERTER = new JwsHeaderConverter();
private static final Converter<JwtClaimsSet, JWTClaimsSet> JWT_CLAIMS_SET_CONVERTER = new JwtClaimsSetConverter();
private static final JWSSignerFactory JWS_SIGNER_FACTORY = new DefaultJWSSignerFactory();
private final Map<JWK, JWSSigner> jwsSigners = new ConcurrentHashMap<>();
private final JWKSource<SecurityContext> jwkSource;
private BiConsumer<JoseHeader.Builder, JwtClaimsSet.Builder> jwtCustomizer = (headers, claims) -> {
};
private static final Converter<JoseHeader, JWSHeader> jwsHeaderConverter = new JwsHeaderConverter();
private static final Converter<JwtClaimsSet, JWTClaimsSet> jwtClaimsSetConverter = new JwtClaimsSetConverter();
private final CryptoKeySource keySource;
private BiConsumer<JoseHeader.Builder, JwtClaimsSet.Builder> jwtCustomizer = (headers, claims) -> {};
/**
* Constructs a {@code NimbusJwsEncoder} using the provided parameters.
*
* @param keySource the source for cryptographic keys
* @param jwkSource the {@code com.nimbusds.jose.jwk.source.JWKSource}
*/
public NimbusJwsEncoder(CryptoKeySource keySource) {
Assert.notNull(keySource, "keySource cannot be null");
this.keySource = keySource;
public NimbusJwsEncoder(JWKSource<SecurityContext> jwkSource) {
Assert.notNull(jwkSource, "jwkSource cannot be null");
this.jwkSource = jwkSource;
}
/**
* Sets the {@link Jwt} customizer to be provided the
* {@link JoseHeader.Builder} and {@link JwtClaimsSet.Builder}
* allowing for further customizations.
*
* Sets the {@link Jwt} customizer to be provided the {@link JoseHeader.Builder} and
* {@link JwtClaimsSet.Builder} allowing for further customizations.
* @param jwtCustomizer the {@link Jwt} customizer to be provided the
* {@link JoseHeader.Builder} and {@link JwtClaimsSet.Builder}
*/
@ -125,78 +116,85 @@ public final class NimbusJwsEncoder implements JwtEncoder {
Assert.notNull(headers, "headers cannot be null");
Assert.notNull(claims, "claims cannot be null");
CryptoKey<?> cryptoKey = selectKey(headers);
if (cryptoKey == null) {
throw new JwtEncodingException(String.format(
ENCODING_ERROR_MESSAGE_TEMPLATE,
"Unsupported key for algorithm '" + headers.getJwsAlgorithm().getName() + "'"));
}
JWSSigner jwsSigner;
if (AsymmetricKey.class.isAssignableFrom(cryptoKey.getClass())) {
if (!cryptoKey.getAlgorithm().equals(RSA_KEY_TYPE)) {
throw new JwtEncodingException(String.format(
ENCODING_ERROR_MESSAGE_TEMPLATE,
"Unsupported key type '" + cryptoKey.getAlgorithm() + "'"));
}
PrivateKey privateKey = (PrivateKey) cryptoKey.getKey();
jwsSigner = new RSASSASigner(privateKey);
} else {
SecretKey secretKey = (SecretKey) cryptoKey.getKey();
try {
jwsSigner = new MACSigner(secretKey);
} catch (KeyLengthException ex) {
throw new JwtEncodingException(String.format(
ENCODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex);
}
}
// @formatter:off
JoseHeader.Builder headersBuilder = JoseHeader.from(headers)
.type(JOSEObjectType.JWT.getType())
.keyId(cryptoKey.getId());
.type(JOSEObjectType.JWT.getType());
JwtClaimsSet.Builder claimsBuilder = JwtClaimsSet.from(claims)
.id(UUID.randomUUID().toString());
// @formatter:on
this.jwtCustomizer.accept(headersBuilder, claimsBuilder);
headers = headersBuilder.build();
JWK jwk = selectJwk(headersBuilder);
if (jwk == null) {
throw new JwtEncodingException(
String.format(ENCODING_ERROR_MESSAGE_TEMPLATE, "Failed to select a JWK signing key"));
}
else if (!StringUtils.hasText(jwk.getKeyID())) {
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
"The \"kid\" (key ID) from the selected JWK cannot be empty"));
}
headers = headersBuilder.keyId(jwk.getKeyID()).build();
claims = claimsBuilder.build();
JWSHeader jwsHeader = jwsHeaderConverter.convert(headers);
JWTClaimsSet jwtClaimsSet = jwtClaimsSetConverter.convert(claims);
JWSHeader jwsHeader = JWS_HEADER_CONVERTER.convert(headers);
JWTClaimsSet jwtClaimsSet = JWT_CLAIMS_SET_CONVERTER.convert(claims);
SignedJWT signedJWT = new SignedJWT(jwsHeader, jwtClaimsSet);
JWSSigner jwsSigner = this.jwsSigners.computeIfAbsent(jwk, (key) -> {
try {
return JWS_SIGNER_FACTORY.createJWSSigner(key);
}
catch (JOSEException ex) {
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
"Failed to create a JWS Signer -> " + ex.getMessage()), ex);
}
});
SignedJWT signedJwt = new SignedJWT(jwsHeader, jwtClaimsSet);
try {
signedJWT.sign(jwsSigner);
} catch (JOSEException ex) {
throw new JwtEncodingException(String.format(
ENCODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex);
signedJwt.sign(jwsSigner);
}
String jws = signedJWT.serialize();
catch (JOSEException ex) {
throw new JwtEncodingException(
String.format(ENCODING_ERROR_MESSAGE_TEMPLATE, "Failed to sign the JWT -> " + ex.getMessage()), ex);
}
String jws = signedJwt.serialize();
return new Jwt(jws, claims.getIssuedAt(), claims.getExpiresAt(),
headers.getHeaders(), claims.getClaims());
return new Jwt(jws, claims.getIssuedAt(), claims.getExpiresAt(), headers.getHeaders(), claims.getClaims());
}
private CryptoKey<?> selectKey(JoseHeader headers) {
JwsAlgorithm jwsAlgorithm = headers.getJwsAlgorithm();
String keyAlgorithm = jcaKeyAlgorithmMappings.get(jwsAlgorithm);
if (!StringUtils.hasText(keyAlgorithm)) {
return null;
private JWK selectJwk(JoseHeader.Builder headersBuilder) {
final AtomicReference<JWSAlgorithm> jwsAlgorithm = new AtomicReference<>();
headersBuilder.headers((h) -> {
JwsAlgorithm jwsAlg = (JwsAlgorithm) h.get(JoseHeaderNames.ALG);
jwsAlgorithm.set(JWSAlgorithm.parse(jwsAlg.getName()));
});
JWSHeader jwsHeader = new JWSHeader(jwsAlgorithm.get());
JWKSelector jwkSelector = new JWKSelector(JWKMatcher.forJWSHeader(jwsHeader));
List<JWK> jwks;
try {
jwks = this.jwkSource.get(jwkSelector, null);
}
catch (KeySourceException ex) {
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
"Failed to select a JWK signing key -> " + ex.getMessage()), ex);
}
return this.keySource.getKeys().stream()
.filter(key -> key.getAlgorithm().equals(keyAlgorithm))
.findFirst()
.orElse(null);
if (jwks.size() > 1) {
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
"Found multiple JWK signing keys for algorithm '" + jwsAlgorithm.get().getName() + "'"));
}
return !jwks.isEmpty() ? jwks.get(0) : null;
}
private static class JwsHeaderConverter implements Converter<JoseHeader, JWSHeader> {
@Override
public JWSHeader convert(JoseHeader headers) {
JWSHeader.Builder builder = new JWSHeader.Builder(
JWSAlgorithm.parse(headers.getJwsAlgorithm().getName()));
JWSHeader.Builder builder = new JWSHeader.Builder(JWSAlgorithm.parse(headers.getJwsAlgorithm().getName()));
Set<String> critical = headers.getCritical();
if (!CollectionUtils.isEmpty(critical)) {
@ -208,14 +206,14 @@ public final class NimbusJwsEncoder implements JwtEncoder {
builder.contentType(contentType);
}
String jwkSetUri = headers.getJwkSetUri();
if (StringUtils.hasText(jwkSetUri)) {
URL jwkSetUri = headers.getJwkSetUri();
if (jwkSetUri != null) {
try {
builder.jwkURL(new URI(jwkSetUri));
} catch (Exception ex) {
throw new JwtEncodingException(String.format(
ENCODING_ERROR_MESSAGE_TEMPLATE,
"Failed to convert '" + JoseHeaderNames.JKU + "' JOSE header"), ex);
builder.jwkURL(jwkSetUri.toURI());
}
catch (Exception ex) {
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
"Failed to convert '" + JoseHeaderNames.JKU + "' JOSE header to a URI"), ex);
}
}
@ -223,9 +221,9 @@ public final class NimbusJwsEncoder implements JwtEncoder {
if (!CollectionUtils.isEmpty(jwk)) {
try {
builder.jwk(JWK.parse(jwk));
} catch (Exception ex) {
throw new JwtEncodingException(String.format(
ENCODING_ERROR_MESSAGE_TEMPLATE,
}
catch (Exception ex) {
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
"Failed to convert '" + JoseHeaderNames.JWK + "' JOSE header"), ex);
}
}
@ -242,10 +240,7 @@ public final class NimbusJwsEncoder implements JwtEncoder {
List<String> x509CertificateChain = headers.getX509CertificateChain();
if (!CollectionUtils.isEmpty(x509CertificateChain)) {
builder.x509CertChain(
x509CertificateChain.stream()
.map(Base64::new)
.collect(Collectors.toList()));
builder.x509CertChain(x509CertificateChain.stream().map(Base64::new).collect(Collectors.toList()));
}
String x509SHA1Thumbprint = headers.getX509SHA1Thumbprint();
@ -258,19 +253,19 @@ public final class NimbusJwsEncoder implements JwtEncoder {
builder.x509CertSHA256Thumbprint(new Base64URL(x509SHA256Thumbprint));
}
String x509Uri = headers.getX509Uri();
if (StringUtils.hasText(x509Uri)) {
URL x509Uri = headers.getX509Uri();
if (x509Uri != null) {
try {
builder.x509CertURL(new URI(x509Uri));
} catch (Exception ex) {
throw new JwtEncodingException(String.format(
ENCODING_ERROR_MESSAGE_TEMPLATE,
"Failed to convert '" + JoseHeaderNames.X5U + "' JOSE header"), ex);
builder.x509CertURL(x509Uri.toURI());
}
catch (Exception ex) {
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
"Failed to convert '" + JoseHeaderNames.X5U + "' JOSE header to a URI"), ex);
}
}
Map<String, Object> customHeaders = headers.getHeaders().entrySet().stream()
.filter(header -> !JWSHeader.getRegisteredParameterNames().contains(header.getKey()))
.filter((header) -> !JWSHeader.getRegisteredParameterNames().contains(header.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
if (!CollectionUtils.isEmpty(customHeaders)) {
builder.customParams(customHeaders);
@ -278,6 +273,7 @@ public final class NimbusJwsEncoder implements JwtEncoder {
return builder.build();
}
}
private static class JwtClaimsSetConverter implements Converter<JwtClaimsSet, JWTClaimsSet> {
@ -322,7 +318,7 @@ public final class NimbusJwsEncoder implements JwtEncoder {
}
Map<String, Object> customClaims = claims.getClaims().entrySet().stream()
.filter(claim -> !JWTClaimsSet.getRegisteredNames().contains(claim.getKey()))
.filter((claim) -> !JWTClaimsSet.getRegisteredNames().contains(claim.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
if (!CollectionUtils.isEmpty(customClaims)) {
customClaims.forEach(builder::claim);
@ -330,5 +326,7 @@ public final class NimbusJwsEncoder implements JwtEncoder {
return builder.build();
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -21,7 +21,7 @@ import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken2;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
import org.springframework.security.oauth2.jose.JoseHeader;
import org.springframework.security.oauth2.jwt.JoseHeader;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,70 +15,65 @@
*/
package org.springframework.security.oauth2.server.authorization.web;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.Curve;
import com.nimbusds.jose.jwk.ECKey;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.KeyUse;
import com.nimbusds.jose.jwk.RSAKey;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.security.crypto.key.AsymmetricKey;
import org.springframework.security.crypto.key.CryptoKeySource;
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 java.io.IOException;
import java.io.Writer;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Writer;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Objects;
import java.util.stream.Collectors;
import com.nimbusds.jose.jwk.JWKMatcher;
import com.nimbusds.jose.jwk.JWKSelector;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
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;
/**
* A {@code Filter} that processes JWK Set requests.
*
* @author Joe Grandja
* @since 0.0.1
* @see CryptoKeySource
* @see com.nimbusds.jose.jwk.source.JWKSource
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7517">JSON Web Key (JWK)</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7517#section-5">Section 5 JWK Set Format</a>
*/
public class JwkSetEndpointFilter extends OncePerRequestFilter {
public class NimbusJwkSetEndpointFilter extends OncePerRequestFilter {
/**
* The default endpoint {@code URI} for JWK Set requests.
*/
public static final String DEFAULT_JWK_SET_ENDPOINT_URI = "/oauth2/jwks";
private final CryptoKeySource keySource;
private final JWKSource<SecurityContext> jwkSource;
private final JWKSelector jwkSelector;
private final RequestMatcher requestMatcher;
/**
* Constructs a {@code JwkSetEndpointFilter} using the provided parameters.
*
* @param keySource the source for cryptographic keys
* Constructs a {@code NimbusJwkSetEndpointFilter} using the provided parameters.
* @param jwkSource the {@code com.nimbusds.jose.jwk.source.JWKSource}
*/
public JwkSetEndpointFilter(CryptoKeySource keySource) {
this(keySource, DEFAULT_JWK_SET_ENDPOINT_URI);
public NimbusJwkSetEndpointFilter(JWKSource<SecurityContext> jwkSource) {
this(jwkSource, DEFAULT_JWK_SET_ENDPOINT_URI);
}
/**
* Constructs a {@code JwkSetEndpointFilter} using the provided parameters.
* Constructs a {@code NimbusJwkSetEndpointFilter} using the provided parameters.
*
* @param keySource the source for cryptographic keys
* @param jwkSource the {@code com.nimbusds.jose.jwk.source.JWKSource}
* @param jwkSetEndpointUri the endpoint {@code URI} for JWK Set requests
*/
public JwkSetEndpointFilter(CryptoKeySource keySource, String jwkSetEndpointUri) {
Assert.notNull(keySource, "keySource cannot be null");
public NimbusJwkSetEndpointFilter(JWKSource<SecurityContext> jwkSource, String jwkSetEndpointUri) {
Assert.notNull(jwkSource, "jwkSource cannot be null");
Assert.hasText(jwkSetEndpointUri, "jwkSetEndpointUri cannot be empty");
this.keySource = keySource;
this.jwkSource = jwkSource;
this.jwkSelector = new JWKSelector(new JWKMatcher.Builder().publicOnly(true).build());
this.requestMatcher = new AntPathRequestMatcher(jwkSetEndpointUri, HttpMethod.GET.name());
}
@ -91,43 +86,17 @@ public class JwkSetEndpointFilter extends OncePerRequestFilter {
return;
}
JWKSet jwkSet = buildJwkSet();
JWKSet jwkSet;
try {
jwkSet = new JWKSet(this.jwkSource.get(this.jwkSelector, null));
}
catch (Exception ex) {
throw new IllegalStateException("Failed to select the JWK public key(s) -> " + ex.getMessage(), ex);
}
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
try (Writer writer = response.getWriter()) {
writer.write(jwkSet.toString());
}
}
private JWKSet buildJwkSet() {
return new JWKSet(
this.keySource.getKeys().stream()
.filter(key -> AsymmetricKey.class.isAssignableFrom(key.getClass()))
.map(AsymmetricKey.class::cast)
.map(this::convert)
.filter(Objects::nonNull)
.collect(Collectors.toList())
);
}
private JWK convert(AsymmetricKey asymmetricKey) {
JWK jwk = null;
if (asymmetricKey.getPublicKey() instanceof RSAPublicKey) {
RSAPublicKey publicKey = (RSAPublicKey) asymmetricKey.getPublicKey();
jwk = new RSAKey.Builder(publicKey)
.keyUse(KeyUse.SIGNATURE)
.algorithm(JWSAlgorithm.RS256)
.keyID(asymmetricKey.getId())
.build();
} else if (asymmetricKey.getPublicKey() instanceof ECPublicKey) {
ECPublicKey publicKey = (ECPublicKey) asymmetricKey.getPublicKey();
Curve curve = Curve.forECParameterSpec(publicKey.getParams());
jwk = new ECKey.Builder(curve, publicKey)
.keyUse(KeyUse.SIGNATURE)
.algorithm(JWSAlgorithm.ES256)
.keyID(asymmetricKey.getId())
.build();
}
return jwk;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,11 +15,20 @@
*/
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.function.BiConsumer;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
@ -27,16 +36,15 @@ import org.springframework.http.HttpHeaders;
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.crypto.key.CryptoKeySource;
import org.springframework.security.crypto.key.StaticKeyGeneratingCryptoKeySource;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
import org.springframework.security.oauth2.jose.JoseHeader;
import org.springframework.security.oauth2.jose.jws.NimbusJwsEncoder;
import org.springframework.security.oauth2.jose.TestJwks;
import org.springframework.security.oauth2.jwt.JoseHeader;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwsEncoder;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
@ -54,11 +62,6 @@ import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.function.BiConsumer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.CoreMatchers.containsString;
import static org.mockito.ArgumentMatchers.any;
@ -90,7 +93,7 @@ public class OAuth2AuthorizationCodeGrantTests {
private static RegisteredClientRepository registeredClientRepository;
private static OAuth2AuthorizationService authorizationService;
private static CryptoKeySource keySource;
private static JWKSource<SecurityContext> jwkSource;
private static NimbusJwsEncoder jwtEncoder;
private static BiConsumer<JoseHeader.Builder, JwtClaimsSet.Builder> jwtCustomizer;
@ -104,8 +107,9 @@ public class OAuth2AuthorizationCodeGrantTests {
public static void init() {
registeredClientRepository = mock(RegisteredClientRepository.class);
authorizationService = mock(OAuth2AuthorizationService.class);
keySource = new StaticKeyGeneratingCryptoKeySource();
jwtEncoder = new NimbusJwsEncoder(keySource);
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
jwtEncoder = new NimbusJwsEncoder(jwkSource);
jwtCustomizer = mock(BiConsumer.class);
jwtEncoder.setJwtCustomizer(jwtCustomizer);
}
@ -298,8 +302,8 @@ public class OAuth2AuthorizationCodeGrantTests {
}
@Bean
CryptoKeySource keySource() {
return keySource;
JWKSource<SecurityContext> jwkSource() {
return jwkSource;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,10 +15,18 @@
*/
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
@ -26,10 +34,9 @@ import org.springframework.http.HttpHeaders;
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.crypto.key.CryptoKeySource;
import org.springframework.security.crypto.key.StaticKeyGeneratingCryptoKeySource;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.jose.TestJwks;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
@ -38,10 +45,6 @@ import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenE
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
@ -61,7 +64,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
public class OAuth2ClientCredentialsGrantTests {
private static RegisteredClientRepository registeredClientRepository;
private static OAuth2AuthorizationService authorizationService;
private static CryptoKeySource keySource;
private static JWKSource<SecurityContext> jwkSource;
@Rule
public final SpringTestRule spring = new SpringTestRule();
@ -73,7 +76,8 @@ public class OAuth2ClientCredentialsGrantTests {
public static void init() {
registeredClientRepository = mock(RegisteredClientRepository.class);
authorizationService = mock(OAuth2AuthorizationService.class);
keySource = new StaticKeyGeneratingCryptoKeySource();
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
@Before
@ -159,8 +163,8 @@ public class OAuth2ClientCredentialsGrantTests {
}
@Bean
CryptoKeySource keySource() {
return keySource;
JWKSource<SecurityContext> jwkSource() {
return jwkSource;
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,10 +15,18 @@
*/
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
@ -26,10 +34,9 @@ import org.springframework.http.HttpHeaders;
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.crypto.key.CryptoKeySource;
import org.springframework.security.crypto.key.StaticKeyGeneratingCryptoKeySource;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.jose.TestJwks;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
@ -42,10 +49,6 @@ import org.springframework.test.web.servlet.MockMvc;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import static org.hamcrest.CoreMatchers.containsString;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
@ -67,7 +70,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
public class OAuth2RefreshTokenGrantTests {
private static RegisteredClientRepository registeredClientRepository;
private static OAuth2AuthorizationService authorizationService;
private static CryptoKeySource keySource;
private static JWKSource<SecurityContext> jwkSource;
@Rule
public final SpringTestRule spring = new SpringTestRule();
@ -79,7 +82,8 @@ public class OAuth2RefreshTokenGrantTests {
public static void init() {
registeredClientRepository = mock(RegisteredClientRepository.class);
authorizationService = mock(OAuth2AuthorizationService.class);
keySource = new StaticKeyGeneratingCryptoKeySource();
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
@Before
@ -153,8 +157,8 @@ public class OAuth2RefreshTokenGrantTests {
}
@Bean
CryptoKeySource keySource() {
return keySource;
JWKSource<SecurityContext> jwkSource() {
return jwkSource;
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,11 +15,19 @@
*/
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
@ -27,12 +35,11 @@ import org.springframework.http.HttpHeaders;
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.crypto.key.CryptoKeySource;
import org.springframework.security.crypto.key.StaticKeyGeneratingCryptoKeySource;
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames2;
import org.springframework.security.oauth2.jose.TestJwks;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
@ -46,10 +53,6 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
@ -66,7 +69,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
public class OAuth2TokenRevocationTests {
private static RegisteredClientRepository registeredClientRepository;
private static OAuth2AuthorizationService authorizationService;
private static CryptoKeySource keySource;
private static JWKSource<SecurityContext> jwkSource;
@Rule
public final SpringTestRule spring = new SpringTestRule();
@ -78,7 +81,8 @@ public class OAuth2TokenRevocationTests {
public static void init() {
registeredClientRepository = mock(RegisteredClientRepository.class);
authorizationService = mock(OAuth2AuthorizationService.class);
keySource = new StaticKeyGeneratingCryptoKeySource();
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
@Before
@ -181,8 +185,8 @@ public class OAuth2TokenRevocationTests {
}
@Bean
CryptoKeySource keySource() {
return keySource;
JWKSource<SecurityContext> jwkSource() {
return jwkSource;
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,11 +15,19 @@
*/
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
@ -27,12 +35,11 @@ import org.springframework.http.HttpHeaders;
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.crypto.key.CryptoKeySource;
import org.springframework.security.crypto.key.StaticKeyGeneratingCryptoKeySource;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.jose.TestJwks;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.TokenType;
@ -40,20 +47,16 @@ import org.springframework.security.oauth2.server.authorization.client.Registere
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcProviderConfigurationEndpointFilter;
import org.springframework.security.oauth2.server.authorization.token.OAuth2AuthorizationCode;
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcProviderConfigurationEndpointFilter;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.hamcrest.CoreMatchers.containsString;
@ -80,7 +83,7 @@ public class OidcTests {
private static final String issuerUrl = "https://example.com/issuer1";
private static RegisteredClientRepository registeredClientRepository;
private static OAuth2AuthorizationService authorizationService;
private static CryptoKeySource keySource;
private static JWKSource<SecurityContext> jwkSource;
@Rule
public final SpringTestRule spring = new SpringTestRule();
@ -92,7 +95,8 @@ public class OidcTests {
public static void init() {
registeredClientRepository = mock(RegisteredClientRepository.class);
authorizationService = mock(OAuth2AuthorizationService.class);
keySource = new StaticKeyGeneratingCryptoKeySource();
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
@Before
@ -224,8 +228,8 @@ public class OidcTests {
}
@Bean
CryptoKeySource keySource() {
return keySource;
JWKSource<SecurityContext> jwkSource() {
return jwkSource;
}
}

View File

@ -1,113 +0,0 @@
/*
* 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.crypto.key;
import org.junit.BeforeClass;
import org.junit.Test;
import javax.crypto.SecretKey;
import java.security.KeyPair;
import java.util.HashMap;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.springframework.security.crypto.key.KeyGeneratorUtils.generateRsaKey;
import static org.springframework.security.crypto.key.KeyGeneratorUtils.generateSecretKey;
/**
* Tests for {@link CryptoKey}.
*
* @author Joe Grandja
*/
public class CryptoKeyTests {
private static SecretKey secretKey;
private static KeyPair rsaKeyPair;
@BeforeClass
public static void init() {
secretKey = generateSecretKey();
rsaKeyPair = generateRsaKey();
}
@Test
public void symmetricWhenSecretKeyNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> CryptoKey.symmetric(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("key cannot be null");
}
@Test
public void metadataWhenNameNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> CryptoKey.symmetric(secretKey).metadata(null, "value"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("name cannot be empty");
}
@Test
public void metadataWhenValueNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> CryptoKey.symmetric(secretKey).metadata("name", null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("value cannot be null");
}
@Test
public void symmetricWhenAllAttributesProvidedThenAllAttributesAreSet() {
Map<String, Object> keyMetadata = new HashMap<>();
keyMetadata.put("name1", "value1");
keyMetadata.put("name2", "value2");
SymmetricKey symmetricKey = CryptoKey.symmetric(secretKey)
.id("id")
.metadata(metadata -> metadata.putAll(keyMetadata))
.build();
assertThat(symmetricKey.getKey()).isEqualTo(secretKey);
assertThat(symmetricKey.getId()).isEqualTo("id");
assertThat(symmetricKey.getMetadata()).isEqualTo(keyMetadata);
}
@Test
public void asymmetricWhenPrivateKeyNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> CryptoKey.asymmetric(null, rsaKeyPair.getPublic()))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("key cannot be null");
}
@Test
public void asymmetricWhenPublicKeyNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> CryptoKey.asymmetric(rsaKeyPair.getPrivate(), null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("publicKey cannot be null");
}
@Test
public void asymmetricWhenAllAttributesProvidedThenAllAttributesAreSet() {
Map<String, Object> keyMetadata = new HashMap<>();
keyMetadata.put("name1", "value1");
keyMetadata.put("name2", "value2");
AsymmetricKey asymmetricKey = CryptoKey.asymmetric(rsaKeyPair.getPrivate(), rsaKeyPair.getPublic())
.id("id")
.metadata(metadata -> metadata.putAll(keyMetadata))
.build();
assertThat(asymmetricKey.getKey()).isEqualTo(rsaKeyPair.getPrivate());
assertThat(asymmetricKey.getPublicKey()).isEqualTo(rsaKeyPair.getPublic());
assertThat(asymmetricKey.getId()).isEqualTo("id");
assertThat(asymmetricKey.getMetadata()).isEqualTo(keyMetadata);
}
}

View File

@ -1,42 +0,0 @@
/*
* 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.crypto.key;
import java.security.KeyPair;
import static org.springframework.security.crypto.key.KeyGeneratorUtils.generateEcKey;
import static org.springframework.security.crypto.key.KeyGeneratorUtils.generateRsaKey;
import static org.springframework.security.crypto.key.KeyGeneratorUtils.generateSecretKey;
/**
* @author Joe Grandja
*/
public class TestCryptoKeys {
public static SymmetricKey.Builder secretKey() {
return CryptoKey.symmetric(generateSecretKey());
}
public static AsymmetricKey.Builder rsaKey() {
KeyPair rsaKeyPair = generateRsaKey();
return CryptoKey.asymmetric(rsaKeyPair.getPrivate(), rsaKeyPair.getPublic());
}
public static AsymmetricKey.Builder ecKey() {
KeyPair ecKeyPair = generateEcKey();
return CryptoKey.asymmetric(ecKeyPair.getPrivate(), ecKeyPair.getPublic());
}
}

View File

@ -0,0 +1,89 @@
/*
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.jose;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import javax.crypto.SecretKey;
import com.nimbusds.jose.jwk.Curve;
import com.nimbusds.jose.jwk.ECKey;
import com.nimbusds.jose.jwk.KeyUse;
import com.nimbusds.jose.jwk.OctetSequenceKey;
import com.nimbusds.jose.jwk.RSAKey;
/**
* @author Joe Grandja
*/
public final class TestJwks {
// @formatter:off
public static final RSAKey DEFAULT_RSA_JWK =
jwk(
TestKeys.DEFAULT_PUBLIC_KEY,
TestKeys.DEFAULT_PRIVATE_KEY
).build();
// @formatter:on
// @formatter:off
public static final ECKey DEFAULT_EC_JWK =
jwk(
(ECPublicKey) TestKeys.DEFAULT_EC_KEY_PAIR.getPublic(),
(ECPrivateKey) TestKeys.DEFAULT_EC_KEY_PAIR.getPrivate()
).build();
// @formatter:on
// @formatter:off
public static final OctetSequenceKey DEFAULT_SECRET_JWK =
jwk(
TestKeys.DEFAULT_SECRET_KEY
).build();
// @formatter:on
private TestJwks() {
}
public static RSAKey.Builder jwk(RSAPublicKey publicKey, RSAPrivateKey privateKey) {
// @formatter:off
return new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyUse(KeyUse.SIGNATURE)
.keyID("rsa-jwk-kid");
// @formatter:on
}
public static ECKey.Builder jwk(ECPublicKey publicKey, ECPrivateKey privateKey) {
// @formatter:off
Curve curve = Curve.forECParameterSpec(publicKey.getParams());
return new ECKey.Builder(curve, publicKey)
.privateKey(privateKey)
.keyUse(KeyUse.SIGNATURE)
.keyID("ec-jwk-kid");
// @formatter:on
}
public static OctetSequenceKey.Builder jwk(SecretKey secretKey) {
// @formatter:off
return new OctetSequenceKey.Builder(secretKey)
.keyUse(KeyUse.SIGNATURE)
.keyID("secret-jwk-kid");
// @formatter:on
}
}

View File

@ -0,0 +1,154 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.jose;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.ECFieldFp;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPoint;
import java.security.spec.EllipticCurve;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
/**
* TODO
* This class is a straight copy from Spring Security.
* It should be removed when merging this codebase into Spring Security.
*
* @author Joe Grandja
* @since 5.2
*/
public final class TestKeys {
public static final KeyFactory kf;
static {
try {
kf = KeyFactory.getInstance("RSA");
}
catch (NoSuchAlgorithmException ex) {
throw new IllegalStateException(ex);
}
}
public static final String DEFAULT_ENCODED_SECRET_KEY = "bCzY/M48bbkwBEWjmNSIEPfwApcvXOnkCxORBEbPr+4=";
public static final SecretKey DEFAULT_SECRET_KEY = new SecretKeySpec(
Base64.getDecoder().decode(DEFAULT_ENCODED_SECRET_KEY), "AES");
// @formatter:off
public static final String DEFAULT_RSA_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3FlqJr5TRskIQIgdE3Dd"
+ "7D9lboWdcTUT8a+fJR7MAvQm7XXNoYkm3v7MQL1NYtDvL2l8CAnc0WdSTINU6IRv"
+ "c5Kqo2Q4csNX9SHOmEfzoROjQqahEcve1jBXluoCXdYuYpx4/1tfRgG6ii4Uhxh6"
+ "iI8qNMJQX+fLfqhbfYfxBQVRPywBkAbIP4x1EAsbC6FSNmkhCxiMNqEgxaIpY8C2"
+ "kJdJ/ZIV+WW4noDdzpKqHcwmB8FsrumlVY/DNVvUSDIipiq9PbP4H99TXN1o746o"
+ "RaNa07rq1hoCgMSSy+85SagCoxlmyE+D+of9SsMY8Ol9t0rdzpobBuhyJ/o5dfvj"
+ "KwIDAQAB";
// @formatter:on
public static final RSAPublicKey DEFAULT_PUBLIC_KEY;
static {
X509EncodedKeySpec spec = new X509EncodedKeySpec(Base64.getDecoder().decode(DEFAULT_RSA_PUBLIC_KEY));
try {
DEFAULT_PUBLIC_KEY = (RSAPublicKey) kf.generatePublic(spec);
}
catch (InvalidKeySpecException ex) {
throw new IllegalArgumentException(ex);
}
}
// @formatter:off
public static final String DEFAULT_RSA_PRIVATE_KEY = "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDcWWomvlNGyQhA"
+ "iB0TcN3sP2VuhZ1xNRPxr58lHswC9Cbtdc2hiSbe/sxAvU1i0O8vaXwICdzRZ1JM"
+ "g1TohG9zkqqjZDhyw1f1Ic6YR/OhE6NCpqERy97WMFeW6gJd1i5inHj/W19GAbqK"
+ "LhSHGHqIjyo0wlBf58t+qFt9h/EFBVE/LAGQBsg/jHUQCxsLoVI2aSELGIw2oSDF"
+ "oiljwLaQl0n9khX5ZbiegN3OkqodzCYHwWyu6aVVj8M1W9RIMiKmKr09s/gf31Nc"
+ "3WjvjqhFo1rTuurWGgKAxJLL7zlJqAKjGWbIT4P6h/1Kwxjw6X23St3OmhsG6HIn"
+ "+jl1++MrAgMBAAECggEBAMf820wop3pyUOwI3aLcaH7YFx5VZMzvqJdNlvpg1jbE"
+ "E2Sn66b1zPLNfOIxLcBG8x8r9Ody1Bi2Vsqc0/5o3KKfdgHvnxAB3Z3dPh2WCDek"
+ "lCOVClEVoLzziTuuTdGO5/CWJXdWHcVzIjPxmK34eJXioiLaTYqN3XKqKMdpD0ZG"
+ "mtNTGvGf+9fQ4i94t0WqIxpMpGt7NM4RHy3+Onggev0zLiDANC23mWrTsUgect/7"
+ "62TYg8g1bKwLAb9wCBT+BiOuCc2wrArRLOJgUkj/F4/gtrR9ima34SvWUyoUaKA0"
+ "bi4YBX9l8oJwFGHbU9uFGEMnH0T/V0KtIB7qetReywkCgYEA9cFyfBIQrYISV/OA"
+ "+Z0bo3vh2aL0QgKrSXZ924cLt7itQAHNZ2ya+e3JRlTczi5mnWfjPWZ6eJB/8MlH"
+ "Gpn12o/POEkU+XjZZSPe1RWGt5g0S3lWqyx9toCS9ACXcN9tGbaqcFSVI73zVTRA"
+ "8J9grR0fbGn7jaTlTX2tnlOTQ60CgYEA5YjYpEq4L8UUMFkuj+BsS3u0oEBnzuHd"
+ "I9LEHmN+CMPosvabQu5wkJXLuqo2TxRnAznsA8R3pCLkdPGoWMCiWRAsCn979TdY"
+ "QbqO2qvBAD2Q19GtY7lIu6C35/enQWzJUMQE3WW0OvjLzZ0l/9mA2FBRR+3F9A1d"
+ "rBdnmv0c3TcCgYEAi2i+ggVZcqPbtgrLOk5WVGo9F1GqUBvlgNn30WWNTx4zIaEk"
+ "HSxtyaOLTxtq2odV7Kr3LGiKxwPpn/T+Ief+oIp92YcTn+VfJVGw4Z3BezqbR8lA"
+ "Uf/+HF5ZfpMrVXtZD4Igs3I33Duv4sCuqhEvLWTc44pHifVloozNxYfRfU0CgYBN"
+ "HXa7a6cJ1Yp829l62QlJKtx6Ymj95oAnQu5Ez2ROiZMqXRO4nucOjGUP55Orac1a"
+ "FiGm+mC/skFS0MWgW8evaHGDbWU180wheQ35hW6oKAb7myRHtr4q20ouEtQMdQIF"
+ "snV39G1iyqeeAsf7dxWElydXpRi2b68i3BIgzhzebQKBgQCdUQuTsqV9y/JFpu6H"
+ "c5TVvhG/ubfBspI5DhQqIGijnVBzFT//UfIYMSKJo75qqBEyP2EJSmCsunWsAFsM"
+ "TszuiGTkrKcZy9G0wJqPztZZl2F2+bJgnA6nBEV7g5PA4Af+QSmaIhRwqGDAuROR"
+ "47jndeyIaMTNETEmOnms+as17g==";
// @formatter:on
public static final RSAPrivateKey DEFAULT_PRIVATE_KEY;
static {
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(DEFAULT_RSA_PRIVATE_KEY));
try {
DEFAULT_PRIVATE_KEY = (RSAPrivateKey) kf.generatePrivate(spec);
}
catch (InvalidKeySpecException ex) {
throw new IllegalArgumentException(ex);
}
}
public static final KeyPair DEFAULT_RSA_KEY_PAIR = new KeyPair(DEFAULT_PUBLIC_KEY, DEFAULT_PRIVATE_KEY);
public static final KeyPair DEFAULT_EC_KEY_PAIR = generateEcKeyPair();
static KeyPair generateEcKeyPair() {
EllipticCurve ellipticCurve = new EllipticCurve(
new ECFieldFp(new BigInteger(
"115792089210356248762697446949407573530086143415290314195533631308867097853951")),
new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853948"),
new BigInteger("41058363725152142129326129780047268409114441015993725554835256314039467401291"));
ECPoint ecPoint = new ECPoint(
new BigInteger("48439561293906451759052585252797914202762949526041747995844080717082404635286"),
new BigInteger("36134250956749795798585127919587881956611106672985015071877198253568414405109"));
ECParameterSpec ecParameterSpec = new ECParameterSpec(ellipticCurve, ecPoint,
new BigInteger("115792089210356248762697446949407573529996955224135760342422259061068512044369"), 1);
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
keyPairGenerator.initialize(ecParameterSpec);
keyPair = keyPairGenerator.generateKeyPair();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
private TestKeys() {
}
}

View File

@ -1,177 +0,0 @@
/*
* 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.jose.jws;
import org.junit.Before;
import org.junit.Test;
import org.springframework.security.crypto.key.AsymmetricKey;
import org.springframework.security.crypto.key.CryptoKeySource;
import org.springframework.security.crypto.key.TestCryptoKeys;
import org.springframework.security.oauth2.jose.JoseHeader;
import org.springframework.security.oauth2.jose.JoseHeaderNames;
import org.springframework.security.oauth2.jose.TestJoseHeaders;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncodingException;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.TestJwtClaimsSets;
import java.security.interfaces.RSAPublicKey;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Tests for {@link NimbusJwsEncoder}.
*
* @author Joe Grandja
*/
public class NimbusJwsEncoderTests {
private CryptoKeySource keySource;
private NimbusJwsEncoder jwtEncoder;
@Before
public void setUp() {
this.keySource = mock(CryptoKeySource.class);
this.jwtEncoder = new NimbusJwsEncoder(this.keySource);
}
@Test
public void constructorWhenKeySourceNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new NimbusJwsEncoder(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("keySource cannot be null");
}
@Test
public void setJwtCustomizerWhenNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> this.jwtEncoder.setJwtCustomizer(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("jwtCustomizer cannot be null");
}
@Test
public void encodeWhenHeadersNullThenThrowIllegalArgumentException() {
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
assertThatThrownBy(() -> this.jwtEncoder.encode(null, jwtClaimsSet))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("headers cannot be null");
}
@Test
public void encodeWhenClaimsNullThenThrowIllegalArgumentException() {
JoseHeader joseHeader = TestJoseHeaders.joseHeader().build();
assertThatThrownBy(() -> this.jwtEncoder.encode(joseHeader, null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("claims cannot be null");
}
@Test
public void encodeWhenUnsupportedKeyThenThrowJwtEncodingException() {
JoseHeader joseHeader = TestJoseHeaders.joseHeader().build();
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
assertThatThrownBy(() -> this.jwtEncoder.encode(joseHeader, jwtClaimsSet))
.isInstanceOf(JwtEncodingException.class)
.hasMessageContaining("Unsupported key for algorithm 'RS256'");
}
@Test
public void encodeWhenUnsupportedKeyAlgorithmThenThrowJwtEncodingException() {
JoseHeader joseHeader = TestJoseHeaders.joseHeader(SignatureAlgorithm.ES256).build();
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
assertThatThrownBy(() -> this.jwtEncoder.encode(joseHeader, jwtClaimsSet))
.isInstanceOf(JwtEncodingException.class)
.hasMessageContaining("Unsupported key for algorithm 'ES256'");
}
@Test
public void encodeWhenUnsupportedKeyTypeThenThrowJwtEncodingException() {
AsymmetricKey ecKey = TestCryptoKeys.ecKey().build();
when(this.keySource.getKeys()).thenReturn(Collections.singleton(ecKey));
JoseHeader joseHeader = TestJoseHeaders.joseHeader(SignatureAlgorithm.ES256).build();
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
assertThatThrownBy(() -> this.jwtEncoder.encode(joseHeader, jwtClaimsSet))
.isInstanceOf(JwtEncodingException.class)
.hasMessageContaining("Unsupported key type 'EC'");
}
@Test
public void encodeWhenSuccessThenDecodes() {
AsymmetricKey rsaKey = TestCryptoKeys.rsaKey().build();
when(this.keySource.getKeys()).thenReturn(Collections.singleton(rsaKey));
JoseHeader joseHeader = TestJoseHeaders.joseHeader()
.headers(headers -> headers.remove(JoseHeaderNames.CRIT))
.build();
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
Jwt jws = this.jwtEncoder.encode(joseHeader, jwtClaimsSet);
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey((RSAPublicKey) rsaKey.getPublicKey()).build();
jwtDecoder.decode(jws.getTokenValue());
}
@Test
public void encodeWhenCustomizerSetThenCalled() {
AsymmetricKey rsaKey = TestCryptoKeys.rsaKey().build();
when(this.keySource.getKeys()).thenReturn(Collections.singleton(rsaKey));
JoseHeader joseHeader = TestJoseHeaders.joseHeader()
.headers(headers -> headers.remove(JoseHeaderNames.CRIT))
.build();
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
BiConsumer<JoseHeader.Builder, JwtClaimsSet.Builder> jwtCustomizer = mock(BiConsumer.class);
this.jwtEncoder.setJwtCustomizer(jwtCustomizer);
this.jwtEncoder.encode(joseHeader, jwtClaimsSet);
verify(jwtCustomizer).accept(any(JoseHeader.Builder.class), any(JwtClaimsSet.Builder.class));
}
@Test
public void encodeWhenMultipleActiveKeysThenUseFirst() {
AsymmetricKey rsaKey1 = TestCryptoKeys.rsaKey().build();
AsymmetricKey rsaKey2 = TestCryptoKeys.rsaKey().build();
when(this.keySource.getKeys()).thenReturn(
Stream.of(rsaKey1, rsaKey2)
.collect(Collectors.toCollection(LinkedHashSet::new)));
JoseHeader joseHeader = TestJoseHeaders.joseHeader()
.headers(headers -> headers.remove(JoseHeaderNames.CRIT))
.build();
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
Jwt jws = this.jwtEncoder.encode(joseHeader, jwtClaimsSet);
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey((RSAPublicKey) rsaKey1.getPublicKey()).build();
jwtDecoder.decode(jws.getTokenValue());
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -13,9 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.jose;
package org.springframework.security.oauth2.jwt;
import org.junit.Test;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import static org.assertj.core.api.Assertions.assertThat;
@ -40,14 +41,13 @@ public class JoseHeaderTests {
JoseHeader expectedJoseHeader = TestJoseHeaders.joseHeader().build();
JoseHeader joseHeader = JoseHeader.withAlgorithm(expectedJoseHeader.getJwsAlgorithm())
.jwkSetUri(expectedJoseHeader.getJwkSetUri())
.jwkSetUri(expectedJoseHeader.getJwkSetUri().toExternalForm())
.jwk(expectedJoseHeader.getJwk())
.keyId(expectedJoseHeader.getKeyId())
.x509Uri(expectedJoseHeader.getX509Uri())
.x509Uri(expectedJoseHeader.getX509Uri().toExternalForm())
.x509CertificateChain(expectedJoseHeader.getX509CertificateChain())
.x509SHA1Thumbprint(expectedJoseHeader.getX509SHA1Thumbprint())
.x509SHA256Thumbprint(expectedJoseHeader.getX509SHA256Thumbprint())
.critical(expectedJoseHeader.getCritical())
.type(expectedJoseHeader.getType())
.contentType(expectedJoseHeader.getContentType())
.headers(headers -> headers.put("custom-header-name", "custom-header-value"))

View File

@ -0,0 +1,291 @@
/*
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.jwt;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.BiConsumer;
import com.nimbusds.jose.KeySourceException;
import com.nimbusds.jose.jwk.ECKey;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSelector;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.KeyUse;
import com.nimbusds.jose.jwk.OctetSequenceKey;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.junit.Before;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.security.oauth2.jose.TestJwks;
import org.springframework.security.oauth2.jose.TestKeys;
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.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link NimbusJwsEncoder}.
*
* @author Joe Grandja
*/
public class NimbusJwsEncoderTests {
private JWKSource<SecurityContext> jwkSource;
private NimbusJwsEncoder jwsEncoder;
@Before
public void setUp() {
this.jwkSource = mock(JWKSource.class);
this.jwsEncoder = new NimbusJwsEncoder(this.jwkSource);
}
@Test
public void constructorWhenJwkSourceNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> new NimbusJwsEncoder(null))
.withMessage("jwkSource cannot be null");
}
@Test
public void setJwtCustomizerWhenNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.jwsEncoder.setJwtCustomizer(null))
.withMessage("jwtCustomizer cannot be null");
}
@Test
public void encodeWhenHeadersNullThenThrowIllegalArgumentException() {
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
assertThatIllegalArgumentException().isThrownBy(() -> this.jwsEncoder.encode(null, jwtClaimsSet))
.withMessage("headers cannot be null");
}
@Test
public void encodeWhenClaimsNullThenThrowIllegalArgumentException() {
JoseHeader joseHeader = TestJoseHeaders.joseHeader().build();
assertThatIllegalArgumentException().isThrownBy(() -> this.jwsEncoder.encode(joseHeader, null))
.withMessage("claims cannot be null");
}
@Test
public void encodeWhenCustomizerSetThenCalled() throws Exception {
RSAKey rsaJwk = TestJwks.DEFAULT_RSA_JWK;
given(this.jwkSource.get(any(), any())).willReturn(Collections.singletonList(rsaJwk));
BiConsumer<JoseHeader.Builder, JwtClaimsSet.Builder> jwtCustomizer = mock(BiConsumer.class);
this.jwsEncoder.setJwtCustomizer(jwtCustomizer);
JoseHeader joseHeader = TestJoseHeaders.joseHeader().build();
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
this.jwsEncoder.encode(joseHeader, jwtClaimsSet);
verify(jwtCustomizer).accept(any(JoseHeader.Builder.class), any(JwtClaimsSet.Builder.class));
}
@Test
public void encodeWhenJwkSelectFailedThenThrowJwtEncodingException() throws Exception {
given(this.jwkSource.get(any(), any())).willThrow(new KeySourceException("key source error"));
JoseHeader joseHeader = TestJoseHeaders.joseHeader().build();
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
assertThatExceptionOfType(JwtEncodingException.class)
.isThrownBy(() -> this.jwsEncoder.encode(joseHeader, jwtClaimsSet))
.withMessageContaining("Failed to select a JWK signing key -> key source error");
}
@Test
public void encodeWhenJwkMultipleSelectedThenThrowJwtEncodingException() throws Exception {
RSAKey rsaJwk = TestJwks.DEFAULT_RSA_JWK;
given(this.jwkSource.get(any(), any())).willReturn(Arrays.asList(rsaJwk, rsaJwk));
JoseHeader joseHeader = TestJoseHeaders.joseHeader().build();
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
assertThatExceptionOfType(JwtEncodingException.class)
.isThrownBy(() -> this.jwsEncoder.encode(joseHeader, jwtClaimsSet))
.withMessageContaining("Found multiple JWK signing keys for algorithm 'RS256'");
}
@Test
public void encodeWhenJwkSelectEmptyThenThrowJwtEncodingException() {
JoseHeader joseHeader = TestJoseHeaders.joseHeader().build();
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
assertThatExceptionOfType(JwtEncodingException.class)
.isThrownBy(() -> this.jwsEncoder.encode(joseHeader, jwtClaimsSet))
.withMessageContaining("Failed to select a JWK signing key");
}
@Test
public void encodeWhenJwkKidNullThenThrowJwtEncodingException() throws Exception {
// @formatter:off
RSAKey rsaJwk = TestJwks.jwk(TestKeys.DEFAULT_PUBLIC_KEY, TestKeys.DEFAULT_PRIVATE_KEY)
.keyID(null)
.build();
// @formatter:on
given(this.jwkSource.get(any(), any())).willReturn(Collections.singletonList(rsaJwk));
JoseHeader joseHeader = TestJoseHeaders.joseHeader().build();
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
assertThatExceptionOfType(JwtEncodingException.class)
.isThrownBy(() -> this.jwsEncoder.encode(joseHeader, jwtClaimsSet))
.withMessageContaining("The \"kid\" (key ID) from the selected JWK cannot be empty");
}
@Test
public void encodeWhenJwkUseEncryptionThenThrowJwtEncodingException() throws Exception {
// @formatter:off
RSAKey rsaJwk = TestJwks.jwk(TestKeys.DEFAULT_PUBLIC_KEY, TestKeys.DEFAULT_PRIVATE_KEY)
.keyUse(KeyUse.ENCRYPTION)
.build();
// @formatter:on
given(this.jwkSource.get(any(), any())).willReturn(Collections.singletonList(rsaJwk));
JoseHeader joseHeader = TestJoseHeaders.joseHeader().build();
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
assertThatExceptionOfType(JwtEncodingException.class)
.isThrownBy(() -> this.jwsEncoder.encode(joseHeader, jwtClaimsSet)).withMessageContaining(
"Failed to create a JWS Signer -> The JWK use must be sig (signature) or unspecified");
}
@Test
public void encodeWhenSuccessThenDecodes() throws Exception {
RSAKey rsaJwk = TestJwks.DEFAULT_RSA_JWK;
given(this.jwkSource.get(any(), any())).willReturn(Collections.singletonList(rsaJwk));
JoseHeader joseHeader = TestJoseHeaders.joseHeader().build();
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
Jwt encodedJws = this.jwsEncoder.encode(joseHeader, jwtClaimsSet);
// Assert headers/claims were added
assertThat(encodedJws.getHeaders().get(JoseHeaderNames.TYP)).isEqualTo("JWT");
assertThat(encodedJws.getHeaders().get(JoseHeaderNames.KID)).isEqualTo(rsaJwk.getKeyID());
assertThat(encodedJws.getId()).isNotNull();
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(rsaJwk.toRSAPublicKey()).build();
jwtDecoder.decode(encodedJws.getTokenValue());
}
@Test
public void encodeWhenKeysRotatedThenNewKeyUsed() throws Exception {
TestJWKSource jwkSource = new TestJWKSource();
JWKSource<SecurityContext> jwkSourceDelegate = spy(new JWKSource<SecurityContext>() {
@Override
public List<JWK> get(JWKSelector jwkSelector, SecurityContext context) {
return jwkSource.get(jwkSelector, context);
}
});
NimbusJwsEncoder jwsEncoder = new NimbusJwsEncoder(jwkSourceDelegate);
JwkListResultCaptor jwkListResultCaptor = new JwkListResultCaptor();
willAnswer(jwkListResultCaptor).given(jwkSourceDelegate).get(any(), any());
JoseHeader joseHeader = TestJoseHeaders.joseHeader().build();
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
Jwt encodedJws = jwsEncoder.encode(joseHeader, jwtClaimsSet);
JWK jwk1 = jwkListResultCaptor.getResult().get(0);
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(((RSAKey) jwk1).toRSAPublicKey()).build();
jwtDecoder.decode(encodedJws.getTokenValue());
jwkSource.rotate(); // Trigger key rotation
encodedJws = jwsEncoder.encode(joseHeader, jwtClaimsSet);
JWK jwk2 = jwkListResultCaptor.getResult().get(0);
jwtDecoder = NimbusJwtDecoder.withPublicKey(((RSAKey) jwk2).toRSAPublicKey()).build();
jwtDecoder.decode(encodedJws.getTokenValue());
assertThat(jwk1.getKeyID()).isNotEqualTo(jwk2.getKeyID());
}
private static final class JwkListResultCaptor implements Answer<List<JWK>> {
private List<JWK> result;
private List<JWK> getResult() {
return this.result;
}
@SuppressWarnings("unchecked")
@Override
public List<JWK> answer(InvocationOnMock invocationOnMock) throws Throwable {
this.result = (List<JWK>) invocationOnMock.callRealMethod();
return this.result;
}
}
private static final class TestJWKSource implements JWKSource<SecurityContext> {
private int keyId = 1000;
private JWKSet jwkSet;
private TestJWKSource() {
init();
}
@Override
public List<JWK> get(JWKSelector jwkSelector, SecurityContext context) {
return jwkSelector.select(this.jwkSet);
}
private void init() {
// @formatter:off
RSAKey rsaJwk = TestJwks.jwk(TestKeys.DEFAULT_PUBLIC_KEY, TestKeys.DEFAULT_PRIVATE_KEY)
.keyID("rsa-jwk-" + this.keyId++)
.build();
ECKey ecJwk = TestJwks.jwk((ECPublicKey) TestKeys.DEFAULT_EC_KEY_PAIR.getPublic(), (ECPrivateKey) TestKeys.DEFAULT_EC_KEY_PAIR.getPrivate())
.keyID("ec-jwk-" + this.keyId++)
.build();
OctetSequenceKey secretJwk = TestJwks.jwk(TestKeys.DEFAULT_SECRET_KEY)
.keyID("secret-jwk-" + this.keyId++)
.build();
// @formatter:on
this.jwkSet = new JWKSet(Arrays.asList(rsaJwk, ecJwk, secretJwk));
}
private void rotate() {
init();
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -13,38 +13,40 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.jose;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
package org.springframework.security.oauth2.jwt;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
/**
* @author Joe Grandja
*/
public class TestJoseHeaders {
public final class TestJoseHeaders {
private TestJoseHeaders() {
}
public static JoseHeader.Builder joseHeader() {
return joseHeader(SignatureAlgorithm.RS256);
}
public static JoseHeader.Builder joseHeader(SignatureAlgorithm signatureAlgorithm) {
// @formatter:off
return JoseHeader.withAlgorithm(signatureAlgorithm)
.jwkSetUri("https://provider.com/oauth2/jwks")
.jwk(rsaJwk())
.keyId(UUID.randomUUID().toString())
.keyId("keyId")
.x509Uri("https://provider.com/oauth2/x509")
.x509CertificateChain(Arrays.asList("x509Cert1", "x509Cert2"))
.x509SHA1Thumbprint("x509SHA1Thumbprint")
.x509SHA256Thumbprint("x509SHA256Thumbprint")
.critical(Collections.singleton("custom-header-name"))
.type("JWT")
.contentType("jwt-content-type")
.header("custom-header-name", "custom-header-value");
// @formatter:on
}
private static Map<String, Object> rsaJwk() {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -18,18 +18,21 @@ package org.springframework.security.oauth2.jwt;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.UUID;
/**
* @author Joe Grandja
*/
public class TestJwtClaimsSets {
public final class TestJwtClaimsSets {
private TestJwtClaimsSets() {
}
public static JwtClaimsSet.Builder jwtClaimsSet() {
String issuer = "https://provider.com";
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS);
// @formatter:off
return JwtClaimsSet.builder()
.issuer(issuer)
.subject("subject")
@ -37,7 +40,8 @@ public class TestJwtClaimsSets {
.issuedAt(issuedAt)
.notBefore(issuedAt)
.expiresAt(expiresAt)
.id(UUID.randomUUID().toString())
.id("jti")
.claim("custom-claim-name", "custom-claim-value");
// @formatter:on
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -28,7 +28,7 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.oauth2.jose.JoseHeaderNames;
import org.springframework.security.oauth2.jwt.JoseHeaderNames;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -23,7 +23,7 @@ import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.jose.JoseHeaderNames;
import org.springframework.security.oauth2.jwt.JoseHeaderNames;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtEncoder;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -25,7 +25,7 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken2;
import org.springframework.security.oauth2.jose.JoseHeaderNames;
import org.springframework.security.oauth2.jwt.JoseHeaderNames;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtEncoder;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,62 +15,61 @@
*/
package org.springframework.security.oauth2.server.authorization.web;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.ECKey;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.KeyUse;
import com.nimbusds.jose.jwk.RSAKey;
import org.junit.Before;
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.crypto.key.AsymmetricKey;
import org.springframework.security.crypto.key.CryptoKeySource;
import org.springframework.security.crypto.key.SymmetricKey;
import org.springframework.security.crypto.key.TestCryptoKeys;
import java.util.Arrays;
import java.util.Collections;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Collections;
import java.util.HashSet;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.nimbusds.jose.jwk.ECKey;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.KeyUse;
import com.nimbusds.jose.jwk.OctetSequenceKey;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.junit.Before;
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.jose.TestJwks;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
/**
* Tests for {@link JwkSetEndpointFilter}.
* Tests for {@link NimbusJwkSetEndpointFilter}.
*
* @author Joe Grandja
*/
public class JwkSetEndpointFilterTests {
private CryptoKeySource keySource;
private JwkSetEndpointFilter filter;
public class NimbusJwkSetEndpointFilterTests {
private JWKSource<SecurityContext> jwkSource;
private NimbusJwkSetEndpointFilter filter;
@Before
public void setUp() {
this.keySource = mock(CryptoKeySource.class);
this.filter = new JwkSetEndpointFilter(this.keySource);
this.jwkSource = mock(JWKSource.class);
this.filter = new NimbusJwkSetEndpointFilter(this.jwkSource);
}
@Test
public void constructorWhenKeySourceNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new JwkSetEndpointFilter(null))
public void constructorWhenJwkSourceNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new NimbusJwkSetEndpointFilter(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("keySource cannot be null");
.hasMessage("jwkSource cannot be null");
}
@Test
public void constructorWhenJwkSetEndpointUriNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new JwkSetEndpointFilter(this.keySource, null))
assertThatThrownBy(() -> new NimbusJwkSetEndpointFilter(this.jwkSource, null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("jwkSetEndpointUri cannot be empty");
}
@ -90,7 +89,7 @@ public class JwkSetEndpointFilterTests {
@Test
public void doFilterWhenJwkSetRequestPostThenNotProcessed() throws Exception {
String requestUri = JwkSetEndpointFilter.DEFAULT_JWK_SET_ENDPOINT_URI;
String requestUri = NimbusJwkSetEndpointFilter.DEFAULT_JWK_SET_ENDPOINT_URI;
MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
request.setServletPath(requestUri);
MockHttpServletResponse response = new MockHttpServletResponse();
@ -103,12 +102,11 @@ public class JwkSetEndpointFilterTests {
@Test
public void doFilterWhenAsymmetricKeysThenJwkSetResponse() throws Exception {
AsymmetricKey rsaKey = TestCryptoKeys.rsaKey().build();
AsymmetricKey ecKey = TestCryptoKeys.ecKey().build();
when(this.keySource.getKeys()).thenReturn(
Stream.of(rsaKey, ecKey).collect(Collectors.toSet()));
RSAKey rsaJwk = TestJwks.DEFAULT_RSA_JWK;
ECKey ecJwk = TestJwks.DEFAULT_EC_JWK;
given(this.jwkSource.get(any(), any())).willReturn(Arrays.asList(rsaJwk, ecJwk));
String requestUri = JwkSetEndpointFilter.DEFAULT_JWK_SET_ENDPOINT_URI;
String requestUri = NimbusJwkSetEndpointFilter.DEFAULT_JWK_SET_ENDPOINT_URI;
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
MockHttpServletResponse response = new MockHttpServletResponse();
@ -123,29 +121,25 @@ public class JwkSetEndpointFilterTests {
JWKSet jwkSet = JWKSet.parse(response.getContentAsString());
assertThat(jwkSet.getKeys()).hasSize(2);
RSAKey rsaJwk = (RSAKey) jwkSet.getKeyByKeyId(rsaKey.getId());
assertThat(rsaJwk).isNotNull();
assertThat(rsaJwk.toRSAPublicKey()).isEqualTo(rsaKey.getPublicKey());
assertThat(rsaJwk.toRSAPrivateKey()).isNull();
assertThat(rsaJwk.getKeyUse()).isEqualTo(KeyUse.SIGNATURE);
assertThat(rsaJwk.getAlgorithm()).isEqualTo(JWSAlgorithm.RS256);
RSAKey rsaJwkResult = (RSAKey) jwkSet.getKeyByKeyId(rsaJwk.getKeyID());
assertThat(rsaJwkResult).isNotNull();
assertThat(rsaJwkResult.toRSAPublicKey()).isEqualTo(rsaJwk.toRSAPublicKey());
assertThat(rsaJwkResult.toRSAPrivateKey()).isNull();
assertThat(rsaJwkResult.getKeyUse()).isEqualTo(KeyUse.SIGNATURE);
ECKey ecJwk = (ECKey) jwkSet.getKeyByKeyId(ecKey.getId());
assertThat(ecJwk).isNotNull();
assertThat(ecJwk.toECPublicKey()).isEqualTo(ecKey.getPublicKey());
assertThat(ecJwk.toECPublicKey()).isEqualTo(ecKey.getPublicKey());
assertThat(ecJwk.toECPrivateKey()).isNull();
assertThat(ecJwk.getKeyUse()).isEqualTo(KeyUse.SIGNATURE);
assertThat(ecJwk.getAlgorithm()).isEqualTo(JWSAlgorithm.ES256);
ECKey ecJwkResult = (ECKey) jwkSet.getKeyByKeyId(ecJwk.getKeyID());
assertThat(ecJwkResult).isNotNull();
assertThat(ecJwkResult.toECPublicKey()).isEqualTo(ecJwk.toECPublicKey());
assertThat(ecJwkResult.toECPrivateKey()).isNull();
assertThat(ecJwkResult.getKeyUse()).isEqualTo(KeyUse.SIGNATURE);
}
@Test
public void doFilterWhenSymmetricKeysThenJwkSetResponseEmpty() throws Exception {
SymmetricKey secretKey = TestCryptoKeys.secretKey().build();
when(this.keySource.getKeys()).thenReturn(
new HashSet<>(Collections.singleton(secretKey)));
OctetSequenceKey secretJwk = TestJwks.DEFAULT_SECRET_JWK;
given(this.jwkSource.get(any(), any())).willReturn(Collections.singletonList(secretJwk));
String requestUri = JwkSetEndpointFilter.DEFAULT_JWK_SET_ENDPOINT_URI;
String requestUri = NimbusJwkSetEndpointFilter.DEFAULT_JWK_SET_ENDPOINT_URI;
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
MockHttpServletResponse response = new MockHttpServletResponse();

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,12 +15,18 @@
*/
package sample.config;
import java.util.UUID;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import sample.jose.Jwks;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.crypto.key.CryptoKeySource;
import org.springframework.security.crypto.key.StaticKeyGeneratingCryptoKeySource;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
@ -29,8 +35,6 @@ import org.springframework.security.oauth2.server.authorization.client.Registere
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import java.util.UUID;
/**
* @author Joe Grandja
* @since 0.0.1
@ -61,8 +65,10 @@ public class AuthorizationServerConfig {
// @formatter:on
@Bean
public CryptoKeySource keySource() {
return new StaticKeyGeneratingCryptoKeySource();
public JWKSource<SecurityContext> jwkSource() {
RSAKey rsaKey = Jwks.generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
@Bean

View File

@ -0,0 +1,74 @@
/*
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.jose;
import java.security.KeyPair;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;
import javax.crypto.SecretKey;
import com.nimbusds.jose.jwk.Curve;
import com.nimbusds.jose.jwk.ECKey;
import com.nimbusds.jose.jwk.OctetSequenceKey;
import com.nimbusds.jose.jwk.RSAKey;
/**
* @author Joe Grandja
* @since 0.1.0
*/
public final class Jwks {
private Jwks() {
}
public static RSAKey generateRsa() {
KeyPair keyPair = KeyGeneratorUtils.generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
// @formatter:off
return new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
// @formatter:on
}
public static ECKey generateEc() {
KeyPair keyPair = KeyGeneratorUtils.generateEcKey();
ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic();
ECPrivateKey privateKey = (ECPrivateKey) keyPair.getPrivate();
Curve curve = Curve.forECParameterSpec(publicKey.getParams());
// @formatter:off
return new ECKey.Builder(curve, publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
// @formatter:on
}
public static OctetSequenceKey generateSecret() {
SecretKey secretKey = KeyGeneratorUtils.generateSecretKey();
// @formatter:off
return new OctetSequenceKey.Builder(secretKey)
.keyID(UUID.randomUUID().toString())
.build();
// @formatter:on
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -13,10 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.crypto.key;
package sample.jose;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
@ -25,12 +23,18 @@ import java.security.spec.ECParameterSpec;
import java.security.spec.ECPoint;
import java.security.spec.EllipticCurve;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
/**
* @author Joe Grandja
* @since 0.0.1
* @since 0.1.0
*/
final class KeyGeneratorUtils {
private KeyGeneratorUtils() {
}
static SecretKey generateSecretKey() {
SecretKey hmacKey;
try {