Replace ManagedKey with CryptoKey

Closes gh-105
This commit is contained in:
Joe Grandja 2020-10-25 19:58:29 -04:00
parent 8100568613
commit a9423c6b13
23 changed files with 757 additions and 731 deletions

View File

@ -24,7 +24,7 @@ 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.keys.KeyManager;
import org.springframework.security.crypto.key.CryptoKeySource;
import org.springframework.security.oauth2.jose.jws.NimbusJwsEncoder;
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
@ -106,14 +106,14 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
}
/**
* Sets the key manager.
* Sets the source for cryptographic keys.
*
* @param keyManager the key manager
* @param keySource the source for cryptographic keys
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
*/
public OAuth2AuthorizationServerConfigurer<B> keyManager(KeyManager keyManager) {
Assert.notNull(keyManager, "keyManager cannot be null");
this.getBuilder().setSharedObject(KeyManager.class, keyManager);
public OAuth2AuthorizationServerConfigurer<B> keySource(CryptoKeySource keySource) {
Assert.notNull(keySource, "keySource cannot be null");
this.getBuilder().setSharedObject(CryptoKeySource.class, keySource);
return this;
}
@ -135,7 +135,7 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
getAuthorizationService(builder));
builder.authenticationProvider(postProcess(clientAuthenticationProvider));
NimbusJwsEncoder jwtEncoder = new NimbusJwsEncoder(getKeyManager(builder));
NimbusJwsEncoder jwtEncoder = new NimbusJwsEncoder(getKeySource(builder));
OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider =
new OAuth2AuthorizationCodeAuthenticationProvider(
@ -172,7 +172,7 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
@Override
public void configure(B builder) {
JwkSetEndpointFilter jwkSetEndpointFilter = new JwkSetEndpointFilter(getKeyManager(builder));
JwkSetEndpointFilter jwkSetEndpointFilter = new JwkSetEndpointFilter(getKeySource(builder));
builder.addFilterBefore(postProcess(jwkSetEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
@ -237,16 +237,16 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
return (!authorizationServiceMap.isEmpty() ? authorizationServiceMap.values().iterator().next() : null);
}
private static <B extends HttpSecurityBuilder<B>> KeyManager getKeyManager(B builder) {
KeyManager keyManager = builder.getSharedObject(KeyManager.class);
if (keyManager == null) {
keyManager = getKeyManagerBean(builder);
builder.setSharedObject(KeyManager.class, keyManager);
private static <B extends HttpSecurityBuilder<B>> CryptoKeySource getKeySource(B builder) {
CryptoKeySource keySource = builder.getSharedObject(CryptoKeySource.class);
if (keySource == null) {
keySource = getKeySourceBean(builder);
builder.setSharedObject(CryptoKeySource.class, keySource);
}
return keyManager;
return keySource;
}
private static <B extends HttpSecurityBuilder<B>> KeyManager getKeyManagerBean(B builder) {
return builder.getSharedObject(ApplicationContext.class).getBean(KeyManager.class);
private static <B extends HttpSecurityBuilder<B>> CryptoKeySource getKeySourceBean(B builder) {
return builder.getSharedObject(ApplicationContext.class).getBean(CryptoKeySource.class);
}
}

View File

@ -0,0 +1,78 @@
/*
* 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

@ -0,0 +1,231 @@
/*
* 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

@ -0,0 +1,38 @@
/*
* 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

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.crypto.keys;
package org.springframework.security.crypto.key;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;

View File

@ -0,0 +1,62 @@
/*
* 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

@ -0,0 +1,61 @@
/*
* 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,58 +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.keys;
import org.springframework.lang.Nullable;
import java.util.Set;
/**
* Implementations of this interface are responsible for the management of {@link ManagedKey}(s),
* e.g. {@code javax.crypto.SecretKey}, {@code java.security.PrivateKey}, {@code java.security.PublicKey}, etc.
*
* @author Joe Grandja
* @since 0.0.1
* @see ManagedKey
*/
public interface KeyManager {
/**
* Returns the {@link ManagedKey} identified by the provided {@code keyId},
* or {@code null} if not found.
*
* @param keyId the key ID
* @return the {@link ManagedKey}, or {@code null} if not found
*/
@Nullable
ManagedKey findByKeyId(String keyId);
/**
* Returns a {@code Set} of {@link ManagedKey}(s) having the provided key {@code algorithm},
* or an empty {@code Set} if not found.
*
* @param algorithm the key algorithm
* @return a {@code Set} of {@link ManagedKey}(s), or an empty {@code Set} if not found
*/
Set<ManagedKey> findByAlgorithm(String algorithm);
/**
* Returns a {@code Set} of the {@link ManagedKey}(s).
*
* @return a {@code Set} of the {@link ManagedKey}(s)
*/
Set<ManagedKey> getKeys();
}

View File

@ -1,246 +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.keys;
import org.springframework.lang.Nullable;
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.time.Instant;
import java.util.Objects;
/**
* A {@code java.security.Key} that is managed by a {@link KeyManager}.
*
* @author Joe Grandja
* @since 0.0.1
* @see KeyManager
*/
public final class ManagedKey implements Serializable {
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
private Key key;
private PublicKey publicKey;
private String keyId;
private Instant activatedOn;
private Instant deactivatedOn;
private ManagedKey() {
}
/**
* Returns {@code true} if this is a symmetric key, {@code false} otherwise.
*
* @return {@code true} if this is a symmetric key, {@code false} otherwise
*/
public boolean isSymmetric() {
return SecretKey.class.isAssignableFrom(this.key.getClass());
}
/**
* Returns {@code true} if this is a asymmetric key, {@code false} otherwise.
*
* @return {@code true} if this is a asymmetric key, {@code false} otherwise
*/
public boolean isAsymmetric() {
return PrivateKey.class.isAssignableFrom(this.key.getClass());
}
/**
* Returns a type of {@code java.security.Key},
* e.g. {@code javax.crypto.SecretKey} or {@code java.security.PrivateKey}.
*
* @param <T> the type of {@code java.security.Key}
* @return the type of {@code java.security.Key}
*/
@SuppressWarnings("unchecked")
public <T extends Key> T getKey() {
return (T) this.key;
}
/**
* Returns the {@code java.security.PublicKey} if this is a asymmetric key, {@code null} otherwise.
*
* @return the {@code java.security.PublicKey} if this is a asymmetric key, {@code null} otherwise
*/
@Nullable
public PublicKey getPublicKey() {
return this.publicKey;
}
/**
* Returns the key ID.
*
* @return the key ID
*/
public String getKeyId() {
return this.keyId;
}
/**
* Returns the time when this key was activated.
*
* @return the time when this key was activated
*/
public Instant getActivatedOn() {
return this.activatedOn;
}
/**
* Returns the time when this key was deactivated, {@code null} if still active.
*
* @return the time when this key was deactivated, {@code null} if still active
*/
@Nullable
public Instant getDeactivatedOn() {
return this.deactivatedOn;
}
/**
* Returns {@code true} if this key is active, {@code false} otherwise.
*
* @return {@code true} if this key is active, {@code false} otherwise
*/
public boolean isActive() {
return getDeactivatedOn() == null;
}
/**
* Returns the key algorithm.
*
* @return the key algorithm
*/
public String getAlgorithm() {
return this.key.getAlgorithm();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
ManagedKey that = (ManagedKey) obj;
return Objects.equals(this.keyId, that.keyId);
}
@Override
public int hashCode() {
return Objects.hash(this.keyId);
}
/**
* Returns a new {@link Builder}, initialized with the provided {@code javax.crypto.SecretKey}.
*
* @param secretKey the {@code javax.crypto.SecretKey}
* @return the {@link Builder}
*/
public static Builder withSymmetricKey(SecretKey secretKey) {
return new Builder(secretKey);
}
/**
* Returns a new {@link Builder}, initialized with the provided
* {@code java.security.PublicKey} and {@code java.security.PrivateKey}.
*
* @param publicKey the {@code java.security.PublicKey}
* @param privateKey the {@code java.security.PrivateKey}
* @return the {@link Builder}
*/
public static Builder withAsymmetricKey(PublicKey publicKey, PrivateKey privateKey) {
return new Builder(publicKey, privateKey);
}
/**
* A builder for {@link ManagedKey}.
*/
public static class Builder {
private Key key;
private PublicKey publicKey;
private String keyId;
private Instant activatedOn;
private Instant deactivatedOn;
private Builder(SecretKey secretKey) {
Assert.notNull(secretKey, "secretKey cannot be null");
this.key = secretKey;
}
private Builder(PublicKey publicKey, PrivateKey privateKey) {
Assert.notNull(publicKey, "publicKey cannot be null");
Assert.notNull(privateKey, "privateKey cannot be null");
this.key = privateKey;
this.publicKey = publicKey;
}
/**
* Sets the key ID.
*
* @param keyId the key ID
* @return the {@link Builder}
*/
public Builder keyId(String keyId) {
this.keyId = keyId;
return this;
}
/**
* Sets the time when this key was activated.
*
* @param activatedOn the time when this key was activated
* @return the {@link Builder}
*/
public Builder activatedOn(Instant activatedOn) {
this.activatedOn = activatedOn;
return this;
}
/**
* Sets the time when this key was deactivated.
*
* @param deactivatedOn the time when this key was deactivated
* @return the {@link Builder}
*/
public Builder deactivatedOn(Instant deactivatedOn) {
this.deactivatedOn = deactivatedOn;
return this;
}
/**
* Builds a new {@link ManagedKey}.
*
* @return a {@link ManagedKey}
*/
public ManagedKey build() {
Assert.hasText(this.keyId, "keyId cannot be empty");
Assert.notNull(this.activatedOn, "activatedOn cannot be null");
ManagedKey managedKey = new ManagedKey();
managedKey.key = this.key;
managedKey.publicKey = this.publicKey;
managedKey.keyId = this.keyId;
managedKey.activatedOn = this.activatedOn;
managedKey.deactivatedOn = this.deactivatedOn;
return managedKey;
}
}
}

View File

@ -1,89 +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.keys;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import javax.crypto.SecretKey;
import java.security.KeyPair;
import java.time.Instant;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.springframework.security.crypto.keys.KeyGeneratorUtils.generateRsaKey;
import static org.springframework.security.crypto.keys.KeyGeneratorUtils.generateSecretKey;
/**
* An implementation of a {@link KeyManager} that generates the {@link ManagedKey}(s) when constructed.
*
* <p>
* <b>NOTE:</b> This implementation should ONLY be used during development/testing.
*
* @author Joe Grandja
* @since 0.0.1
* @see KeyManager
*/
public final class StaticKeyGeneratingKeyManager implements KeyManager {
private final Map<String, ManagedKey> keys;
public StaticKeyGeneratingKeyManager() {
this.keys = Collections.unmodifiableMap(new HashMap<>(generateKeys()));
}
@Nullable
@Override
public ManagedKey findByKeyId(String keyId) {
Assert.hasText(keyId, "keyId cannot be empty");
return this.keys.get(keyId);
}
@Override
public Set<ManagedKey> findByAlgorithm(String algorithm) {
Assert.hasText(algorithm, "algorithm cannot be empty");
return this.keys.values().stream()
.filter(managedKey -> managedKey.getAlgorithm().equals(algorithm))
.collect(Collectors.toSet());
}
@Override
public Set<ManagedKey> getKeys() {
return new HashSet<>(this.keys.values());
}
private static Map<String, ManagedKey> generateKeys() {
KeyPair rsaKeyPair = generateRsaKey();
ManagedKey rsaManagedKey = ManagedKey.withAsymmetricKey(rsaKeyPair.getPublic(), rsaKeyPair.getPrivate())
.keyId(UUID.randomUUID().toString())
.activatedOn(Instant.now())
.build();
SecretKey hmacKey = generateSecretKey();
ManagedKey secretManagedKey = ManagedKey.withSymmetricKey(hmacKey)
.keyId(UUID.randomUUID().toString())
.activatedOn(Instant.now())
.build();
return Stream.of(rsaManagedKey, secretManagedKey)
.collect(Collectors.toMap(ManagedKey::getKeyId, v -> v));
}
}

View File

@ -29,8 +29,9 @@ 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.keys.KeyManager;
import org.springframework.security.crypto.keys.ManagedKey;
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;
@ -58,7 +59,7 @@ 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 KeyManager} supplied via the constructor.
* from the {@link CryptoKeySource} supplied via the constructor.
*
* <p>
* <b>NOTE:</b> This implementation uses the Nimbus JOSE + JWT SDK.
@ -66,7 +67,7 @@ import java.util.stream.Collectors;
* @author Joe Grandja
* @since 0.0.1
* @see JwtEncoder
* @see KeyManager
* @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>
@ -92,16 +93,16 @@ public final class NimbusJwsEncoder implements JwtEncoder {
};
private static final Converter<JoseHeader, JWSHeader> jwsHeaderConverter = new JwsHeaderConverter();
private static final Converter<JwtClaimsSet, JWTClaimsSet> jwtClaimsSetConverter = new JwtClaimsSetConverter();
private final KeyManager keyManager;
private final CryptoKeySource keySource;
/**
* Constructs a {@code NimbusJwsEncoder} using the provided parameters.
*
* @param keyManager the key manager
* @param keySource the source for cryptographic keys
*/
public NimbusJwsEncoder(KeyManager keyManager) {
Assert.notNull(keyManager, "keyManager cannot be null");
this.keyManager = keyManager;
public NimbusJwsEncoder(CryptoKeySource keySource) {
Assert.notNull(keySource, "keySource cannot be null");
this.keySource = keySource;
}
@Override
@ -109,24 +110,24 @@ public final class NimbusJwsEncoder implements JwtEncoder {
Assert.notNull(headers, "headers cannot be null");
Assert.notNull(claims, "claims cannot be null");
ManagedKey managedKey = selectKey(headers);
if (managedKey == 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 (managedKey.isAsymmetric()) {
if (!managedKey.getAlgorithm().equals(RSA_KEY_TYPE)) {
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 '" + managedKey.getAlgorithm() + "'"));
"Unsupported key type '" + cryptoKey.getAlgorithm() + "'"));
}
PrivateKey privateKey = managedKey.getKey();
PrivateKey privateKey = (PrivateKey) cryptoKey.getKey();
jwsSigner = new RSASSASigner(privateKey);
} else {
SecretKey secretKey = managedKey.getKey();
SecretKey secretKey = (SecretKey) cryptoKey.getKey();
try {
jwsSigner = new MACSigner(secretKey);
} catch (KeyLengthException ex) {
@ -137,7 +138,7 @@ public final class NimbusJwsEncoder implements JwtEncoder {
headers = JoseHeader.from(headers)
.type(JOSEObjectType.JWT.getType())
.keyId(managedKey.getKeyId())
.keyId(cryptoKey.getId())
.build();
JWSHeader jwsHeader = jwsHeaderConverter.convert(headers);
@ -159,28 +160,19 @@ public final class NimbusJwsEncoder implements JwtEncoder {
headers.getHeaders(), claims.getClaims());
}
private ManagedKey selectKey(JoseHeader headers) {
private CryptoKey<?> selectKey(JoseHeader headers) {
JwsAlgorithm jwsAlgorithm = headers.getJwsAlgorithm();
String keyAlgorithm = jcaKeyAlgorithmMappings.get(jwsAlgorithm);
if (!StringUtils.hasText(keyAlgorithm)) {
return null;
}
Set<ManagedKey> matchingKeys = this.keyManager.findByAlgorithm(keyAlgorithm);
if (CollectionUtils.isEmpty(matchingKeys)) {
return null;
}
return matchingKeys.stream()
.filter(ManagedKey::isActive)
.max(this::mostRecentActivated)
return this.keySource.getKeys().stream()
.filter(key -> key.getAlgorithm().equals(keyAlgorithm))
.findFirst()
.orElse(null);
}
private int mostRecentActivated(ManagedKey managedKey1, ManagedKey managedKey2) {
return managedKey1.getActivatedOn().isAfter(managedKey2.getActivatedOn()) ? 1 : -1;
}
private static class JwsHeaderConverter implements Converter<JoseHeader, JWSHeader> {
@Override

View File

@ -24,8 +24,8 @@ 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.keys.KeyManager;
import org.springframework.security.crypto.keys.ManagedKey;
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;
@ -47,7 +47,7 @@ import java.util.stream.Collectors;
*
* @author Joe Grandja
* @since 0.0.1
* @see KeyManager
* @see CryptoKeySource
* @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>
*/
@ -57,28 +57,28 @@ public class JwkSetEndpointFilter extends OncePerRequestFilter {
*/
public static final String DEFAULT_JWK_SET_ENDPOINT_URI = "/oauth2/jwks";
private final KeyManager keyManager;
private final CryptoKeySource keySource;
private final RequestMatcher requestMatcher;
/**
* Constructs a {@code JwkSetEndpointFilter} using the provided parameters.
*
* @param keyManager the key manager
* @param keySource the source for cryptographic keys
*/
public JwkSetEndpointFilter(KeyManager keyManager) {
this(keyManager, DEFAULT_JWK_SET_ENDPOINT_URI);
public JwkSetEndpointFilter(CryptoKeySource keySource) {
this(keySource, DEFAULT_JWK_SET_ENDPOINT_URI);
}
/**
* Constructs a {@code JwkSetEndpointFilter} using the provided parameters.
*
* @param keyManager the key manager
* @param keySource the source for cryptographic keys
* @param jwkSetEndpointUri the endpoint {@code URI} for JWK Set requests
*/
public JwkSetEndpointFilter(KeyManager keyManager, String jwkSetEndpointUri) {
Assert.notNull(keyManager, "keyManager cannot be null");
public JwkSetEndpointFilter(CryptoKeySource keySource, String jwkSetEndpointUri) {
Assert.notNull(keySource, "keySource cannot be null");
Assert.hasText(jwkSetEndpointUri, "jwkSetEndpointUri cannot be empty");
this.keyManager = keyManager;
this.keySource = keySource;
this.requestMatcher = new AntPathRequestMatcher(jwkSetEndpointUri, HttpMethod.GET.name());
}
@ -101,30 +101,31 @@ public class JwkSetEndpointFilter extends OncePerRequestFilter {
private JWKSet buildJwkSet() {
return new JWKSet(
this.keyManager.getKeys().stream()
.filter(managedKey -> managedKey.isActive() && managedKey.isAsymmetric())
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(ManagedKey managedKey) {
private JWK convert(AsymmetricKey asymmetricKey) {
JWK jwk = null;
if (managedKey.getPublicKey() instanceof RSAPublicKey) {
RSAPublicKey publicKey = (RSAPublicKey) managedKey.getPublicKey();
if (asymmetricKey.getPublicKey() instanceof RSAPublicKey) {
RSAPublicKey publicKey = (RSAPublicKey) asymmetricKey.getPublicKey();
jwk = new RSAKey.Builder(publicKey)
.keyUse(KeyUse.SIGNATURE)
.algorithm(JWSAlgorithm.RS256)
.keyID(managedKey.getKeyId())
.keyID(asymmetricKey.getId())
.build();
} else if (managedKey.getPublicKey() instanceof ECPublicKey) {
ECPublicKey publicKey = (ECPublicKey) managedKey.getPublicKey();
} 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(managedKey.getKeyId())
.keyID(asymmetricKey.getId())
.build();
}
return jwk;

View File

@ -27,8 +27,8 @@ 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.keys.KeyManager;
import org.springframework.security.crypto.keys.StaticKeyGeneratingKeyManager;
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;
@ -85,7 +85,7 @@ public class OAuth2AuthorizationCodeGrantTests {
private static RegisteredClientRepository registeredClientRepository;
private static OAuth2AuthorizationService authorizationService;
private static KeyManager keyManager;
private static CryptoKeySource keySource;
@Rule
public final SpringTestRule spring = new SpringTestRule();
@ -97,7 +97,7 @@ public class OAuth2AuthorizationCodeGrantTests {
public static void init() {
registeredClientRepository = mock(RegisteredClientRepository.class);
authorizationService = mock(OAuth2AuthorizationService.class);
keyManager = new StaticKeyGeneratingKeyManager();
keySource = new StaticKeyGeneratingCryptoKeySource();
}
@Before
@ -266,8 +266,8 @@ public class OAuth2AuthorizationCodeGrantTests {
}
@Bean
KeyManager keyManager() {
return keyManager;
CryptoKeySource keySource() {
return keySource;
}
}
}

View File

@ -26,8 +26,8 @@ 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.keys.KeyManager;
import org.springframework.security.crypto.keys.StaticKeyGeneratingKeyManager;
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.server.authorization.OAuth2AuthorizationService;
@ -61,7 +61,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
public class OAuth2ClientCredentialsGrantTests {
private static RegisteredClientRepository registeredClientRepository;
private static OAuth2AuthorizationService authorizationService;
private static KeyManager keyManager;
private static CryptoKeySource keySource;
@Rule
public final SpringTestRule spring = new SpringTestRule();
@ -73,7 +73,7 @@ public class OAuth2ClientCredentialsGrantTests {
public static void init() {
registeredClientRepository = mock(RegisteredClientRepository.class);
authorizationService = mock(OAuth2AuthorizationService.class);
keyManager = new StaticKeyGeneratingKeyManager();
keySource = new StaticKeyGeneratingCryptoKeySource();
}
@Before
@ -159,8 +159,8 @@ public class OAuth2ClientCredentialsGrantTests {
}
@Bean
KeyManager keyManager() {
return keyManager;
CryptoKeySource keySource() {
return keySource;
}
}
}

View File

@ -26,8 +26,8 @@ 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.keys.KeyManager;
import org.springframework.security.crypto.keys.StaticKeyGeneratingKeyManager;
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.server.authorization.OAuth2Authorization;
@ -67,6 +67,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;
@Rule
public final SpringTestRule spring = new SpringTestRule();
@ -78,6 +79,7 @@ public class OAuth2RefreshTokenGrantTests {
public static void init() {
registeredClientRepository = mock(RegisteredClientRepository.class);
authorizationService = mock(OAuth2AuthorizationService.class);
keySource = new StaticKeyGeneratingCryptoKeySource();
}
@Before
@ -151,6 +153,8 @@ public class OAuth2RefreshTokenGrantTests {
}
@Bean
KeyManager keyManager() { return new StaticKeyGeneratingKeyManager(); }
CryptoKeySource keySource() {
return keySource;
}
}
}

View File

@ -27,8 +27,8 @@ 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.keys.KeyManager;
import org.springframework.security.crypto.keys.StaticKeyGeneratingKeyManager;
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;
@ -66,7 +66,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
public class OAuth2TokenRevocationTests {
private static RegisteredClientRepository registeredClientRepository;
private static OAuth2AuthorizationService authorizationService;
private static KeyManager keyManager;
private static CryptoKeySource keySource;
@Rule
public final SpringTestRule spring = new SpringTestRule();
@ -78,7 +78,7 @@ public class OAuth2TokenRevocationTests {
public static void init() {
registeredClientRepository = mock(RegisteredClientRepository.class);
authorizationService = mock(OAuth2AuthorizationService.class);
keyManager = new StaticKeyGeneratingKeyManager();
keySource = new StaticKeyGeneratingCryptoKeySource();
}
@Before
@ -181,8 +181,8 @@ public class OAuth2TokenRevocationTests {
}
@Bean
KeyManager keyManager() {
return keyManager;
CryptoKeySource keySource() {
return keySource;
}
}
}

View File

@ -0,0 +1,113 @@
/*
* 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

@ -0,0 +1,42 @@
/*
* 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

@ -1,120 +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.keys;
import org.junit.BeforeClass;
import org.junit.Test;
import javax.crypto.SecretKey;
import java.security.Key;
import java.security.KeyPair;
import java.security.PrivateKey;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.springframework.security.crypto.keys.KeyGeneratorUtils.generateRsaKey;
import static org.springframework.security.crypto.keys.KeyGeneratorUtils.generateSecretKey;
/**
* Tests for {@link ManagedKey}.
*
* @author Joe Grandja
*/
public class ManagedKeyTests {
private static SecretKey secretKey;
private static KeyPair rsaKeyPair;
@BeforeClass
public static void init() {
secretKey = generateSecretKey();
rsaKeyPair = generateRsaKey();
}
@Test
public void withSymmetricKeyWhenNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> ManagedKey.withSymmetricKey(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("secretKey cannot be null");
}
@Test
public void buildWhenKeyIdNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> ManagedKey.withSymmetricKey(secretKey).build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("keyId cannot be empty");
}
@Test
public void buildWhenActivatedOnNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> ManagedKey.withSymmetricKey(secretKey).keyId("keyId").build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("activatedOn cannot be null");
}
@Test
public void buildWhenSymmetricKeyAllAttributesProvidedThenAllAttributesAreSet() {
ManagedKey expectedManagedKey = TestManagedKeys.secretManagedKey().build();
ManagedKey managedKey = ManagedKey.withSymmetricKey(expectedManagedKey.getKey())
.keyId(expectedManagedKey.getKeyId())
.activatedOn(expectedManagedKey.getActivatedOn())
.build();
assertThat(managedKey.isSymmetric()).isTrue();
assertThat(managedKey.<Key>getKey()).isInstanceOf(SecretKey.class);
assertThat(managedKey.<SecretKey>getKey()).isEqualTo(expectedManagedKey.getKey());
assertThat(managedKey.getPublicKey()).isNull();
assertThat(managedKey.getKeyId()).isEqualTo(expectedManagedKey.getKeyId());
assertThat(managedKey.getActivatedOn()).isEqualTo(expectedManagedKey.getActivatedOn());
assertThat(managedKey.getDeactivatedOn()).isEqualTo(expectedManagedKey.getDeactivatedOn());
assertThat(managedKey.isActive()).isTrue();
assertThat(managedKey.getAlgorithm()).isEqualTo(expectedManagedKey.getAlgorithm());
}
@Test
public void withAsymmetricKeyWhenPublicKeyNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> ManagedKey.withAsymmetricKey(null, rsaKeyPair.getPrivate()))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("publicKey cannot be null");
}
@Test
public void withAsymmetricKeyWhenPrivateKeyNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> ManagedKey.withAsymmetricKey(rsaKeyPair.getPublic(), null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("privateKey cannot be null");
}
@Test
public void buildWhenAsymmetricKeyAllAttributesProvidedThenAllAttributesAreSet() {
ManagedKey expectedManagedKey = TestManagedKeys.rsaManagedKey().build();
ManagedKey managedKey = ManagedKey.withAsymmetricKey(expectedManagedKey.getPublicKey(), expectedManagedKey.getKey())
.keyId(expectedManagedKey.getKeyId())
.activatedOn(expectedManagedKey.getActivatedOn())
.build();
assertThat(managedKey.isAsymmetric()).isTrue();
assertThat(managedKey.<Key>getKey()).isInstanceOf(PrivateKey.class);
assertThat(managedKey.<PrivateKey>getKey()).isEqualTo(expectedManagedKey.getKey());
assertThat(managedKey.getPublicKey()).isNotNull();
assertThat(managedKey.getKeyId()).isEqualTo(expectedManagedKey.getKeyId());
assertThat(managedKey.getActivatedOn()).isEqualTo(expectedManagedKey.getActivatedOn());
assertThat(managedKey.getDeactivatedOn()).isEqualTo(expectedManagedKey.getDeactivatedOn());
assertThat(managedKey.isActive()).isTrue();
assertThat(managedKey.getAlgorithm()).isEqualTo(expectedManagedKey.getAlgorithm());
}
}

View File

@ -1,50 +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.keys;
import java.security.KeyPair;
import java.time.Instant;
import java.util.UUID;
import static org.springframework.security.crypto.keys.KeyGeneratorUtils.generateEcKey;
import static org.springframework.security.crypto.keys.KeyGeneratorUtils.generateRsaKey;
import static org.springframework.security.crypto.keys.KeyGeneratorUtils.generateSecretKey;
/**
* @author Joe Grandja
*/
public class TestManagedKeys {
public static ManagedKey.Builder secretManagedKey() {
return ManagedKey.withSymmetricKey(generateSecretKey())
.keyId(UUID.randomUUID().toString())
.activatedOn(Instant.now());
}
public static ManagedKey.Builder rsaManagedKey() {
KeyPair rsaKeyPair = generateRsaKey();
return ManagedKey.withAsymmetricKey(rsaKeyPair.getPublic(), rsaKeyPair.getPrivate())
.keyId(UUID.randomUUID().toString())
.activatedOn(Instant.now());
}
public static ManagedKey.Builder ecManagedKey() {
KeyPair ecKeyPair = generateEcKey();
return ManagedKey.withAsymmetricKey(ecKeyPair.getPublic(), ecKeyPair.getPrivate())
.keyId(UUID.randomUUID().toString())
.activatedOn(Instant.now());
}
}

View File

@ -17,9 +17,9 @@ package org.springframework.security.oauth2.jose.jws;
import org.junit.Before;
import org.junit.Test;
import org.springframework.security.crypto.keys.KeyManager;
import org.springframework.security.crypto.keys.ManagedKey;
import org.springframework.security.crypto.keys.TestManagedKeys;
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;
@ -30,14 +30,12 @@ import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.TestJwtClaimsSets;
import java.security.interfaces.RSAPublicKey;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.LinkedHashSet;
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.when;
@ -47,20 +45,20 @@ import static org.mockito.Mockito.when;
* @author Joe Grandja
*/
public class NimbusJwsEncoderTests {
private KeyManager keyManager;
private CryptoKeySource keySource;
private NimbusJwsEncoder jwtEncoder;
@Before
public void setUp() {
this.keyManager = mock(KeyManager.class);
this.jwtEncoder = new NimbusJwsEncoder(this.keyManager);
this.keySource = mock(CryptoKeySource.class);
this.jwtEncoder = new NimbusJwsEncoder(this.keySource);
}
@Test
public void constructorWhenKeyManagerNullThenThrowIllegalArgumentException() {
public void constructorWhenKeySourceNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new NimbusJwsEncoder(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("keyManager cannot be null");
.hasMessage("keySource cannot be null");
}
@Test
@ -103,8 +101,8 @@ public class NimbusJwsEncoderTests {
@Test
public void encodeWhenUnsupportedKeyTypeThenThrowJwtEncodingException() {
ManagedKey managedKey = TestManagedKeys.ecManagedKey().build();
when(this.keyManager.findByAlgorithm(any())).thenReturn(Collections.singleton(managedKey));
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();
@ -116,8 +114,8 @@ public class NimbusJwsEncoderTests {
@Test
public void encodeWhenSuccessThenDecodes() {
ManagedKey managedKey = TestManagedKeys.rsaManagedKey().build();
when(this.keyManager.findByAlgorithm(any())).thenReturn(Collections.singleton(managedKey));
AsymmetricKey rsaKey = TestCryptoKeys.rsaKey().build();
when(this.keySource.getKeys()).thenReturn(Collections.singleton(rsaKey));
JoseHeader joseHeader = TestJoseHeaders.joseHeader()
.headers(headers -> headers.remove(JoseHeaderNames.CRIT))
@ -126,25 +124,17 @@ public class NimbusJwsEncoderTests {
Jwt jws = this.jwtEncoder.encode(joseHeader, jwtClaimsSet);
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey((RSAPublicKey) managedKey.getPublicKey()).build();
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey((RSAPublicKey) rsaKey.getPublicKey()).build();
jwtDecoder.decode(jws.getTokenValue());
}
@Test
public void encodeWhenMultipleActiveKeysThenUseMostRecent() {
ManagedKey managedKeyActivated2DaysAgo = TestManagedKeys.rsaManagedKey()
.activatedOn(Instant.now().minus(2, ChronoUnit.DAYS))
.build();
ManagedKey managedKeyActivated1DayAgo = TestManagedKeys.rsaManagedKey()
.activatedOn(Instant.now().minus(1, ChronoUnit.DAYS))
.build();
ManagedKey managedKeyActivatedToday = TestManagedKeys.rsaManagedKey()
.activatedOn(Instant.now())
.build();
when(this.keyManager.findByAlgorithm(any())).thenReturn(
Stream.of(managedKeyActivated2DaysAgo, managedKeyActivated1DayAgo, managedKeyActivatedToday)
.collect(Collectors.toSet()));
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))
@ -153,7 +143,7 @@ public class NimbusJwsEncoderTests {
Jwt jws = this.jwtEncoder.encode(joseHeader, jwtClaimsSet);
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey((RSAPublicKey) managedKeyActivatedToday.getPublicKey()).build();
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey((RSAPublicKey) rsaKey1.getPublicKey()).build();
jwtDecoder.decode(jws.getTokenValue());
}
}

View File

@ -25,14 +25,14 @@ 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.keys.KeyManager;
import org.springframework.security.crypto.keys.ManagedKey;
import org.springframework.security.crypto.keys.TestManagedKeys;
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 javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.Instant;
import java.util.Collections;
import java.util.HashSet;
import java.util.stream.Collectors;
@ -52,25 +52,25 @@ import static org.mockito.Mockito.when;
* @author Joe Grandja
*/
public class JwkSetEndpointFilterTests {
private KeyManager keyManager;
private CryptoKeySource keySource;
private JwkSetEndpointFilter filter;
@Before
public void setUp() {
this.keyManager = mock(KeyManager.class);
this.filter = new JwkSetEndpointFilter(this.keyManager);
this.keySource = mock(CryptoKeySource.class);
this.filter = new JwkSetEndpointFilter(this.keySource);
}
@Test
public void constructorWhenKeyManagerNullThenThrowIllegalArgumentException() {
public void constructorWhenKeySourceNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new JwkSetEndpointFilter(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("keyManager cannot be null");
.hasMessage("keySource cannot be null");
}
@Test
public void constructorWhenJwkSetEndpointUriNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new JwkSetEndpointFilter(this.keyManager, null))
assertThatThrownBy(() -> new JwkSetEndpointFilter(this.keySource, null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("jwkSetEndpointUri cannot be empty");
}
@ -103,10 +103,10 @@ public class JwkSetEndpointFilterTests {
@Test
public void doFilterWhenAsymmetricKeysThenJwkSetResponse() throws Exception {
ManagedKey rsaManagedKey = TestManagedKeys.rsaManagedKey().build();
ManagedKey ecManagedKey = TestManagedKeys.ecManagedKey().build();
when(this.keyManager.getKeys()).thenReturn(
Stream.of(rsaManagedKey, ecManagedKey).collect(Collectors.toSet()));
AsymmetricKey rsaKey = TestCryptoKeys.rsaKey().build();
AsymmetricKey ecKey = TestCryptoKeys.ecKey().build();
when(this.keySource.getKeys()).thenReturn(
Stream.of(rsaKey, ecKey).collect(Collectors.toSet()));
String requestUri = JwkSetEndpointFilter.DEFAULT_JWK_SET_ENDPOINT_URI;
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
@ -123,17 +123,17 @@ public class JwkSetEndpointFilterTests {
JWKSet jwkSet = JWKSet.parse(response.getContentAsString());
assertThat(jwkSet.getKeys()).hasSize(2);
RSAKey rsaJwk = (RSAKey) jwkSet.getKeyByKeyId(rsaManagedKey.getKeyId());
RSAKey rsaJwk = (RSAKey) jwkSet.getKeyByKeyId(rsaKey.getId());
assertThat(rsaJwk).isNotNull();
assertThat(rsaJwk.toRSAPublicKey()).isEqualTo(rsaManagedKey.getPublicKey());
assertThat(rsaJwk.toRSAPublicKey()).isEqualTo(rsaKey.getPublicKey());
assertThat(rsaJwk.toRSAPrivateKey()).isNull();
assertThat(rsaJwk.getKeyUse()).isEqualTo(KeyUse.SIGNATURE);
assertThat(rsaJwk.getAlgorithm()).isEqualTo(JWSAlgorithm.RS256);
ECKey ecJwk = (ECKey) jwkSet.getKeyByKeyId(ecManagedKey.getKeyId());
ECKey ecJwk = (ECKey) jwkSet.getKeyByKeyId(ecKey.getId());
assertThat(ecJwk).isNotNull();
assertThat(ecJwk.toECPublicKey()).isEqualTo(ecManagedKey.getPublicKey());
assertThat(ecJwk.toECPublicKey()).isEqualTo(ecManagedKey.getPublicKey());
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);
@ -141,32 +141,9 @@ public class JwkSetEndpointFilterTests {
@Test
public void doFilterWhenSymmetricKeysThenJwkSetResponseEmpty() throws Exception {
ManagedKey secretManagedKey = TestManagedKeys.secretManagedKey().build();
when(this.keyManager.getKeys()).thenReturn(
new HashSet<>(Collections.singleton(secretManagedKey)));
String requestUri = JwkSetEndpointFilter.DEFAULT_JWK_SET_ENDPOINT_URI;
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verifyNoInteractions(filterChain);
assertThat(response.getContentType()).isEqualTo(MediaType.APPLICATION_JSON_VALUE);
JWKSet jwkSet = JWKSet.parse(response.getContentAsString());
assertThat(jwkSet.getKeys()).isEmpty();
}
@Test
public void doFilterWhenNoActiveKeysThenJwkSetResponseEmpty() throws Exception {
ManagedKey rsaManagedKey = TestManagedKeys.rsaManagedKey().deactivatedOn(Instant.now()).build();
ManagedKey ecManagedKey = TestManagedKeys.ecManagedKey().deactivatedOn(Instant.now()).build();
when(this.keyManager.getKeys()).thenReturn(
Stream.of(rsaManagedKey, ecManagedKey).collect(Collectors.toSet()));
SymmetricKey secretKey = TestCryptoKeys.secretKey().build();
when(this.keySource.getKeys()).thenReturn(
new HashSet<>(Collections.singleton(secretKey)));
String requestUri = JwkSetEndpointFilter.DEFAULT_JWK_SET_ENDPOINT_URI;
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);

View File

@ -22,8 +22,8 @@ import org.springframework.security.config.annotation.web.configuration.OAuth2Au
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.keys.KeyManager;
import org.springframework.security.crypto.keys.StaticKeyGeneratingKeyManager;
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.server.authorization.client.InMemoryRegisteredClientRepository;
@ -60,8 +60,8 @@ public class AuthorizationServerConfig {
// @formatter:on
@Bean
public KeyManager keyManager() {
return new StaticKeyGeneratingKeyManager();
public CryptoKeySource keySource() {
return new StaticKeyGeneratingCryptoKeySource();
}
// @formatter:off