28 changed files with 2424 additions and 23 deletions
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
apply plugin: 'io.spring.convention.spring-module' |
||||
|
||||
dependencies { |
||||
compile project(':spring-security-core2') |
||||
compile 'org.springframework.security:spring-security-core' |
||||
compile springCoreDependency |
||||
|
||||
testCompile 'junit:junit' |
||||
testCompile 'org.assertj:assertj-core' |
||||
testCompile 'org.mockito:mockito-core' |
||||
} |
||||
@ -0,0 +1,81 @@
@@ -0,0 +1,81 @@
|
||||
/* |
||||
* 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 javax.crypto.KeyGenerator; |
||||
import javax.crypto.SecretKey; |
||||
import java.math.BigInteger; |
||||
import java.security.KeyPair; |
||||
import java.security.KeyPairGenerator; |
||||
import java.security.spec.ECFieldFp; |
||||
import java.security.spec.ECParameterSpec; |
||||
import java.security.spec.ECPoint; |
||||
import java.security.spec.EllipticCurve; |
||||
|
||||
/** |
||||
* @author Joe Grandja |
||||
* @since 0.0.1 |
||||
*/ |
||||
final class KeyGeneratorUtils { |
||||
|
||||
static SecretKey generateSecretKey() { |
||||
SecretKey hmacKey; |
||||
try { |
||||
hmacKey = KeyGenerator.getInstance("HmacSha256").generateKey(); |
||||
} catch (Exception ex) { |
||||
throw new IllegalStateException(ex); |
||||
} |
||||
return hmacKey; |
||||
} |
||||
|
||||
static KeyPair generateRsaKey() { |
||||
KeyPair keyPair; |
||||
try { |
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); |
||||
keyPairGenerator.initialize(2048); |
||||
keyPair = keyPairGenerator.generateKeyPair(); |
||||
} catch (Exception ex) { |
||||
throw new IllegalStateException(ex); |
||||
} |
||||
return keyPair; |
||||
} |
||||
|
||||
static KeyPair generateEcKey() { |
||||
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; |
||||
} |
||||
} |
||||
@ -0,0 +1,58 @@
@@ -0,0 +1,58 @@
|
||||
/* |
||||
* 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(); |
||||
|
||||
} |
||||
@ -0,0 +1,246 @@
@@ -0,0 +1,246 @@
|
||||
/* |
||||
* 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.core.SpringSecurityCoreVersion2; |
||||
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 = SpringSecurityCoreVersion2.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; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,89 @@
@@ -0,0 +1,89 @@
|
||||
/* |
||||
* 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)); |
||||
} |
||||
} |
||||
@ -0,0 +1,120 @@
@@ -0,0 +1,120 @@
|
||||
/* |
||||
* 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()); |
||||
} |
||||
} |
||||
@ -0,0 +1,50 @@
@@ -0,0 +1,50 @@
|
||||
/* |
||||
* 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()); |
||||
} |
||||
} |
||||
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
apply plugin: 'io.spring.convention.spring-module' |
||||
|
||||
dependencies { |
||||
compile project(':spring-security-crypto2') |
||||
compile 'org.springframework.security:spring-security-oauth2-core' |
||||
compile 'org.springframework.security:spring-security-oauth2-jose' |
||||
compile springCoreDependency |
||||
compile 'com.nimbusds:nimbus-jose-jwt' |
||||
|
||||
testCompile project(path: ':spring-security-crypto2', configuration: 'tests') |
||||
testCompile 'junit:junit' |
||||
testCompile 'org.assertj:assertj-core' |
||||
testCompile 'org.mockito:mockito-core' |
||||
} |
||||
@ -0,0 +1,368 @@
@@ -0,0 +1,368 @@
|
||||
/* |
||||
* 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; |
||||
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm; |
||||
import org.springframework.security.oauth2.jwt.Jwt; |
||||
import org.springframework.util.Assert; |
||||
|
||||
import java.util.Collections; |
||||
import java.util.LinkedHashMap; |
||||
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; |
||||
|
||||
/** |
||||
* The JOSE header is a JSON object representing the header parameters of a JSON Web Token, |
||||
* whether the JWT is a JWS or JWE, that describe the cryptographic operations applied to the JWT |
||||
* and optionally, additional properties of the JWT. |
||||
* |
||||
* @author Anoop Garlapati |
||||
* @author Joe Grandja |
||||
* @since 0.0.1 |
||||
* @see Jwt |
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519#section-5">JWT JOSE Header</a> |
||||
* @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 final class JoseHeader { |
||||
private final Map<String, Object> headers; |
||||
|
||||
private JoseHeader(Map<String, Object> headers) { |
||||
this.headers = Collections.unmodifiableMap(new LinkedHashMap<>(headers)); |
||||
} |
||||
|
||||
/** |
||||
* Returns the JWS algorithm used to digitally sign the JWS. |
||||
* |
||||
* @return the JWS algorithm |
||||
*/ |
||||
public JwsAlgorithm getJwsAlgorithm() { |
||||
return getHeader(ALG); |
||||
} |
||||
|
||||
/** |
||||
* Returns the JWK Set URL that refers to the resource of a set of JSON-encoded public keys, |
||||
* one of which corresponds to the key used to digitally sign the JWS or encrypt the JWE. |
||||
* |
||||
* @return the JWK Set URL |
||||
*/ |
||||
public String getJwkSetUri() { |
||||
return getHeader(JKU); |
||||
} |
||||
|
||||
/** |
||||
* Returns the JSON Web Key which is the public key that corresponds to the key |
||||
* used to digitally sign the JWS or encrypt the JWE. |
||||
* |
||||
* @return the JSON Web Key |
||||
*/ |
||||
public Map<String, Object> getJwk() { |
||||
return getHeader(JWK); |
||||
} |
||||
|
||||
/** |
||||
* Returns the key ID that is a hint indicating which key was used to secure the JWS or JWE. |
||||
* |
||||
* @return the key ID |
||||
*/ |
||||
public String getKeyId() { |
||||
return getHeader(KID); |
||||
} |
||||
|
||||
/** |
||||
* Returns the X.509 URL that refers to the resource for the X.509 public key certificate |
||||
* or certificate chain corresponding to the key used to digitally sign the JWS or encrypt the JWE. |
||||
* |
||||
* @return the X.509 URL |
||||
*/ |
||||
public String getX509Uri() { |
||||
return getHeader(X5U); |
||||
} |
||||
|
||||
/** |
||||
* Returns the X.509 certificate chain that contains the X.509 public key certificate |
||||
* or certificate chain corresponding to the key used to digitally sign the JWS or encrypt the JWE. |
||||
* |
||||
* @return the X.509 certificate chain |
||||
*/ |
||||
public List<String> getX509CertificateChain() { |
||||
return getHeader(X5C); |
||||
} |
||||
|
||||
/** |
||||
* Returns the X.509 certificate SHA-1 thumbprint that 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 the JWS or encrypt the JWE. |
||||
* |
||||
* @return the X.509 certificate SHA-1 thumbprint |
||||
*/ |
||||
public String getX509SHA1Thumbprint() { |
||||
return getHeader(X5T); |
||||
} |
||||
|
||||
/** |
||||
* Returns the X.509 certificate SHA-256 thumbprint that 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 the JWS or encrypt the JWE. |
||||
* |
||||
* @return the X.509 certificate SHA-256 thumbprint |
||||
*/ |
||||
public String getX509SHA256Thumbprint() { |
||||
return getHeader(X5T_S256); |
||||
} |
||||
|
||||
/** |
||||
* Returns the critical headers that indicates which extensions to the JWS/JWE/JWA specifications |
||||
* are being used that MUST be understood and processed. |
||||
* |
||||
* @return the critical headers |
||||
*/ |
||||
public Set<String> getCritical() { |
||||
return getHeader(CRIT); |
||||
} |
||||
|
||||
/** |
||||
* Returns the type header that declares the media type of the JWS/JWE. |
||||
* |
||||
* @return the type header |
||||
*/ |
||||
public String getType() { |
||||
return getHeader(TYP); |
||||
} |
||||
|
||||
/** |
||||
* Returns the content type header that declares the media type of the secured content (the payload). |
||||
* |
||||
* @return the content type header |
||||
*/ |
||||
public String getContentType() { |
||||
return getHeader(CTY); |
||||
} |
||||
|
||||
/** |
||||
* Returns the headers. |
||||
* |
||||
* @return the headers |
||||
*/ |
||||
public Map<String, Object> getHeaders() { |
||||
return this.headers; |
||||
} |
||||
|
||||
/** |
||||
* Returns the header value. |
||||
* |
||||
* @param name the header name |
||||
* @param <T> the type of the header value |
||||
* @return the header value |
||||
*/ |
||||
@SuppressWarnings("unchecked") |
||||
public <T> T getHeader(String name) { |
||||
Assert.hasText(name, "name cannot be empty"); |
||||
return (T) getHeaders().get(name); |
||||
} |
||||
|
||||
/** |
||||
* Returns a new {@link Builder}, initialized with the provided {@link JwsAlgorithm}. |
||||
* |
||||
* @param jwsAlgorithm the {@link JwsAlgorithm} |
||||
* @return the {@link Builder} |
||||
*/ |
||||
public static Builder withAlgorithm(JwsAlgorithm jwsAlgorithm) { |
||||
return new Builder(jwsAlgorithm); |
||||
} |
||||
|
||||
/** |
||||
* Returns a new {@link Builder}, initialized with the provided {@code headers}. |
||||
* |
||||
* @param headers the headers |
||||
* @return the {@link Builder} |
||||
*/ |
||||
public static Builder from(JoseHeader headers) { |
||||
return new Builder(headers); |
||||
} |
||||
|
||||
/** |
||||
* A builder for {@link JoseHeader}. |
||||
*/ |
||||
public static class Builder { |
||||
private final Map<String, Object> headers = new LinkedHashMap<>(); |
||||
|
||||
private Builder(JwsAlgorithm jwsAlgorithm) { |
||||
Assert.notNull(jwsAlgorithm, "jwsAlgorithm cannot be null"); |
||||
header(ALG, jwsAlgorithm); |
||||
} |
||||
|
||||
private Builder(JoseHeader headers) { |
||||
Assert.notNull(headers, "headers cannot be null"); |
||||
this.headers.putAll(headers.getHeaders()); |
||||
} |
||||
|
||||
/** |
||||
* Sets the JWK Set URL that refers to the resource of a set of JSON-encoded public keys, |
||||
* one of which corresponds to the key used to digitally sign the JWS or encrypt the JWE. |
||||
* |
||||
* @param jwkSetUri the JWK Set URL |
||||
* @return the {@link Builder} |
||||
*/ |
||||
public Builder jwkSetUri(String jwkSetUri) { |
||||
return header(JKU, jwkSetUri); |
||||
} |
||||
|
||||
/** |
||||
* Sets the JSON Web Key which is the public key that corresponds to the key |
||||
* used to digitally sign the JWS or encrypt the JWE. |
||||
* |
||||
* @param jwk the JSON Web Key |
||||
* @return the {@link Builder} |
||||
*/ |
||||
public Builder jwk(Map<String, Object> jwk) { |
||||
return header(JWK, jwk); |
||||
} |
||||
|
||||
/** |
||||
* Sets the key ID that is a hint indicating which key was used to secure the JWS or JWE. |
||||
* |
||||
* @param keyId the key ID |
||||
* @return the {@link Builder} |
||||
*/ |
||||
public Builder keyId(String keyId) { |
||||
return header(KID, keyId); |
||||
} |
||||
|
||||
/** |
||||
* Sets the X.509 URL that refers to the resource for the X.509 public key certificate |
||||
* or certificate chain corresponding to the key used to digitally sign the JWS or encrypt the JWE. |
||||
* |
||||
* @param x509Uri the X.509 URL |
||||
* @return the {@link Builder} |
||||
*/ |
||||
public Builder x509Uri(String x509Uri) { |
||||
return header(X5U, x509Uri); |
||||
} |
||||
|
||||
/** |
||||
* Sets the X.509 certificate chain that contains the X.509 public key certificate |
||||
* or certificate chain corresponding to the key used to digitally sign the JWS or encrypt the JWE. |
||||
* |
||||
* @param x509CertificateChain the X.509 certificate chain |
||||
* @return the {@link Builder} |
||||
*/ |
||||
public Builder x509CertificateChain(List<String> x509CertificateChain) { |
||||
return header(X5C, x509CertificateChain); |
||||
} |
||||
|
||||
/** |
||||
* Sets the X.509 certificate SHA-1 thumbprint that 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 the JWS or encrypt the JWE. |
||||
* |
||||
* @param x509SHA1Thumbprint the X.509 certificate SHA-1 thumbprint |
||||
* @return the {@link Builder} |
||||
*/ |
||||
public Builder x509SHA1Thumbprint(String x509SHA1Thumbprint) { |
||||
return header(X5T, x509SHA1Thumbprint); |
||||
} |
||||
|
||||
/** |
||||
* Sets the X.509 certificate SHA-256 thumbprint that 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 the JWS or encrypt the JWE. |
||||
* |
||||
* @param x509SHA256Thumbprint the X.509 certificate SHA-256 thumbprint |
||||
* @return the {@link Builder} |
||||
*/ |
||||
public Builder x509SHA256Thumbprint(String x509SHA256Thumbprint) { |
||||
return header(X5T_S256, x509SHA256Thumbprint); |
||||
} |
||||
|
||||
/** |
||||
* Sets the critical headers that indicates which extensions to the JWS/JWE/JWA specifications |
||||
* are being used that MUST be understood and processed. |
||||
* |
||||
* @param headerNames the critical header names |
||||
* @return the {@link Builder} |
||||
*/ |
||||
public Builder critical(Set<String> headerNames) { |
||||
return header(CRIT, headerNames); |
||||
} |
||||
|
||||
/** |
||||
* Sets the type header that declares the media type of the JWS/JWE. |
||||
* |
||||
* @param type the type header |
||||
* @return the {@link Builder} |
||||
*/ |
||||
public Builder type(String type) { |
||||
return header(TYP, type); |
||||
} |
||||
|
||||
/** |
||||
* Sets the content type header that declares the media type of the secured content (the payload). |
||||
* |
||||
* @param contentType the content type header |
||||
* @return the {@link Builder} |
||||
*/ |
||||
public Builder contentType(String contentType) { |
||||
return header(CTY, contentType); |
||||
} |
||||
|
||||
/** |
||||
* Sets the header. |
||||
* |
||||
* @param name the header name |
||||
* @param value the header value |
||||
* @return the {@link Builder} |
||||
*/ |
||||
public Builder header(String name, Object value) { |
||||
Assert.hasText(name, "name cannot be empty"); |
||||
Assert.notNull(value, "value cannot be null"); |
||||
this.headers.put(name, value); |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* A {@code Consumer} to be provided access to the headers |
||||
* allowing the ability to add, replace, or remove. |
||||
* |
||||
* @param headersConsumer a {@code Consumer} of the headers |
||||
* @return the {@link Builder} |
||||
*/ |
||||
public Builder headers(Consumer<Map<String, Object>> headersConsumer) { |
||||
headersConsumer.accept(this.headers); |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Builds a new {@link JoseHeader}. |
||||
* |
||||
* @return a {@link JoseHeader} |
||||
*/ |
||||
public JoseHeader build() { |
||||
Assert.notEmpty(this.headers, "headers cannot be empty"); |
||||
return new JoseHeader(this.headers); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,96 @@
@@ -0,0 +1,96 @@
|
||||
/* |
||||
* 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; |
||||
|
||||
/** |
||||
* The Registered Header Parameter Names defined by the JSON Web Token (JWT), |
||||
* JSON Web Signature (JWS) and JSON Web Encryption (JWE) specifications |
||||
* that may be contained in the JOSE Header of a JWT. |
||||
* |
||||
* @author Anoop Garlapati |
||||
* @author Joe Grandja |
||||
* @since 0.0.1 |
||||
* @see JoseHeader |
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519#section-5">JWT JOSE Header</a> |
||||
* @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 { |
||||
|
||||
/** |
||||
* {@code alg} - the algorithm header identifies the cryptographic algorithm used to secure a JWS or JWE |
||||
*/ |
||||
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"; |
||||
|
||||
/** |
||||
* {@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"; |
||||
|
||||
/** |
||||
* {@code kid} - the key ID header is a hint indicating which key was used to secure a JWS or JWE |
||||
*/ |
||||
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"; |
||||
|
||||
/** |
||||
* {@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"; |
||||
|
||||
/** |
||||
* {@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"; |
||||
|
||||
/** |
||||
* {@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"; |
||||
|
||||
/** |
||||
* {@code typ} - the type header is used by JWS/JWE applications to declare the media type of a JWS/JWE |
||||
*/ |
||||
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"; |
||||
|
||||
/** |
||||
* {@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"; |
||||
|
||||
} |
||||
@ -0,0 +1,325 @@
@@ -0,0 +1,325 @@
|
||||
/* |
||||
* 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 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.jwk.JWK; |
||||
import com.nimbusds.jose.util.Base64; |
||||
import com.nimbusds.jose.util.Base64URL; |
||||
import com.nimbusds.jwt.JWTClaimsSet; |
||||
import com.nimbusds.jwt.SignedJWT; |
||||
import net.minidev.json.JSONObject; |
||||
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.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.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.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. |
||||
* |
||||
* <p> |
||||
* <b>NOTE:</b> This implementation uses the Nimbus JOSE + JWT SDK. |
||||
* |
||||
* @author Joe Grandja |
||||
* @since 0.0.1 |
||||
* @see JwtEncoder |
||||
* @see KeyManager |
||||
* @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 Converter<JoseHeader, JWSHeader> jwsHeaderConverter = new JwsHeaderConverter(); |
||||
private static final Converter<JwtClaimsSet, JWTClaimsSet> jwtClaimsSetConverter = new JwtClaimsSetConverter(); |
||||
private final KeyManager keyManager; |
||||
|
||||
/** |
||||
* Constructs a {@code NimbusJwsEncoder} using the provided parameters. |
||||
* |
||||
* @param keyManager the key manager |
||||
*/ |
||||
public NimbusJwsEncoder(KeyManager keyManager) { |
||||
Assert.notNull(keyManager, "keyManager cannot be null"); |
||||
this.keyManager = keyManager; |
||||
} |
||||
|
||||
@Override |
||||
public Jwt encode(JoseHeader headers, JwtClaimsSet claims) throws JwtEncodingException { |
||||
Assert.notNull(headers, "headers cannot be null"); |
||||
Assert.notNull(claims, "claims cannot be null"); |
||||
|
||||
ManagedKey managedKey = selectKey(headers); |
||||
if (managedKey == 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)) { |
||||
throw new JwtEncodingException(String.format( |
||||
ENCODING_ERROR_MESSAGE_TEMPLATE, |
||||
"Unsupported key type '" + managedKey.getAlgorithm() + "'")); |
||||
} |
||||
PrivateKey privateKey = managedKey.getKey(); |
||||
jwsSigner = new RSASSASigner(privateKey); |
||||
} else { |
||||
SecretKey secretKey = managedKey.getKey(); |
||||
try { |
||||
jwsSigner = new MACSigner(secretKey); |
||||
} catch (KeyLengthException ex) { |
||||
throw new JwtEncodingException(String.format( |
||||
ENCODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex); |
||||
} |
||||
} |
||||
|
||||
headers = JoseHeader.from(headers) |
||||
.type(JOSEObjectType.JWT.getType()) |
||||
.keyId(managedKey.getKeyId()) |
||||
.build(); |
||||
JWSHeader jwsHeader = jwsHeaderConverter.convert(headers); |
||||
|
||||
claims = JwtClaimsSet.from(claims) |
||||
.id(UUID.randomUUID().toString()) |
||||
.build(); |
||||
JWTClaimsSet jwtClaimsSet = jwtClaimsSetConverter.convert(claims); |
||||
|
||||
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); |
||||
} |
||||
String jws = signedJWT.serialize(); |
||||
|
||||
return new Jwt(jws, claims.getIssuedAt(), claims.getExpiresAt(), |
||||
headers.getHeaders(), claims.getClaims()); |
||||
} |
||||
|
||||
private ManagedKey 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) |
||||
.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 |
||||
public JWSHeader convert(JoseHeader headers) { |
||||
JWSHeader.Builder builder = new JWSHeader.Builder( |
||||
JWSAlgorithm.parse(headers.getJwsAlgorithm().getName())); |
||||
|
||||
Set<String> critical = headers.getCritical(); |
||||
if (!CollectionUtils.isEmpty(critical)) { |
||||
builder.criticalParams(critical); |
||||
} |
||||
|
||||
String contentType = headers.getContentType(); |
||||
if (StringUtils.hasText(contentType)) { |
||||
builder.contentType(contentType); |
||||
} |
||||
|
||||
String jwkSetUri = headers.getJwkSetUri(); |
||||
if (StringUtils.hasText(jwkSetUri)) { |
||||
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); |
||||
} |
||||
} |
||||
|
||||
Map<String, Object> jwk = headers.getJwk(); |
||||
if (!CollectionUtils.isEmpty(jwk)) { |
||||
try { |
||||
builder.jwk(JWK.parse(new JSONObject(jwk))); |
||||
} catch (Exception ex) { |
||||
throw new JwtEncodingException(String.format( |
||||
ENCODING_ERROR_MESSAGE_TEMPLATE, |
||||
"Failed to convert '" + JoseHeaderNames.JWK + "' JOSE header"), ex); |
||||
} |
||||
} |
||||
|
||||
String keyId = headers.getKeyId(); |
||||
if (StringUtils.hasText(keyId)) { |
||||
builder.keyID(keyId); |
||||
} |
||||
|
||||
String type = headers.getType(); |
||||
if (StringUtils.hasText(type)) { |
||||
builder.type(new JOSEObjectType(type)); |
||||
} |
||||
|
||||
List<String> x509CertificateChain = headers.getX509CertificateChain(); |
||||
if (!CollectionUtils.isEmpty(x509CertificateChain)) { |
||||
builder.x509CertChain( |
||||
x509CertificateChain.stream() |
||||
.map(Base64::new) |
||||
.collect(Collectors.toList())); |
||||
} |
||||
|
||||
String x509SHA1Thumbprint = headers.getX509SHA1Thumbprint(); |
||||
if (StringUtils.hasText(x509SHA1Thumbprint)) { |
||||
builder.x509CertThumbprint(new Base64URL(x509SHA1Thumbprint)); |
||||
} |
||||
|
||||
String x509SHA256Thumbprint = headers.getX509SHA256Thumbprint(); |
||||
if (StringUtils.hasText(x509SHA256Thumbprint)) { |
||||
builder.x509CertSHA256Thumbprint(new Base64URL(x509SHA256Thumbprint)); |
||||
} |
||||
|
||||
String x509Uri = headers.getX509Uri(); |
||||
if (StringUtils.hasText(x509Uri)) { |
||||
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); |
||||
} |
||||
} |
||||
|
||||
Map<String, Object> customHeaders = headers.getHeaders().entrySet().stream() |
||||
.filter(header -> !JWSHeader.getRegisteredParameterNames().contains(header.getKey())) |
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); |
||||
if (!CollectionUtils.isEmpty(customHeaders)) { |
||||
builder.customParams(customHeaders); |
||||
} |
||||
|
||||
return builder.build(); |
||||
} |
||||
} |
||||
|
||||
private static class JwtClaimsSetConverter implements Converter<JwtClaimsSet, JWTClaimsSet> { |
||||
|
||||
@Override |
||||
public JWTClaimsSet convert(JwtClaimsSet claims) { |
||||
JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder(); |
||||
|
||||
URL issuer = claims.getIssuer(); |
||||
if (issuer != null) { |
||||
builder.issuer(issuer.toExternalForm()); |
||||
} |
||||
|
||||
String subject = claims.getSubject(); |
||||
if (StringUtils.hasText(subject)) { |
||||
builder.subject(subject); |
||||
} |
||||
|
||||
List<String> audience = claims.getAudience(); |
||||
if (!CollectionUtils.isEmpty(audience)) { |
||||
builder.audience(audience); |
||||
} |
||||
|
||||
Instant issuedAt = claims.getIssuedAt(); |
||||
if (issuedAt != null) { |
||||
builder.issueTime(Date.from(issuedAt)); |
||||
} |
||||
|
||||
Instant expiresAt = claims.getExpiresAt(); |
||||
if (expiresAt != null) { |
||||
builder.expirationTime(Date.from(expiresAt)); |
||||
} |
||||
|
||||
Instant notBefore = claims.getNotBefore(); |
||||
if (notBefore != null) { |
||||
builder.notBeforeTime(Date.from(notBefore)); |
||||
} |
||||
|
||||
String jwtId = claims.getId(); |
||||
if (StringUtils.hasText(jwtId)) { |
||||
builder.jwtID(jwtId); |
||||
} |
||||
|
||||
Map<String, Object> customClaims = claims.getClaims().entrySet().stream() |
||||
.filter(claim -> !JWTClaimsSet.getRegisteredNames().contains(claim.getKey())) |
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); |
||||
if (!CollectionUtils.isEmpty(customClaims)) { |
||||
customClaims.forEach(builder::claim); |
||||
} |
||||
|
||||
return builder.build(); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,198 @@
@@ -0,0 +1,198 @@
|
||||
/* |
||||
* 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.jwt; |
||||
|
||||
import org.springframework.util.Assert; |
||||
|
||||
import java.net.URL; |
||||
import java.time.Instant; |
||||
import java.util.Collections; |
||||
import java.util.LinkedHashMap; |
||||
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; |
||||
|
||||
/** |
||||
* The {@link Jwt JWT} Claims Set is a JSON object representing the claims conveyed by a JSON Web Token. |
||||
* |
||||
* @author Anoop Garlapati |
||||
* @author Joe Grandja |
||||
* @since 0.0.1 |
||||
* @see Jwt |
||||
* @see JwtClaimAccessor |
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519#section-4">JWT Claims Set</a> |
||||
*/ |
||||
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)); |
||||
} |
||||
|
||||
@Override |
||||
public Map<String, Object> getClaims() { |
||||
return this.claims; |
||||
} |
||||
|
||||
/** |
||||
* Returns a new {@link Builder}. |
||||
* |
||||
* @return the {@link Builder} |
||||
*/ |
||||
public static Builder withClaims() { |
||||
return new Builder(); |
||||
} |
||||
|
||||
/** |
||||
* Returns a new {@link Builder}, initialized with the provided {@code claims}. |
||||
* |
||||
* @param claims a JWT claims set |
||||
* @return the {@link Builder} |
||||
*/ |
||||
public static Builder from(JwtClaimsSet claims) { |
||||
return new Builder(claims); |
||||
} |
||||
|
||||
/** |
||||
* A builder for {@link JwtClaimsSet}. |
||||
*/ |
||||
public static class Builder { |
||||
private final Map<String, Object> claims = new LinkedHashMap<>(); |
||||
|
||||
private Builder() { |
||||
} |
||||
|
||||
private Builder(JwtClaimsSet claims) { |
||||
Assert.notNull(claims, "claims cannot be null"); |
||||
this.claims.putAll(claims.getClaims()); |
||||
} |
||||
|
||||
/** |
||||
* Sets the issuer {@code (iss)} claim, which identifies the principal that issued the JWT. |
||||
* |
||||
* @param issuer the issuer identifier |
||||
* @return the {@link Builder} |
||||
*/ |
||||
public Builder issuer(URL issuer) { |
||||
return claim(ISS, issuer); |
||||
} |
||||
|
||||
/** |
||||
* Sets the subject {@code (sub)} claim, which identifies the principal that is the subject of the JWT. |
||||
* |
||||
* @param subject the subject identifier |
||||
* @return the {@link Builder} |
||||
*/ |
||||
public Builder subject(String subject) { |
||||
return claim(SUB, subject); |
||||
} |
||||
|
||||
/** |
||||
* Sets the audience {@code (aud)} claim, which identifies the recipient(s) that the JWT is intended for. |
||||
* |
||||
* @param audience the audience that this JWT is intended for |
||||
* @return the {@link Builder} |
||||
*/ |
||||
public Builder audience(List<String> audience) { |
||||
return claim(AUD, audience); |
||||
} |
||||
|
||||
/** |
||||
* Sets the expiration time {@code (exp)} claim, which identifies the time |
||||
* on or after which the JWT MUST NOT be accepted for processing. |
||||
* |
||||
* @param expiresAt the time on or after which the JWT MUST NOT be accepted for processing |
||||
* @return the {@link Builder} |
||||
*/ |
||||
public Builder expiresAt(Instant expiresAt) { |
||||
return claim(EXP, expiresAt); |
||||
} |
||||
|
||||
/** |
||||
* Sets the not before {@code (nbf)} claim, which identifies the time |
||||
* before which the JWT MUST NOT be accepted for processing. |
||||
* |
||||
* @param notBefore the time before which the JWT MUST NOT be accepted for processing |
||||
* @return the {@link Builder} |
||||
*/ |
||||
public Builder notBefore(Instant notBefore) { |
||||
return claim(NBF, notBefore); |
||||
} |
||||
|
||||
/** |
||||
* Sets the issued at {@code (iat)} claim, which identifies the time at which the JWT was issued. |
||||
* |
||||
* @param issuedAt the time at which the JWT was issued |
||||
* @return the {@link Builder} |
||||
*/ |
||||
public Builder issuedAt(Instant issuedAt) { |
||||
return claim(IAT, issuedAt); |
||||
} |
||||
|
||||
/** |
||||
* Sets the JWT ID {@code (jti)} claim, which provides a unique identifier for the JWT. |
||||
* |
||||
* @param jti the unique identifier for the JWT |
||||
* @return the {@link Builder} |
||||
*/ |
||||
public Builder id(String jti) { |
||||
return claim(JTI, jti); |
||||
} |
||||
|
||||
/** |
||||
* Sets the claim. |
||||
* |
||||
* @param name the claim name |
||||
* @param value the claim value |
||||
* @return the {@link Builder} |
||||
*/ |
||||
public Builder claim(String name, Object value) { |
||||
Assert.hasText(name, "name cannot be empty"); |
||||
Assert.notNull(value, "value cannot be null"); |
||||
this.claims.put(name, value); |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* A {@code Consumer} to be provided access to the claims set |
||||
* allowing the ability to add, replace, or remove. |
||||
* |
||||
* @param claimsConsumer a {@code Consumer} of the claims set |
||||
*/ |
||||
public Builder claims(Consumer<Map<String, Object>> claimsConsumer) { |
||||
claimsConsumer.accept(this.claims); |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Builds a new {@link JwtClaimsSet}. |
||||
* |
||||
* @return a {@link JwtClaimsSet} |
||||
*/ |
||||
public JwtClaimsSet build() { |
||||
Assert.notEmpty(this.claims, "claims cannot be empty"); |
||||
return new JwtClaimsSet(this.claims); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,56 @@
@@ -0,0 +1,56 @@
|
||||
/* |
||||
* 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.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. |
||||
* |
||||
* <p> |
||||
* JWTs may be represented using the JWS Compact Serialization format for a |
||||
* JSON Web Signature (JWS) structure or JWE Compact Serialization format for a |
||||
* JSON Web Encryption (JWE) structure. Therefore, implementors are responsible |
||||
* for signing a JWS and/or encrypting a JWE. |
||||
* |
||||
* @author Anoop Garlapati |
||||
* @author Joe Grandja |
||||
* @since 0.0.1 |
||||
* @see Jwt |
||||
* @see JoseHeader |
||||
* @see JwtClaimsSet |
||||
* @see JwtDecoder |
||||
* @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/rfc7516">JSON Web Encryption (JWE)</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://tools.ietf.org/html/rfc7516#section-3.1">JWE Compact Serialization</a> |
||||
*/ |
||||
@FunctionalInterface |
||||
public interface JwtEncoder { |
||||
|
||||
/** |
||||
* Encode the JWT to it's compact claims representation format. |
||||
* |
||||
* @param headers the JOSE header |
||||
* @param claims the JWT Claims Set |
||||
* @return a {@link Jwt} |
||||
* @throws JwtEncodingException if an error occurs while attempting to encode the JWT |
||||
*/ |
||||
Jwt encode(JoseHeader headers, JwtClaimsSet claims) throws JwtEncodingException; |
||||
|
||||
} |
||||
@ -0,0 +1,46 @@
@@ -0,0 +1,46 @@
|
||||
/* |
||||
* 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.jwt; |
||||
|
||||
/** |
||||
* This exception is thrown when an error occurs |
||||
* while attempting to encode a JSON Web Token (JWT). |
||||
* |
||||
* @author Joe Grandja |
||||
* @since 0.0.1 |
||||
*/ |
||||
public class JwtEncodingException extends JwtException { |
||||
|
||||
/** |
||||
* Constructs a {@code JwtEncodingException} using the provided parameters. |
||||
* |
||||
* @param message the detail message |
||||
*/ |
||||
public JwtEncodingException(String message) { |
||||
super(message); |
||||
} |
||||
|
||||
/** |
||||
* Constructs a {@code JwtEncodingException} using the provided parameters. |
||||
* |
||||
* @param message the detail message |
||||
* @param cause the root cause |
||||
*/ |
||||
public JwtEncodingException(String message, Throwable cause) { |
||||
super(message, cause); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,107 @@
@@ -0,0 +1,107 @@
|
||||
/* |
||||
* 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; |
||||
|
||||
import org.junit.Test; |
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy; |
||||
|
||||
/** |
||||
* Tests for {@link JoseHeader}. |
||||
* |
||||
* @author Joe Grandja |
||||
*/ |
||||
public class JoseHeaderTests { |
||||
|
||||
@Test |
||||
public void withAlgorithmWhenNullThenThrowIllegalArgumentException() { |
||||
assertThatThrownBy(() -> JoseHeader.withAlgorithm(null)) |
||||
.isInstanceOf(IllegalArgumentException.class) |
||||
.hasMessage("jwsAlgorithm cannot be null"); |
||||
} |
||||
|
||||
@Test |
||||
public void buildWhenAllHeadersProvidedThenAllHeadersAreSet() { |
||||
JoseHeader expectedJoseHeader = TestJoseHeaders.joseHeader().build(); |
||||
|
||||
JoseHeader joseHeader = JoseHeader.withAlgorithm(expectedJoseHeader.getJwsAlgorithm()) |
||||
.jwkSetUri(expectedJoseHeader.getJwkSetUri()) |
||||
.jwk(expectedJoseHeader.getJwk()) |
||||
.keyId(expectedJoseHeader.getKeyId()) |
||||
.x509Uri(expectedJoseHeader.getX509Uri()) |
||||
.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")) |
||||
.build(); |
||||
|
||||
assertThat(joseHeader.getJwsAlgorithm()).isEqualTo(expectedJoseHeader.getJwsAlgorithm()); |
||||
assertThat(joseHeader.getJwkSetUri()).isEqualTo(expectedJoseHeader.getJwkSetUri()); |
||||
assertThat(joseHeader.getJwk()).isEqualTo(expectedJoseHeader.getJwk()); |
||||
assertThat(joseHeader.getKeyId()).isEqualTo(expectedJoseHeader.getKeyId()); |
||||
assertThat(joseHeader.getX509Uri()).isEqualTo(expectedJoseHeader.getX509Uri()); |
||||
assertThat(joseHeader.getX509CertificateChain()).isEqualTo(expectedJoseHeader.getX509CertificateChain()); |
||||
assertThat(joseHeader.getX509SHA1Thumbprint()).isEqualTo(expectedJoseHeader.getX509SHA1Thumbprint()); |
||||
assertThat(joseHeader.getX509SHA256Thumbprint()).isEqualTo(expectedJoseHeader.getX509SHA256Thumbprint()); |
||||
assertThat(joseHeader.getCritical()).isEqualTo(expectedJoseHeader.getCritical()); |
||||
assertThat(joseHeader.getType()).isEqualTo(expectedJoseHeader.getType()); |
||||
assertThat(joseHeader.getContentType()).isEqualTo(expectedJoseHeader.getContentType()); |
||||
assertThat(joseHeader.<String>getHeader("custom-header-name")).isEqualTo("custom-header-value"); |
||||
assertThat(joseHeader.getHeaders()).isEqualTo(expectedJoseHeader.getHeaders()); |
||||
} |
||||
|
||||
@Test |
||||
public void fromWhenNullThenThrowIllegalArgumentException() { |
||||
assertThatThrownBy(() -> JoseHeader.from(null)) |
||||
.isInstanceOf(IllegalArgumentException.class) |
||||
.hasMessage("headers cannot be null"); |
||||
} |
||||
|
||||
@Test |
||||
public void fromWhenHeadersProvidedThenCopied() { |
||||
JoseHeader expectedJoseHeader = TestJoseHeaders.joseHeader().build(); |
||||
JoseHeader joseHeader = JoseHeader.from(expectedJoseHeader).build(); |
||||
assertThat(joseHeader.getHeaders()).isEqualTo(expectedJoseHeader.getHeaders()); |
||||
} |
||||
|
||||
@Test |
||||
public void headerWhenNameNullThenThrowIllegalArgumentException() { |
||||
assertThatThrownBy(() -> JoseHeader.withAlgorithm(SignatureAlgorithm.RS256).header(null, "value")) |
||||
.isInstanceOf(IllegalArgumentException.class) |
||||
.hasMessage("name cannot be empty"); |
||||
} |
||||
|
||||
@Test |
||||
public void headerWhenValueNullThenThrowIllegalArgumentException() { |
||||
assertThatThrownBy(() -> JoseHeader.withAlgorithm(SignatureAlgorithm.RS256).header("name", null)) |
||||
.isInstanceOf(IllegalArgumentException.class) |
||||
.hasMessage("value cannot be null"); |
||||
} |
||||
|
||||
@Test |
||||
public void getHeaderWhenNullThenThrowIllegalArgumentException() { |
||||
JoseHeader joseHeader = TestJoseHeaders.joseHeader().build(); |
||||
|
||||
assertThatThrownBy(() -> joseHeader.getHeader(null)) |
||||
.isInstanceOf(IllegalArgumentException.class) |
||||
.hasMessage("name cannot be empty"); |
||||
} |
||||
} |
||||
@ -0,0 +1,57 @@
@@ -0,0 +1,57 @@
|
||||
/* |
||||
* 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; |
||||
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; |
||||
|
||||
import java.util.Arrays; |
||||
import java.util.Collections; |
||||
import java.util.HashMap; |
||||
import java.util.Map; |
||||
import java.util.UUID; |
||||
|
||||
/** |
||||
* @author Joe Grandja |
||||
*/ |
||||
public class TestJoseHeaders { |
||||
|
||||
public static JoseHeader.Builder joseHeader() { |
||||
return joseHeader(SignatureAlgorithm.RS256); |
||||
} |
||||
|
||||
public static JoseHeader.Builder joseHeader(SignatureAlgorithm signatureAlgorithm) { |
||||
return JoseHeader.withAlgorithm(signatureAlgorithm) |
||||
.jwkSetUri("https://provider.com/oauth2/jwks") |
||||
.jwk(rsaJwk()) |
||||
.keyId(UUID.randomUUID().toString()) |
||||
.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"); |
||||
} |
||||
|
||||
private static Map<String, Object> rsaJwk() { |
||||
Map<String, Object> rsaJwk = new HashMap<>(); |
||||
rsaJwk.put("kty", "RSA"); |
||||
rsaJwk.put("n", "modulus"); |
||||
rsaJwk.put("e", "exponent"); |
||||
return rsaJwk; |
||||
} |
||||
} |
||||
@ -0,0 +1,159 @@
@@ -0,0 +1,159 @@
|
||||
/* |
||||
* 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.keys.KeyManager; |
||||
import org.springframework.security.crypto.keys.ManagedKey; |
||||
import org.springframework.security.crypto.keys.TestManagedKeys; |
||||
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.time.Instant; |
||||
import java.time.temporal.ChronoUnit; |
||||
import java.util.Collections; |
||||
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; |
||||
|
||||
/** |
||||
* Tests for {@link NimbusJwsEncoder}. |
||||
* |
||||
* @author Joe Grandja |
||||
*/ |
||||
public class NimbusJwsEncoderTests { |
||||
private KeyManager keyManager; |
||||
private NimbusJwsEncoder jwtEncoder; |
||||
|
||||
@Before |
||||
public void setUp() { |
||||
this.keyManager = mock(KeyManager.class); |
||||
this.jwtEncoder = new NimbusJwsEncoder(this.keyManager); |
||||
} |
||||
|
||||
@Test |
||||
public void constructorWhenKeyManagerNullThenThrowIllegalArgumentException() { |
||||
assertThatThrownBy(() -> new NimbusJwsEncoder(null)) |
||||
.isInstanceOf(IllegalArgumentException.class) |
||||
.hasMessage("keyManager 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() { |
||||
ManagedKey managedKey = TestManagedKeys.ecManagedKey().build(); |
||||
when(this.keyManager.findByAlgorithm(any())).thenReturn(Collections.singleton(managedKey)); |
||||
|
||||
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() { |
||||
ManagedKey managedKey = TestManagedKeys.rsaManagedKey().build(); |
||||
when(this.keyManager.findByAlgorithm(any())).thenReturn(Collections.singleton(managedKey)); |
||||
|
||||
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) managedKey.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())); |
||||
|
||||
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) managedKeyActivatedToday.getPublicKey()).build(); |
||||
jwtDecoder.decode(jws.getTokenValue()); |
||||
} |
||||
} |
||||
@ -0,0 +1,90 @@
@@ -0,0 +1,90 @@
|
||||
/* |
||||
* 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.jwt; |
||||
|
||||
import org.junit.Test; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy; |
||||
|
||||
/** |
||||
* Tests for {@link JwtClaimsSet}. |
||||
* |
||||
* @author Joe Grandja |
||||
*/ |
||||
public class JwtClaimsSetTests { |
||||
|
||||
@Test |
||||
public void buildWhenClaimsEmptyThenThrowIllegalArgumentException() { |
||||
assertThatThrownBy(() -> JwtClaimsSet.withClaims().build()) |
||||
.isInstanceOf(IllegalArgumentException.class) |
||||
.hasMessage("claims cannot be empty"); |
||||
} |
||||
|
||||
@Test |
||||
public void buildWhenAllClaimsProvidedThenAllClaimsAreSet() { |
||||
JwtClaimsSet expectedJwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build(); |
||||
|
||||
JwtClaimsSet jwtClaimsSet = JwtClaimsSet.withClaims() |
||||
.issuer(expectedJwtClaimsSet.getIssuer()) |
||||
.subject(expectedJwtClaimsSet.getSubject()) |
||||
.audience(expectedJwtClaimsSet.getAudience()) |
||||
.issuedAt(expectedJwtClaimsSet.getIssuedAt()) |
||||
.notBefore(expectedJwtClaimsSet.getNotBefore()) |
||||
.expiresAt(expectedJwtClaimsSet.getExpiresAt()) |
||||
.id(expectedJwtClaimsSet.getId()) |
||||
.claims(claims -> claims.put("custom-claim-name", "custom-claim-value")) |
||||
.build(); |
||||
|
||||
assertThat(jwtClaimsSet.getIssuer()).isEqualTo(expectedJwtClaimsSet.getIssuer()); |
||||
assertThat(jwtClaimsSet.getSubject()).isEqualTo(expectedJwtClaimsSet.getSubject()); |
||||
assertThat(jwtClaimsSet.getAudience()).isEqualTo(expectedJwtClaimsSet.getAudience()); |
||||
assertThat(jwtClaimsSet.getIssuedAt()).isEqualTo(expectedJwtClaimsSet.getIssuedAt()); |
||||
assertThat(jwtClaimsSet.getNotBefore()).isEqualTo(expectedJwtClaimsSet.getNotBefore()); |
||||
assertThat(jwtClaimsSet.getExpiresAt()).isEqualTo(expectedJwtClaimsSet.getExpiresAt()); |
||||
assertThat(jwtClaimsSet.getId()).isEqualTo(expectedJwtClaimsSet.getId()); |
||||
assertThat(jwtClaimsSet.<String>getClaim("custom-claim-name")).isEqualTo("custom-claim-value"); |
||||
assertThat(jwtClaimsSet.getClaims()).isEqualTo(expectedJwtClaimsSet.getClaims()); |
||||
} |
||||
|
||||
@Test |
||||
public void fromWhenNullThenThrowIllegalArgumentException() { |
||||
assertThatThrownBy(() -> JwtClaimsSet.from(null)) |
||||
.isInstanceOf(IllegalArgumentException.class) |
||||
.hasMessage("claims cannot be null"); |
||||
} |
||||
|
||||
@Test |
||||
public void fromWhenClaimsProvidedThenCopied() { |
||||
JwtClaimsSet expectedJwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build(); |
||||
JwtClaimsSet jwtClaimsSet = JwtClaimsSet.from(expectedJwtClaimsSet).build(); |
||||
assertThat(jwtClaimsSet.getClaims()).isEqualTo(expectedJwtClaimsSet.getClaims()); |
||||
} |
||||
|
||||
@Test |
||||
public void claimWhenNameNullThenThrowIllegalArgumentException() { |
||||
assertThatThrownBy(() -> JwtClaimsSet.withClaims().claim(null, "value")) |
||||
.isInstanceOf(IllegalArgumentException.class) |
||||
.hasMessage("name cannot be empty"); |
||||
} |
||||
|
||||
@Test |
||||
public void claimWhenValueNullThenThrowIllegalArgumentException() { |
||||
assertThatThrownBy(() -> JwtClaimsSet.withClaims().claim("name", null)) |
||||
.isInstanceOf(IllegalArgumentException.class) |
||||
.hasMessage("value cannot be null"); |
||||
} |
||||
} |
||||
@ -0,0 +1,50 @@
@@ -0,0 +1,50 @@
|
||||
/* |
||||
* 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.jwt; |
||||
|
||||
import java.net.MalformedURLException; |
||||
import java.net.URI; |
||||
import java.net.URL; |
||||
import java.time.Instant; |
||||
import java.time.temporal.ChronoUnit; |
||||
import java.util.Collections; |
||||
import java.util.UUID; |
||||
|
||||
/** |
||||
* @author Joe Grandja |
||||
*/ |
||||
public class TestJwtClaimsSets { |
||||
|
||||
public static JwtClaimsSet.Builder jwtClaimsSet() { |
||||
URL issuer = null; |
||||
try { |
||||
issuer = URI.create("https://provider.com").toURL(); |
||||
} catch (MalformedURLException e) { } |
||||
|
||||
Instant issuedAt = Instant.now(); |
||||
Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS); |
||||
|
||||
return JwtClaimsSet.withClaims() |
||||
.issuer(issuer) |
||||
.subject("subject") |
||||
.audience(Collections.singletonList("client-1")) |
||||
.issuedAt(issuedAt) |
||||
.notBefore(issuedAt) |
||||
.expiresAt(expiresAt) |
||||
.id(UUID.randomUUID().toString()) |
||||
.claim("custom-claim-name", "custom-claim-value"); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue