diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java index 2749aae..f53976e 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,18 +15,28 @@ */ package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization; +import java.net.URI; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; + import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.context.ApplicationContext; +import org.springframework.core.ResolvableType; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer; -import org.springframework.security.crypto.key.CryptoKeySource; -import org.springframework.security.oauth2.jose.jws.NimbusJwsEncoder; import org.springframework.security.oauth2.jwt.JwtEncoder; +import org.springframework.security.oauth2.jwt.NimbusJwsEncoder; import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationProvider; @@ -36,12 +46,12 @@ import org.springframework.security.oauth2.server.authorization.authentication.O import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationProvider; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.config.ProviderSettings; -import org.springframework.security.oauth2.server.authorization.web.JwkSetEndpointFilter; +import org.springframework.security.oauth2.server.authorization.oidc.web.OidcProviderConfigurationEndpointFilter; +import org.springframework.security.oauth2.server.authorization.web.NimbusJwkSetEndpointFilter; import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter; import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientAuthenticationFilter; import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter; import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenRevocationEndpointFilter; -import org.springframework.security.oauth2.server.authorization.oidc.web.OidcProviderConfigurationEndpointFilter; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint; @@ -55,12 +65,6 @@ import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; import org.springframework.util.StringUtils; -import java.net.URI; -import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - /** * An {@link AbstractHttpConfigurer} for OAuth 2.0 Authorization Server support. * @@ -73,7 +77,7 @@ import java.util.Map; * @see OAuth2AuthorizationEndpointFilter * @see OAuth2TokenEndpointFilter * @see OAuth2TokenRevocationEndpointFilter - * @see JwkSetEndpointFilter + * @see NimbusJwkSetEndpointFilter * @see OidcProviderConfigurationEndpointFilter * @see OAuth2ClientAuthenticationFilter */ @@ -92,7 +96,7 @@ public final class OAuth2AuthorizationServerConfigurer keySource(CryptoKeySource keySource) { - Assert.notNull(keySource, "keySource cannot be null"); - this.getBuilder().setSharedObject(CryptoKeySource.class, keySource); - return this; - } - /** * Sets the provider settings. * @@ -219,8 +211,9 @@ public final class OAuth2AuthorizationServerConfigurer jwkSource = getJwkSource(builder); + NimbusJwkSetEndpointFilter jwkSetEndpointFilter = new NimbusJwkSetEndpointFilter( + jwkSource, providerSettings.jwkSetEndpoint()); builder.addFilterBefore(postProcess(jwkSetEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class); @@ -284,21 +277,27 @@ public final class OAuth2AuthorizationServerConfigurer> JwtEncoder getJwtEncoder(B builder) { - JwtEncoder jwtEncoder = getOptionalBean(builder, JwtEncoder.class); + JwtEncoder jwtEncoder = builder.getSharedObject(JwtEncoder.class); if (jwtEncoder == null) { - CryptoKeySource keySource = getKeySource(builder); - jwtEncoder = new NimbusJwsEncoder(keySource); + jwtEncoder = getOptionalBean(builder, JwtEncoder.class); + if (jwtEncoder == null) { + JWKSource jwkSource = getJwkSource(builder); + jwtEncoder = new NimbusJwsEncoder(jwkSource); + } + builder.setSharedObject(JwtEncoder.class, jwtEncoder); } return jwtEncoder; } - private static > CryptoKeySource getKeySource(B builder) { - CryptoKeySource keySource = builder.getSharedObject(CryptoKeySource.class); - if (keySource == null) { - keySource = getBean(builder, CryptoKeySource.class); - builder.setSharedObject(CryptoKeySource.class, keySource); + @SuppressWarnings("unchecked") + private static > JWKSource getJwkSource(B builder) { + JWKSource jwkSource = builder.getSharedObject(JWKSource.class); + if (jwkSource == null) { + ResolvableType type = ResolvableType.forClassWithGenerics(JWKSource.class, SecurityContext.class); + jwkSource = getBean(builder, type); + builder.setSharedObject(JWKSource.class, jwkSource); } - return keySource; + return jwkSource; } private static > ProviderSettings getProviderSettings(B builder) { @@ -317,6 +316,19 @@ public final class OAuth2AuthorizationServerConfigurer, T> T getBean(B builder, ResolvableType type) { + ApplicationContext context = builder.getSharedObject(ApplicationContext.class); + String[] names = context.getBeanNamesForType(type); + if (names.length == 1) { + return (T) context.getBean(names[0]); + } + if (names.length > 1) { + throw new NoUniqueBeanDefinitionException(type, names); + } + throw new NoSuchBeanDefinitionException(type); + } + private static , T> T getOptionalBean(B builder, Class type) { Map beansMap = BeanFactoryUtils.beansOfTypeIncludingAncestors( builder.getSharedObject(ApplicationContext.class), type); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/crypto/key/AsymmetricKey.java b/oauth2-authorization-server/src/main/java/org/springframework/security/crypto/key/AsymmetricKey.java deleted file mode 100644 index 36b222e..0000000 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/crypto/key/AsymmetricKey.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.security.crypto.key; - -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; - -import java.security.PrivateKey; -import java.security.PublicKey; -import java.util.Map; -import java.util.UUID; - -/** - * A {@link CryptoKey} that holds a {@code java.security.PrivateKey} - * and {@code java.security.PublicKey} used for asymmetric algorithm's. - * - * @author Joe Grandja - * @since 0.1.0 - * @see CryptoKey - * @see PrivateKey - * @see PublicKey - */ -public final class AsymmetricKey extends CryptoKey { - private final PublicKey publicKey; - - private AsymmetricKey(PrivateKey privateKey, PublicKey publicKey, String id, Map 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 { - 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); - } - } -} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/crypto/key/CryptoKey.java b/oauth2-authorization-server/src/main/java/org/springframework/security/crypto/key/CryptoKey.java deleted file mode 100644 index f028ca3..0000000 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/crypto/key/CryptoKey.java +++ /dev/null @@ -1,231 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.security.crypto.key; - -import org.springframework.security.oauth2.server.authorization.Version; -import org.springframework.util.Assert; - -import javax.crypto.SecretKey; -import java.io.Serializable; -import java.security.Key; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Objects; -import java.util.function.Consumer; - -/** - * A holder of a {@code java.security.Key} used for cryptographic operations. - * - * @param the type of {@code java.security.Key} - * @author Joe Grandja - * @since 0.1.0 - * @see CryptoKeySource - */ -public class CryptoKey implements Serializable { - private static final long serialVersionUID = Version.SERIAL_VERSION_UID; - private final K key; - private final String id; - private final Map 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 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 the type of the metadata - * @return the metadata value, or {@code null} if not available - */ - @SuppressWarnings("unchecked") - public final 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 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 the type of {@link CryptoKey} - * @param the type of {@link AbstractBuilder} - */ - protected abstract static class AbstractBuilder, B extends AbstractBuilder> implements Serializable { - private static final long serialVersionUID = Version.SERIAL_VERSION_UID; - protected Key key; - protected String id; - protected Map 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> metadataConsumer) { - metadataConsumer.accept(this.metadata); - return (B) this; - } - - /** - * Creates the {@link CryptoKey}. - * - * @return the {@link CryptoKey} - */ - protected abstract T build(); - - } -} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/crypto/key/CryptoKeySource.java b/oauth2-authorization-server/src/main/java/org/springframework/security/crypto/key/CryptoKeySource.java deleted file mode 100644 index 657750c..0000000 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/crypto/key/CryptoKeySource.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.security.crypto.key; - -import java.util.Set; - -/** - * A source for {@link CryptoKey}'s. - * - * @author Joe Grandja - * @since 0.1.0 - * @see CryptoKey - * @see SymmetricKey - * @see AsymmetricKey - */ -public interface CryptoKeySource { - - /** - * Returns a {@code Set} of {@link CryptoKey}'s. - * - * @return a {@code Set} of {@link CryptoKey}'s - */ - Set> getKeys(); - -} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/crypto/key/StaticKeyGeneratingCryptoKeySource.java b/oauth2-authorization-server/src/main/java/org/springframework/security/crypto/key/StaticKeyGeneratingCryptoKeySource.java deleted file mode 100644 index f54cfd7..0000000 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/crypto/key/StaticKeyGeneratingCryptoKeySource.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.security.crypto.key; - -import javax.crypto.SecretKey; -import java.security.KeyPair; -import java.util.Collections; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static org.springframework.security.crypto.key.KeyGeneratorUtils.generateRsaKey; -import static org.springframework.security.crypto.key.KeyGeneratorUtils.generateSecretKey; - -/** - * An implementation of a {@link CryptoKeySource} that generates the {@link CryptoKey}'s when constructed. - * - *

- * NOTE: 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> keys; - - public StaticKeyGeneratingCryptoKeySource() { - this.keys = Collections.unmodifiableMap(generateKeys()); - } - - @Override - public Set> getKeys() { - return new HashSet<>(this.keys.values()); - } - - private static Map> 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)); - } -} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/crypto/key/SymmetricKey.java b/oauth2-authorization-server/src/main/java/org/springframework/security/crypto/key/SymmetricKey.java deleted file mode 100644 index 8f70efa..0000000 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/crypto/key/SymmetricKey.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.security.crypto.key; - -import org.springframework.util.StringUtils; - -import javax.crypto.SecretKey; -import java.util.Map; -import java.util.UUID; - -/** - * A {@link CryptoKey} that holds a {@code javax.crypto.SecretKey} - * used for symmetric algorithm's. - * - * @author Joe Grandja - * @since 0.1.0 - * @see CryptoKey - * @see SecretKey - */ -public final class SymmetricKey extends CryptoKey { - - private SymmetricKey(SecretKey key, String id, Map metadata) { - super(key, id, metadata); - } - - /** - * A builder for {@link SymmetricKey}. - */ - public static class Builder extends AbstractBuilder { - - 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); - } - } -} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/jose/JoseHeader.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/jwt/JoseHeader.java similarity index 82% rename from oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/jose/JoseHeader.java rename to oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/jwt/JoseHeader.java index 993f861..aad6963 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/jose/JoseHeader.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/jwt/JoseHeader.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,30 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.security.oauth2.jose; - -import org.springframework.security.oauth2.jose.jws.JwsAlgorithm; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.util.Assert; +package org.springframework.security.oauth2.jwt; +import java.net.URL; import java.util.Collections; -import java.util.LinkedHashMap; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Consumer; -import static org.springframework.security.oauth2.jose.JoseHeaderNames.ALG; -import static org.springframework.security.oauth2.jose.JoseHeaderNames.CRIT; -import static org.springframework.security.oauth2.jose.JoseHeaderNames.CTY; -import static org.springframework.security.oauth2.jose.JoseHeaderNames.JKU; -import static org.springframework.security.oauth2.jose.JoseHeaderNames.JWK; -import static org.springframework.security.oauth2.jose.JoseHeaderNames.KID; -import static org.springframework.security.oauth2.jose.JoseHeaderNames.TYP; -import static org.springframework.security.oauth2.jose.JoseHeaderNames.X5C; -import static org.springframework.security.oauth2.jose.JoseHeaderNames.X5T; -import static org.springframework.security.oauth2.jose.JoseHeaderNames.X5T_S256; -import static org.springframework.security.oauth2.jose.JoseHeaderNames.X5U; +import org.springframework.security.oauth2.core.converter.ClaimConversionService; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithm; +import org.springframework.util.Assert; /** * The JOSE header is a JSON object representing the header parameters of a JSON Web Token, @@ -55,16 +44,16 @@ public final class JoseHeader { private final Map headers; private JoseHeader(Map headers) { - this.headers = Collections.unmodifiableMap(new LinkedHashMap<>(headers)); + this.headers = Collections.unmodifiableMap(new HashMap<>(headers)); } /** - * Returns the JWS algorithm used to digitally sign the JWS. + * Returns the {@link JwsAlgorithm JWS algorithm} used to digitally sign the JWS. * * @return the JWS algorithm */ public JwsAlgorithm getJwsAlgorithm() { - return getHeader(ALG); + return getHeader(JoseHeaderNames.ALG); } /** @@ -73,8 +62,8 @@ public final class JoseHeader { * * @return the JWK Set URL */ - public String getJwkSetUri() { - return getHeader(JKU); + public URL getJwkSetUri() { + return getHeader(JoseHeaderNames.JKU); } /** @@ -84,7 +73,7 @@ public final class JoseHeader { * @return the JSON Web Key */ public Map getJwk() { - return getHeader(JWK); + return getHeader(JoseHeaderNames.JWK); } /** @@ -93,7 +82,7 @@ public final class JoseHeader { * @return the key ID */ public String getKeyId() { - return getHeader(KID); + return getHeader(JoseHeaderNames.KID); } /** @@ -102,8 +91,8 @@ public final class JoseHeader { * * @return the X.509 URL */ - public String getX509Uri() { - return getHeader(X5U); + public URL getX509Uri() { + return getHeader(JoseHeaderNames.X5U); } /** @@ -113,7 +102,7 @@ public final class JoseHeader { * @return the X.509 certificate chain */ public List getX509CertificateChain() { - return getHeader(X5C); + return getHeader(JoseHeaderNames.X5C); } /** @@ -123,7 +112,7 @@ public final class JoseHeader { * @return the X.509 certificate SHA-1 thumbprint */ public String getX509SHA1Thumbprint() { - return getHeader(X5T); + return getHeader(JoseHeaderNames.X5T); } /** @@ -133,7 +122,7 @@ public final class JoseHeader { * @return the X.509 certificate SHA-256 thumbprint */ public String getX509SHA256Thumbprint() { - return getHeader(X5T_S256); + return getHeader(JoseHeaderNames.X5T_S256); } /** @@ -143,7 +132,7 @@ public final class JoseHeader { * @return the critical headers */ public Set getCritical() { - return getHeader(CRIT); + return getHeader(JoseHeaderNames.CRIT); } /** @@ -152,7 +141,7 @@ public final class JoseHeader { * @return the type header */ public String getType() { - return getHeader(TYP); + return getHeader(JoseHeaderNames.TYP); } /** @@ -161,7 +150,7 @@ public final class JoseHeader { * @return the content type header */ public String getContentType() { - return getHeader(CTY); + return getHeader(JoseHeaderNames.CTY); } /** @@ -209,12 +198,12 @@ public final class JoseHeader { /** * A builder for {@link JoseHeader}. */ - public static class Builder { - private final Map headers = new LinkedHashMap<>(); + public static final class Builder { + private final Map headers = new HashMap<>(); private Builder(JwsAlgorithm jwsAlgorithm) { Assert.notNull(jwsAlgorithm, "jwsAlgorithm cannot be null"); - header(ALG, jwsAlgorithm); + header(JoseHeaderNames.ALG, jwsAlgorithm); } private Builder(JoseHeader headers) { @@ -230,7 +219,7 @@ public final class JoseHeader { * @return the {@link Builder} */ public Builder jwkSetUri(String jwkSetUri) { - return header(JKU, jwkSetUri); + return header(JoseHeaderNames.JKU, jwkSetUri); } /** @@ -241,7 +230,7 @@ public final class JoseHeader { * @return the {@link Builder} */ public Builder jwk(Map jwk) { - return header(JWK, jwk); + return header(JoseHeaderNames.JWK, jwk); } /** @@ -251,7 +240,7 @@ public final class JoseHeader { * @return the {@link Builder} */ public Builder keyId(String keyId) { - return header(KID, keyId); + return header(JoseHeaderNames.KID, keyId); } /** @@ -262,7 +251,7 @@ public final class JoseHeader { * @return the {@link Builder} */ public Builder x509Uri(String x509Uri) { - return header(X5U, x509Uri); + return header(JoseHeaderNames.X5U, x509Uri); } /** @@ -273,7 +262,7 @@ public final class JoseHeader { * @return the {@link Builder} */ public Builder x509CertificateChain(List x509CertificateChain) { - return header(X5C, x509CertificateChain); + return header(JoseHeaderNames.X5C, x509CertificateChain); } /** @@ -284,7 +273,7 @@ public final class JoseHeader { * @return the {@link Builder} */ public Builder x509SHA1Thumbprint(String x509SHA1Thumbprint) { - return header(X5T, x509SHA1Thumbprint); + return header(JoseHeaderNames.X5T, x509SHA1Thumbprint); } /** @@ -295,7 +284,7 @@ public final class JoseHeader { * @return the {@link Builder} */ public Builder x509SHA256Thumbprint(String x509SHA256Thumbprint) { - return header(X5T_S256, x509SHA256Thumbprint); + return header(JoseHeaderNames.X5T_S256, x509SHA256Thumbprint); } /** @@ -306,7 +295,7 @@ public final class JoseHeader { * @return the {@link Builder} */ public Builder critical(Set headerNames) { - return header(CRIT, headerNames); + return header(JoseHeaderNames.CRIT, headerNames); } /** @@ -316,7 +305,7 @@ public final class JoseHeader { * @return the {@link Builder} */ public Builder type(String type) { - return header(TYP, type); + return header(JoseHeaderNames.TYP, type); } /** @@ -326,7 +315,7 @@ public final class JoseHeader { * @return the {@link Builder} */ public Builder contentType(String contentType) { - return header(CTY, contentType); + return header(JoseHeaderNames.CTY, contentType); } /** @@ -362,7 +351,19 @@ public final class JoseHeader { */ public JoseHeader build() { Assert.notEmpty(this.headers, "headers cannot be empty"); + convertAsURL(JoseHeaderNames.JKU); + convertAsURL(JoseHeaderNames.X5U); return new JoseHeader(this.headers); } + + private void convertAsURL(String header) { + Object value = this.headers.get(header); + if (value != null) { + URL convertedValue = ClaimConversionService.getSharedInstance().convert(value, URL.class); + Assert.isTrue(convertedValue != null, + () -> "Unable to convert header '" + header + "' of type '" + value.getClass() + "' to URL."); + this.headers.put(header, convertedValue); + } + } } } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/jose/JoseHeaderNames.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/jwt/JoseHeaderNames.java similarity index 83% rename from oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/jose/JoseHeaderNames.java rename to oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/jwt/JoseHeaderNames.java index d259d99..e53b66e 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/jose/JoseHeaderNames.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/jwt/JoseHeaderNames.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.security.oauth2.jose; +package org.springframework.security.oauth2.jwt; /** * The Registered Header Parameter Names defined by the JSON Web Token (JWT), @@ -28,69 +28,72 @@ package org.springframework.security.oauth2.jose; * @see JWS JOSE Header * @see JWE JOSE Header */ -public interface JoseHeaderNames { +public final class JoseHeaderNames { /** * {@code alg} - the algorithm header identifies the cryptographic algorithm used to secure a JWS or JWE */ - String ALG = "alg"; + public static final String ALG = "alg"; /** * {@code jku} - the JWK Set URL header is a URI that refers to a resource for a set of JSON-encoded public keys, * one of which corresponds to the key used to digitally sign a JWS or encrypt a JWE */ - String JKU = "jku"; + public static final String JKU = "jku"; /** * {@code jwk} - the JSON Web Key header is the public key that corresponds to the key * used to digitally sign a JWS or encrypt a JWE */ - String JWK = "jwk"; + public static final String JWK = "jwk"; /** * {@code kid} - the key ID header is a hint indicating which key was used to secure a JWS or JWE */ - String KID = "kid"; + public static final String KID = "kid"; /** * {@code x5u} - the X.509 URL header is a URI that refers to a resource for the X.509 public key certificate * or certificate chain corresponding to the key used to digitally sign a JWS or encrypt a JWE */ - String X5U = "x5u"; + public static final String X5U = "x5u"; /** * {@code x5c} - the X.509 certificate chain header contains the X.509 public key certificate * or certificate chain corresponding to the key used to digitally sign a JWS or encrypt a JWE */ - String X5C = "x5c"; + public static final String X5C = "x5c"; /** * {@code x5t} - the X.509 certificate SHA-1 thumbprint header is a base64url-encoded SHA-1 thumbprint (a.k.a. digest) * of the DER encoding of the X.509 certificate corresponding to the key used to digitally sign a JWS or encrypt a JWE */ - String X5T = "x5t"; + public static final String X5T = "x5t"; /** * {@code x5t#S256} - the X.509 certificate SHA-256 thumbprint header is a base64url-encoded SHA-256 thumbprint (a.k.a. digest) * of the DER encoding of the X.509 certificate corresponding to the key used to digitally sign a JWS or encrypt a JWE */ - String X5T_S256 = "x5t#S256"; + public static final String X5T_S256 = "x5t#S256"; /** * {@code typ} - the type header is used by JWS/JWE applications to declare the media type of a JWS/JWE */ - String TYP = "typ"; + public static final String TYP = "typ"; /** * {@code cty} - the content type header is used by JWS/JWE applications to declare the media type * of the secured content (the payload) */ - String CTY = "cty"; + public static final String CTY = "cty"; /** * {@code crit} - the critical header indicates that extensions to the JWS/JWE/JWA specifications * are being used that MUST be understood and processed */ - String CRIT = "crit"; + public static final String CRIT = "crit"; + + private JoseHeaderNames() { + } } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/jwt/JwtClaimsSet.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/jwt/JwtClaimsSet.java index cd51b3c..f7f84e8 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/jwt/JwtClaimsSet.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/jwt/JwtClaimsSet.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,22 +15,14 @@ */ package org.springframework.security.oauth2.jwt; -import org.springframework.util.Assert; - import java.time.Instant; import java.util.Collections; -import java.util.LinkedHashMap; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Consumer; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.AUD; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.EXP; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.IAT; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.ISS; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.JTI; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.NBF; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.SUB; +import org.springframework.util.Assert; /** * The {@link Jwt JWT} Claims Set is a JSON object representing the claims conveyed by a JSON Web Token. @@ -46,7 +38,7 @@ public final class JwtClaimsSet implements JwtClaimAccessor { private final Map claims; private JwtClaimsSet(Map claims) { - this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims)); + this.claims = Collections.unmodifiableMap(new HashMap<>(claims)); } @Override @@ -76,8 +68,8 @@ public final class JwtClaimsSet implements JwtClaimAccessor { /** * A builder for {@link JwtClaimsSet}. */ - public static class Builder { - private final Map claims = new LinkedHashMap<>(); + public static final class Builder { + private final Map claims = new HashMap<>(); private Builder() { } @@ -94,7 +86,7 @@ public final class JwtClaimsSet implements JwtClaimAccessor { * @return the {@link Builder} */ public Builder issuer(String issuer) { - return claim(ISS, issuer); + return claim(JwtClaimNames.ISS, issuer); } /** @@ -104,7 +96,7 @@ public final class JwtClaimsSet implements JwtClaimAccessor { * @return the {@link Builder} */ public Builder subject(String subject) { - return claim(SUB, subject); + return claim(JwtClaimNames.SUB, subject); } /** @@ -114,7 +106,7 @@ public final class JwtClaimsSet implements JwtClaimAccessor { * @return the {@link Builder} */ public Builder audience(List audience) { - return claim(AUD, audience); + return claim(JwtClaimNames.AUD, audience); } /** @@ -125,7 +117,7 @@ public final class JwtClaimsSet implements JwtClaimAccessor { * @return the {@link Builder} */ public Builder expiresAt(Instant expiresAt) { - return claim(EXP, expiresAt); + return claim(JwtClaimNames.EXP, expiresAt); } /** @@ -136,7 +128,7 @@ public final class JwtClaimsSet implements JwtClaimAccessor { * @return the {@link Builder} */ public Builder notBefore(Instant notBefore) { - return claim(NBF, notBefore); + return claim(JwtClaimNames.NBF, notBefore); } /** @@ -146,7 +138,7 @@ public final class JwtClaimsSet implements JwtClaimAccessor { * @return the {@link Builder} */ public Builder issuedAt(Instant issuedAt) { - return claim(IAT, issuedAt); + return claim(JwtClaimNames.IAT, issuedAt); } /** @@ -156,7 +148,7 @@ public final class JwtClaimsSet implements JwtClaimAccessor { * @return the {@link Builder} */ public Builder id(String jti) { - return claim(JTI, jti); + return claim(JwtClaimNames.JTI, jti); } /** diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/jwt/JwtEncoder.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/jwt/JwtEncoder.java index 846a0df..b960b03 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/jwt/JwtEncoder.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/jwt/JwtEncoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,6 @@ */ package org.springframework.security.oauth2.jwt; -import org.springframework.security.oauth2.jose.JoseHeader; - /** * Implementations of this interface are responsible for encoding * a JSON Web Token (JWT) to it's compact claims representation format. diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/jose/jws/NimbusJwsEncoder.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwsEncoder.java similarity index 52% rename from oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/jose/jws/NimbusJwsEncoder.java rename to oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwsEncoder.java index 943e24d..9374c9d 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/jose/jws/NimbusJwsEncoder.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwsEncoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,54 +13,49 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.security.oauth2.jose.jws; +package org.springframework.security.oauth2.jwt; + +import java.net.URL; +import java.time.Instant; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.JOSEObjectType; import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSHeader; import com.nimbusds.jose.JWSSigner; -import com.nimbusds.jose.KeyLengthException; -import com.nimbusds.jose.crypto.MACSigner; -import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jose.KeySourceException; +import com.nimbusds.jose.crypto.factories.DefaultJWSSignerFactory; import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKMatcher; +import com.nimbusds.jose.jwk.JWKSelector; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jose.produce.JWSSignerFactory; import com.nimbusds.jose.util.Base64; import com.nimbusds.jose.util.Base64URL; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; + import org.springframework.core.convert.converter.Converter; -import org.springframework.security.crypto.key.AsymmetricKey; -import org.springframework.security.crypto.key.CryptoKey; -import org.springframework.security.crypto.key.CryptoKeySource; -import org.springframework.security.oauth2.jose.JoseHeader; -import org.springframework.security.oauth2.jose.JoseHeaderNames; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.jwt.JwtClaimsSet; -import org.springframework.security.oauth2.jwt.JwtEncoder; -import org.springframework.security.oauth2.jwt.JwtEncodingException; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithm; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; -import javax.crypto.SecretKey; -import java.net.URI; -import java.net.URL; -import java.security.PrivateKey; -import java.time.Instant; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; -import java.util.function.BiConsumer; -import java.util.stream.Collectors; - /** - * An implementation of a {@link JwtEncoder} that encodes a JSON Web Token (JWT) - * using the JSON Web Signature (JWS) Compact Serialization format. - * The private/secret key used for signing the JWS is obtained - * from the {@link CryptoKeySource} supplied via the constructor. + * An implementation of a {@link JwtEncoder} that encodes a JSON Web Token (JWT) using the + * JSON Web Signature (JWS) Compact Serialization format. The private/secret key used for + * signing the JWS is supplied by the {@code com.nimbusds.jose.jwk.source.JWKSource} + * provided via the constructor. * *

* NOTE: This implementation uses the Nimbus JOSE + JWT SDK. @@ -68,50 +63,46 @@ import java.util.stream.Collectors; * @author Joe Grandja * @since 0.0.1 * @see JwtEncoder - * @see CryptoKeySource - * @see JSON Web Token (JWT) - * @see JSON Web Signature (JWS) - * @see JWS Compact Serialization - * @see Nimbus JOSE + JWT SDK + * @see com.nimbusds.jose.jwk.source.JWKSource + * @see com.nimbusds.jose.jwk.JWK + * @see JSON Web Token + * (JWT) + * @see JSON Web Signature + * (JWS) + * @see JWS + * Compact Serialization + * @see Nimbus + * JOSE + JWT SDK */ 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 jcaKeyAlgorithmMappings = new HashMap() { - { - put(MacAlgorithm.HS256, "HmacSHA256"); - put(MacAlgorithm.HS384, "HmacSHA384"); - put(MacAlgorithm.HS512, "HmacSHA512"); - put(SignatureAlgorithm.RS256, RSA_KEY_TYPE); - put(SignatureAlgorithm.RS384, RSA_KEY_TYPE); - put(SignatureAlgorithm.RS512, RSA_KEY_TYPE); - put(SignatureAlgorithm.ES256, EC_KEY_TYPE); - put(SignatureAlgorithm.ES384, EC_KEY_TYPE); - put(SignatureAlgorithm.ES512, EC_KEY_TYPE); - } + + private static final String ENCODING_ERROR_MESSAGE_TEMPLATE = "An error occurred while attempting to encode the Jwt: %s"; + + private static final Converter JWS_HEADER_CONVERTER = new JwsHeaderConverter(); + + private static final Converter JWT_CLAIMS_SET_CONVERTER = new JwtClaimsSetConverter(); + + private static final JWSSignerFactory JWS_SIGNER_FACTORY = new DefaultJWSSignerFactory(); + + private final Map jwsSigners = new ConcurrentHashMap<>(); + + private final JWKSource jwkSource; + + private BiConsumer jwtCustomizer = (headers, claims) -> { }; - private static final Converter jwsHeaderConverter = new JwsHeaderConverter(); - private static final Converter jwtClaimsSetConverter = new JwtClaimsSetConverter(); - private final CryptoKeySource keySource; - private BiConsumer jwtCustomizer = (headers, claims) -> {}; /** * Constructs a {@code NimbusJwsEncoder} using the provided parameters. - * - * @param keySource the source for cryptographic keys + * @param jwkSource the {@code com.nimbusds.jose.jwk.source.JWKSource} */ - public NimbusJwsEncoder(CryptoKeySource keySource) { - Assert.notNull(keySource, "keySource cannot be null"); - this.keySource = keySource; + public NimbusJwsEncoder(JWKSource jwkSource) { + Assert.notNull(jwkSource, "jwkSource cannot be null"); + this.jwkSource = jwkSource; } /** - * Sets the {@link Jwt} customizer to be provided the - * {@link JoseHeader.Builder} and {@link JwtClaimsSet.Builder} - * allowing for further customizations. - * + * Sets the {@link Jwt} customizer to be provided the {@link JoseHeader.Builder} and + * {@link JwtClaimsSet.Builder} allowing for further customizations. * @param jwtCustomizer the {@link Jwt} customizer to be provided the * {@link JoseHeader.Builder} and {@link JwtClaimsSet.Builder} */ @@ -125,78 +116,85 @@ public final class NimbusJwsEncoder implements JwtEncoder { Assert.notNull(headers, "headers cannot be null"); Assert.notNull(claims, "claims cannot be null"); - CryptoKey cryptoKey = selectKey(headers); - if (cryptoKey == null) { - throw new JwtEncodingException(String.format( - ENCODING_ERROR_MESSAGE_TEMPLATE, - "Unsupported key for algorithm '" + headers.getJwsAlgorithm().getName() + "'")); - } - - JWSSigner jwsSigner; - if (AsymmetricKey.class.isAssignableFrom(cryptoKey.getClass())) { - if (!cryptoKey.getAlgorithm().equals(RSA_KEY_TYPE)) { - throw new JwtEncodingException(String.format( - ENCODING_ERROR_MESSAGE_TEMPLATE, - "Unsupported key type '" + cryptoKey.getAlgorithm() + "'")); - } - PrivateKey privateKey = (PrivateKey) cryptoKey.getKey(); - jwsSigner = new RSASSASigner(privateKey); - } else { - SecretKey secretKey = (SecretKey) cryptoKey.getKey(); - try { - jwsSigner = new MACSigner(secretKey); - } catch (KeyLengthException ex) { - throw new JwtEncodingException(String.format( - ENCODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex); - } - } - + // @formatter:off JoseHeader.Builder headersBuilder = JoseHeader.from(headers) - .type(JOSEObjectType.JWT.getType()) - .keyId(cryptoKey.getId()); + .type(JOSEObjectType.JWT.getType()); JwtClaimsSet.Builder claimsBuilder = JwtClaimsSet.from(claims) .id(UUID.randomUUID().toString()); + // @formatter:on this.jwtCustomizer.accept(headersBuilder, claimsBuilder); - headers = headersBuilder.build(); + JWK jwk = selectJwk(headersBuilder); + if (jwk == null) { + throw new JwtEncodingException( + String.format(ENCODING_ERROR_MESSAGE_TEMPLATE, "Failed to select a JWK signing key")); + } + else if (!StringUtils.hasText(jwk.getKeyID())) { + throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE, + "The \"kid\" (key ID) from the selected JWK cannot be empty")); + } + + headers = headersBuilder.keyId(jwk.getKeyID()).build(); claims = claimsBuilder.build(); - JWSHeader jwsHeader = jwsHeaderConverter.convert(headers); - JWTClaimsSet jwtClaimsSet = jwtClaimsSetConverter.convert(claims); + JWSHeader jwsHeader = JWS_HEADER_CONVERTER.convert(headers); + JWTClaimsSet jwtClaimsSet = JWT_CLAIMS_SET_CONVERTER.convert(claims); - SignedJWT signedJWT = new SignedJWT(jwsHeader, jwtClaimsSet); + JWSSigner jwsSigner = this.jwsSigners.computeIfAbsent(jwk, (key) -> { + try { + return JWS_SIGNER_FACTORY.createJWSSigner(key); + } + catch (JOSEException ex) { + throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE, + "Failed to create a JWS Signer -> " + ex.getMessage()), ex); + } + }); + + SignedJWT signedJwt = new SignedJWT(jwsHeader, jwtClaimsSet); try { - signedJWT.sign(jwsSigner); - } catch (JOSEException ex) { - throw new JwtEncodingException(String.format( - ENCODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex); + signedJwt.sign(jwsSigner); } - String jws = signedJWT.serialize(); + catch (JOSEException ex) { + throw new JwtEncodingException( + String.format(ENCODING_ERROR_MESSAGE_TEMPLATE, "Failed to sign the JWT -> " + ex.getMessage()), ex); + } + String jws = signedJwt.serialize(); - return new Jwt(jws, claims.getIssuedAt(), claims.getExpiresAt(), - headers.getHeaders(), claims.getClaims()); + return new Jwt(jws, claims.getIssuedAt(), claims.getExpiresAt(), headers.getHeaders(), claims.getClaims()); } - private CryptoKey selectKey(JoseHeader headers) { - JwsAlgorithm jwsAlgorithm = headers.getJwsAlgorithm(); - String keyAlgorithm = jcaKeyAlgorithmMappings.get(jwsAlgorithm); - if (!StringUtils.hasText(keyAlgorithm)) { - return null; + private JWK selectJwk(JoseHeader.Builder headersBuilder) { + final AtomicReference jwsAlgorithm = new AtomicReference<>(); + headersBuilder.headers((h) -> { + JwsAlgorithm jwsAlg = (JwsAlgorithm) h.get(JoseHeaderNames.ALG); + jwsAlgorithm.set(JWSAlgorithm.parse(jwsAlg.getName())); + }); + JWSHeader jwsHeader = new JWSHeader(jwsAlgorithm.get()); + JWKSelector jwkSelector = new JWKSelector(JWKMatcher.forJWSHeader(jwsHeader)); + + List jwks; + try { + jwks = this.jwkSource.get(jwkSelector, null); + } + catch (KeySourceException ex) { + throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE, + "Failed to select a JWK signing key -> " + ex.getMessage()), ex); } - return this.keySource.getKeys().stream() - .filter(key -> key.getAlgorithm().equals(keyAlgorithm)) - .findFirst() - .orElse(null); + if (jwks.size() > 1) { + throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE, + "Found multiple JWK signing keys for algorithm '" + jwsAlgorithm.get().getName() + "'")); + } + + return !jwks.isEmpty() ? jwks.get(0) : null; } private static class JwsHeaderConverter implements Converter { @Override public JWSHeader convert(JoseHeader headers) { - JWSHeader.Builder builder = new JWSHeader.Builder( - JWSAlgorithm.parse(headers.getJwsAlgorithm().getName())); + JWSHeader.Builder builder = new JWSHeader.Builder(JWSAlgorithm.parse(headers.getJwsAlgorithm().getName())); Set critical = headers.getCritical(); if (!CollectionUtils.isEmpty(critical)) { @@ -208,14 +206,14 @@ public final class NimbusJwsEncoder implements JwtEncoder { builder.contentType(contentType); } - String jwkSetUri = headers.getJwkSetUri(); - if (StringUtils.hasText(jwkSetUri)) { + URL jwkSetUri = headers.getJwkSetUri(); + if (jwkSetUri != null) { try { - builder.jwkURL(new URI(jwkSetUri)); - } catch (Exception ex) { - throw new JwtEncodingException(String.format( - ENCODING_ERROR_MESSAGE_TEMPLATE, - "Failed to convert '" + JoseHeaderNames.JKU + "' JOSE header"), ex); + builder.jwkURL(jwkSetUri.toURI()); + } + catch (Exception ex) { + throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE, + "Failed to convert '" + JoseHeaderNames.JKU + "' JOSE header to a URI"), ex); } } @@ -223,9 +221,9 @@ public final class NimbusJwsEncoder implements JwtEncoder { if (!CollectionUtils.isEmpty(jwk)) { try { builder.jwk(JWK.parse(jwk)); - } catch (Exception ex) { - throw new JwtEncodingException(String.format( - ENCODING_ERROR_MESSAGE_TEMPLATE, + } + catch (Exception ex) { + throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE, "Failed to convert '" + JoseHeaderNames.JWK + "' JOSE header"), ex); } } @@ -242,10 +240,7 @@ public final class NimbusJwsEncoder implements JwtEncoder { List x509CertificateChain = headers.getX509CertificateChain(); if (!CollectionUtils.isEmpty(x509CertificateChain)) { - builder.x509CertChain( - x509CertificateChain.stream() - .map(Base64::new) - .collect(Collectors.toList())); + builder.x509CertChain(x509CertificateChain.stream().map(Base64::new).collect(Collectors.toList())); } String x509SHA1Thumbprint = headers.getX509SHA1Thumbprint(); @@ -258,19 +253,19 @@ public final class NimbusJwsEncoder implements JwtEncoder { builder.x509CertSHA256Thumbprint(new Base64URL(x509SHA256Thumbprint)); } - String x509Uri = headers.getX509Uri(); - if (StringUtils.hasText(x509Uri)) { + URL x509Uri = headers.getX509Uri(); + if (x509Uri != null) { try { - builder.x509CertURL(new URI(x509Uri)); - } catch (Exception ex) { - throw new JwtEncodingException(String.format( - ENCODING_ERROR_MESSAGE_TEMPLATE, - "Failed to convert '" + JoseHeaderNames.X5U + "' JOSE header"), ex); + builder.x509CertURL(x509Uri.toURI()); + } + catch (Exception ex) { + throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE, + "Failed to convert '" + JoseHeaderNames.X5U + "' JOSE header to a URI"), ex); } } Map customHeaders = headers.getHeaders().entrySet().stream() - .filter(header -> !JWSHeader.getRegisteredParameterNames().contains(header.getKey())) + .filter((header) -> !JWSHeader.getRegisteredParameterNames().contains(header.getKey())) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); if (!CollectionUtils.isEmpty(customHeaders)) { builder.customParams(customHeaders); @@ -278,6 +273,7 @@ public final class NimbusJwsEncoder implements JwtEncoder { return builder.build(); } + } private static class JwtClaimsSetConverter implements Converter { @@ -322,7 +318,7 @@ public final class NimbusJwsEncoder implements JwtEncoder { } Map customClaims = claims.getClaims().entrySet().stream() - .filter(claim -> !JWTClaimsSet.getRegisteredNames().contains(claim.getKey())) + .filter((claim) -> !JWTClaimsSet.getRegisteredNames().contains(claim.getKey())) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); if (!CollectionUtils.isEmpty(customClaims)) { customClaims.forEach(builder::claim); @@ -330,5 +326,7 @@ public final class NimbusJwsEncoder implements JwtEncoder { return builder.build(); } + } + } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIssuerUtil.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIssuerUtil.java index 9ea4e00..a5a12ce 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIssuerUtil.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIssuerUtil.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.OAuth2RefreshToken2; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; -import org.springframework.security.oauth2.jose.JoseHeader; +import org.springframework.security.oauth2.jwt.JoseHeader; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtClaimsSet; diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/JwkSetEndpointFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/NimbusJwkSetEndpointFilter.java similarity index 51% rename from oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/JwkSetEndpointFilter.java rename to oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/NimbusJwkSetEndpointFilter.java index 5e1c918..7f33597 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/JwkSetEndpointFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/NimbusJwkSetEndpointFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,70 +15,65 @@ */ package org.springframework.security.oauth2.server.authorization.web; -import com.nimbusds.jose.JWSAlgorithm; -import com.nimbusds.jose.jwk.Curve; -import com.nimbusds.jose.jwk.ECKey; -import com.nimbusds.jose.jwk.JWK; -import com.nimbusds.jose.jwk.JWKSet; -import com.nimbusds.jose.jwk.KeyUse; -import com.nimbusds.jose.jwk.RSAKey; -import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; -import org.springframework.security.crypto.key.AsymmetricKey; -import org.springframework.security.crypto.key.CryptoKeySource; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import org.springframework.security.web.util.matcher.RequestMatcher; -import org.springframework.util.Assert; -import org.springframework.web.filter.OncePerRequestFilter; +import java.io.IOException; +import java.io.Writer; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.io.Writer; -import java.security.interfaces.ECPublicKey; -import java.security.interfaces.RSAPublicKey; -import java.util.Objects; -import java.util.stream.Collectors; + +import com.nimbusds.jose.jwk.JWKMatcher; +import com.nimbusds.jose.jwk.JWKSelector; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; + +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.Assert; +import org.springframework.web.filter.OncePerRequestFilter; /** * A {@code Filter} that processes JWK Set requests. * * @author Joe Grandja * @since 0.0.1 - * @see CryptoKeySource + * @see com.nimbusds.jose.jwk.source.JWKSource * @see JSON Web Key (JWK) * @see Section 5 JWK Set Format */ -public class JwkSetEndpointFilter extends OncePerRequestFilter { +public class NimbusJwkSetEndpointFilter extends OncePerRequestFilter { /** * The default endpoint {@code URI} for JWK Set requests. */ public static final String DEFAULT_JWK_SET_ENDPOINT_URI = "/oauth2/jwks"; - private final CryptoKeySource keySource; + private final JWKSource jwkSource; + private final JWKSelector jwkSelector; private final RequestMatcher requestMatcher; /** - * Constructs a {@code JwkSetEndpointFilter} using the provided parameters. - * - * @param keySource the source for cryptographic keys + * Constructs a {@code NimbusJwkSetEndpointFilter} using the provided parameters. + * @param jwkSource the {@code com.nimbusds.jose.jwk.source.JWKSource} */ - public JwkSetEndpointFilter(CryptoKeySource keySource) { - this(keySource, DEFAULT_JWK_SET_ENDPOINT_URI); + public NimbusJwkSetEndpointFilter(JWKSource jwkSource) { + this(jwkSource, DEFAULT_JWK_SET_ENDPOINT_URI); } /** - * Constructs a {@code JwkSetEndpointFilter} using the provided parameters. + * Constructs a {@code NimbusJwkSetEndpointFilter} using the provided parameters. * - * @param keySource the source for cryptographic keys + * @param jwkSource the {@code com.nimbusds.jose.jwk.source.JWKSource} * @param jwkSetEndpointUri the endpoint {@code URI} for JWK Set requests */ - public JwkSetEndpointFilter(CryptoKeySource keySource, String jwkSetEndpointUri) { - Assert.notNull(keySource, "keySource cannot be null"); + public NimbusJwkSetEndpointFilter(JWKSource jwkSource, String jwkSetEndpointUri) { + Assert.notNull(jwkSource, "jwkSource cannot be null"); Assert.hasText(jwkSetEndpointUri, "jwkSetEndpointUri cannot be empty"); - this.keySource = keySource; + this.jwkSource = jwkSource; + this.jwkSelector = new JWKSelector(new JWKMatcher.Builder().publicOnly(true).build()); this.requestMatcher = new AntPathRequestMatcher(jwkSetEndpointUri, HttpMethod.GET.name()); } @@ -91,43 +86,17 @@ public class JwkSetEndpointFilter extends OncePerRequestFilter { return; } - JWKSet jwkSet = buildJwkSet(); + JWKSet jwkSet; + try { + jwkSet = new JWKSet(this.jwkSource.get(this.jwkSelector, null)); + } + catch (Exception ex) { + throw new IllegalStateException("Failed to select the JWK public key(s) -> " + ex.getMessage(), ex); + } response.setContentType(MediaType.APPLICATION_JSON_VALUE); try (Writer writer = response.getWriter()) { writer.write(jwkSet.toString()); } } - - private JWKSet buildJwkSet() { - return new JWKSet( - this.keySource.getKeys().stream() - .filter(key -> AsymmetricKey.class.isAssignableFrom(key.getClass())) - .map(AsymmetricKey.class::cast) - .map(this::convert) - .filter(Objects::nonNull) - .collect(Collectors.toList()) - ); - } - - private JWK convert(AsymmetricKey asymmetricKey) { - JWK jwk = null; - if (asymmetricKey.getPublicKey() instanceof RSAPublicKey) { - RSAPublicKey publicKey = (RSAPublicKey) asymmetricKey.getPublicKey(); - jwk = new RSAKey.Builder(publicKey) - .keyUse(KeyUse.SIGNATURE) - .algorithm(JWSAlgorithm.RS256) - .keyID(asymmetricKey.getId()) - .build(); - } else if (asymmetricKey.getPublicKey() instanceof ECPublicKey) { - ECPublicKey publicKey = (ECPublicKey) asymmetricKey.getPublicKey(); - Curve curve = Curve.forECParameterSpec(publicKey.getParams()); - jwk = new ECKey.Builder(curve, publicKey) - .keyUse(KeyUse.SIGNATURE) - .algorithm(JWSAlgorithm.ES256) - .keyID(asymmetricKey.getId()) - .build(); - } - return jwk; - } } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationCodeGrantTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationCodeGrantTests.java index 8d83a8c..b040da8 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationCodeGrantTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationCodeGrantTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,11 +15,20 @@ */ package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.function.BiConsumer; + +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; @@ -27,16 +36,15 @@ import org.springframework.http.HttpHeaders; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; import org.springframework.security.config.test.SpringTestRule; -import org.springframework.security.crypto.key.CryptoKeySource; -import org.springframework.security.crypto.key.StaticKeyGeneratingCryptoKeySource; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; -import org.springframework.security.oauth2.jose.JoseHeader; -import org.springframework.security.oauth2.jose.jws.NimbusJwsEncoder; +import org.springframework.security.oauth2.jose.TestJwks; +import org.springframework.security.oauth2.jwt.JoseHeader; import org.springframework.security.oauth2.jwt.JwtClaimsSet; import org.springframework.security.oauth2.jwt.JwtEncoder; +import org.springframework.security.oauth2.jwt.NimbusJwsEncoder; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations; @@ -54,11 +62,6 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.function.BiConsumer; - import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.ArgumentMatchers.any; @@ -90,7 +93,7 @@ public class OAuth2AuthorizationCodeGrantTests { private static RegisteredClientRepository registeredClientRepository; private static OAuth2AuthorizationService authorizationService; - private static CryptoKeySource keySource; + private static JWKSource jwkSource; private static NimbusJwsEncoder jwtEncoder; private static BiConsumer jwtCustomizer; @@ -104,8 +107,9 @@ public class OAuth2AuthorizationCodeGrantTests { public static void init() { registeredClientRepository = mock(RegisteredClientRepository.class); authorizationService = mock(OAuth2AuthorizationService.class); - keySource = new StaticKeyGeneratingCryptoKeySource(); - jwtEncoder = new NimbusJwsEncoder(keySource); + JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK); + jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet); + jwtEncoder = new NimbusJwsEncoder(jwkSource); jwtCustomizer = mock(BiConsumer.class); jwtEncoder.setJwtCustomizer(jwtCustomizer); } @@ -298,8 +302,8 @@ public class OAuth2AuthorizationCodeGrantTests { } @Bean - CryptoKeySource keySource() { - return keySource; + JWKSource jwkSource() { + return jwkSource; } } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2ClientCredentialsGrantTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2ClientCredentialsGrantTests.java index ed1a4cc..cdea86d 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2ClientCredentialsGrantTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2ClientCredentialsGrantTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,10 +15,18 @@ */ package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; @@ -26,10 +34,9 @@ import org.springframework.http.HttpHeaders; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; import org.springframework.security.config.test.SpringTestRule; -import org.springframework.security.crypto.key.CryptoKeySource; -import org.springframework.security.crypto.key.StaticKeyGeneratingCryptoKeySource; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.jose.TestJwks; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; @@ -38,10 +45,6 @@ import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenE import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.util.Base64; - import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -61,7 +64,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. public class OAuth2ClientCredentialsGrantTests { private static RegisteredClientRepository registeredClientRepository; private static OAuth2AuthorizationService authorizationService; - private static CryptoKeySource keySource; + private static JWKSource jwkSource; @Rule public final SpringTestRule spring = new SpringTestRule(); @@ -73,7 +76,8 @@ public class OAuth2ClientCredentialsGrantTests { public static void init() { registeredClientRepository = mock(RegisteredClientRepository.class); authorizationService = mock(OAuth2AuthorizationService.class); - keySource = new StaticKeyGeneratingCryptoKeySource(); + JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK); + jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet); } @Before @@ -159,8 +163,8 @@ public class OAuth2ClientCredentialsGrantTests { } @Bean - CryptoKeySource keySource() { - return keySource; + JWKSource jwkSource() { + return jwkSource; } } } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2RefreshTokenGrantTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2RefreshTokenGrantTests.java index 6d48071..84994e1 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2RefreshTokenGrantTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2RefreshTokenGrantTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,10 +15,18 @@ */ package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; @@ -26,10 +34,9 @@ import org.springframework.http.HttpHeaders; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; import org.springframework.security.config.test.SpringTestRule; -import org.springframework.security.crypto.key.CryptoKeySource; -import org.springframework.security.crypto.key.StaticKeyGeneratingCryptoKeySource; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.jose.TestJwks; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations; @@ -42,10 +49,6 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.util.Base64; - import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -67,7 +70,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. public class OAuth2RefreshTokenGrantTests { private static RegisteredClientRepository registeredClientRepository; private static OAuth2AuthorizationService authorizationService; - private static CryptoKeySource keySource; + private static JWKSource jwkSource; @Rule public final SpringTestRule spring = new SpringTestRule(); @@ -79,7 +82,8 @@ public class OAuth2RefreshTokenGrantTests { public static void init() { registeredClientRepository = mock(RegisteredClientRepository.class); authorizationService = mock(OAuth2AuthorizationService.class); - keySource = new StaticKeyGeneratingCryptoKeySource(); + JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK); + jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet); } @Before @@ -153,8 +157,8 @@ public class OAuth2RefreshTokenGrantTests { } @Bean - CryptoKeySource keySource() { - return keySource; + JWKSource jwkSource() { + return jwkSource; } } } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2TokenRevocationTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2TokenRevocationTests.java index 51186ac..bf9967b 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2TokenRevocationTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2TokenRevocationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,11 +15,19 @@ */ package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; @@ -27,12 +35,11 @@ import org.springframework.http.HttpHeaders; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; import org.springframework.security.config.test.SpringTestRule; -import org.springframework.security.crypto.key.CryptoKeySource; -import org.springframework.security.crypto.key.StaticKeyGeneratingCryptoKeySource; import org.springframework.security.oauth2.core.AbstractOAuth2Token; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames2; +import org.springframework.security.oauth2.jose.TestJwks; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations; @@ -46,10 +53,6 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.util.Base64; - import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -66,7 +69,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. public class OAuth2TokenRevocationTests { private static RegisteredClientRepository registeredClientRepository; private static OAuth2AuthorizationService authorizationService; - private static CryptoKeySource keySource; + private static JWKSource jwkSource; @Rule public final SpringTestRule spring = new SpringTestRule(); @@ -78,7 +81,8 @@ public class OAuth2TokenRevocationTests { public static void init() { registeredClientRepository = mock(RegisteredClientRepository.class); authorizationService = mock(OAuth2AuthorizationService.class); - keySource = new StaticKeyGeneratingCryptoKeySource(); + JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK); + jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet); } @Before @@ -181,8 +185,8 @@ public class OAuth2TokenRevocationTests { } @Bean - CryptoKeySource keySource() { - return keySource; + JWKSource jwkSource() { + return jwkSource; } } } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcTests.java index f036324..d081b03 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,11 +15,19 @@ */ package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; @@ -27,12 +35,11 @@ import org.springframework.http.HttpHeaders; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; import org.springframework.security.config.test.SpringTestRule; -import org.springframework.security.crypto.key.CryptoKeySource; -import org.springframework.security.crypto.key.StaticKeyGeneratingCryptoKeySource; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.oidc.OidcScopes; +import org.springframework.security.oauth2.jose.TestJwks; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.TokenType; @@ -40,20 +47,16 @@ import org.springframework.security.oauth2.server.authorization.client.Registere import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; import org.springframework.security.oauth2.server.authorization.config.ProviderSettings; +import org.springframework.security.oauth2.server.authorization.oidc.web.OidcProviderConfigurationEndpointFilter; import org.springframework.security.oauth2.server.authorization.token.OAuth2AuthorizationCode; import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter; import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter; -import org.springframework.security.oauth2.server.authorization.oidc.web.OidcProviderConfigurationEndpointFilter; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.util.Base64; - import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.hamcrest.CoreMatchers.containsString; @@ -80,7 +83,7 @@ public class OidcTests { private static final String issuerUrl = "https://example.com/issuer1"; private static RegisteredClientRepository registeredClientRepository; private static OAuth2AuthorizationService authorizationService; - private static CryptoKeySource keySource; + private static JWKSource jwkSource; @Rule public final SpringTestRule spring = new SpringTestRule(); @@ -92,7 +95,8 @@ public class OidcTests { public static void init() { registeredClientRepository = mock(RegisteredClientRepository.class); authorizationService = mock(OAuth2AuthorizationService.class); - keySource = new StaticKeyGeneratingCryptoKeySource(); + JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK); + jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet); } @Before @@ -224,8 +228,8 @@ public class OidcTests { } @Bean - CryptoKeySource keySource() { - return keySource; + JWKSource jwkSource() { + return jwkSource; } } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/crypto/key/CryptoKeyTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/crypto/key/CryptoKeyTests.java deleted file mode 100644 index 9e2ed38..0000000 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/crypto/key/CryptoKeyTests.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.security.crypto.key; - -import org.junit.BeforeClass; -import org.junit.Test; - -import javax.crypto.SecretKey; -import java.security.KeyPair; -import java.util.HashMap; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.springframework.security.crypto.key.KeyGeneratorUtils.generateRsaKey; -import static org.springframework.security.crypto.key.KeyGeneratorUtils.generateSecretKey; - -/** - * Tests for {@link CryptoKey}. - * - * @author Joe Grandja - */ -public class CryptoKeyTests { - private static SecretKey secretKey; - private static KeyPair rsaKeyPair; - - @BeforeClass - public static void init() { - secretKey = generateSecretKey(); - rsaKeyPair = generateRsaKey(); - } - - @Test - public void symmetricWhenSecretKeyNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> CryptoKey.symmetric(null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("key cannot be null"); - } - - @Test - public void metadataWhenNameNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> CryptoKey.symmetric(secretKey).metadata(null, "value")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("name cannot be empty"); - } - - @Test - public void metadataWhenValueNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> CryptoKey.symmetric(secretKey).metadata("name", null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("value cannot be null"); - } - - @Test - public void symmetricWhenAllAttributesProvidedThenAllAttributesAreSet() { - Map 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 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); - } -} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/crypto/key/TestCryptoKeys.java b/oauth2-authorization-server/src/test/java/org/springframework/security/crypto/key/TestCryptoKeys.java deleted file mode 100644 index f55cd59..0000000 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/crypto/key/TestCryptoKeys.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.security.crypto.key; - -import java.security.KeyPair; - -import static org.springframework.security.crypto.key.KeyGeneratorUtils.generateEcKey; -import static org.springframework.security.crypto.key.KeyGeneratorUtils.generateRsaKey; -import static org.springframework.security.crypto.key.KeyGeneratorUtils.generateSecretKey; - -/** - * @author Joe Grandja - */ -public class TestCryptoKeys { - - public static SymmetricKey.Builder secretKey() { - return CryptoKey.symmetric(generateSecretKey()); - } - - public static AsymmetricKey.Builder rsaKey() { - KeyPair rsaKeyPair = generateRsaKey(); - return CryptoKey.asymmetric(rsaKeyPair.getPrivate(), rsaKeyPair.getPublic()); - } - - public static AsymmetricKey.Builder ecKey() { - KeyPair ecKeyPair = generateEcKey(); - return CryptoKey.asymmetric(ecKeyPair.getPrivate(), ecKeyPair.getPublic()); - } -} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jose/TestJwks.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jose/TestJwks.java new file mode 100644 index 0000000..78e449d --- /dev/null +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jose/TestJwks.java @@ -0,0 +1,89 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.jose; + +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; + +import javax.crypto.SecretKey; + +import com.nimbusds.jose.jwk.Curve; +import com.nimbusds.jose.jwk.ECKey; +import com.nimbusds.jose.jwk.KeyUse; +import com.nimbusds.jose.jwk.OctetSequenceKey; +import com.nimbusds.jose.jwk.RSAKey; + +/** + * @author Joe Grandja + */ +public final class TestJwks { + + // @formatter:off + public static final RSAKey DEFAULT_RSA_JWK = + jwk( + TestKeys.DEFAULT_PUBLIC_KEY, + TestKeys.DEFAULT_PRIVATE_KEY + ).build(); + // @formatter:on + + // @formatter:off + public static final ECKey DEFAULT_EC_JWK = + jwk( + (ECPublicKey) TestKeys.DEFAULT_EC_KEY_PAIR.getPublic(), + (ECPrivateKey) TestKeys.DEFAULT_EC_KEY_PAIR.getPrivate() + ).build(); + // @formatter:on + + // @formatter:off + public static final OctetSequenceKey DEFAULT_SECRET_JWK = + jwk( + TestKeys.DEFAULT_SECRET_KEY + ).build(); + // @formatter:on + + private TestJwks() { + } + + public static RSAKey.Builder jwk(RSAPublicKey publicKey, RSAPrivateKey privateKey) { + // @formatter:off + return new RSAKey.Builder(publicKey) + .privateKey(privateKey) + .keyUse(KeyUse.SIGNATURE) + .keyID("rsa-jwk-kid"); + // @formatter:on + } + + public static ECKey.Builder jwk(ECPublicKey publicKey, ECPrivateKey privateKey) { + // @formatter:off + Curve curve = Curve.forECParameterSpec(publicKey.getParams()); + return new ECKey.Builder(curve, publicKey) + .privateKey(privateKey) + .keyUse(KeyUse.SIGNATURE) + .keyID("ec-jwk-kid"); + // @formatter:on + } + + public static OctetSequenceKey.Builder jwk(SecretKey secretKey) { + // @formatter:off + return new OctetSequenceKey.Builder(secretKey) + .keyUse(KeyUse.SIGNATURE) + .keyID("secret-jwk-kid"); + // @formatter:on + } + +} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jose/TestKeys.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jose/TestKeys.java new file mode 100644 index 0000000..0baa8cb --- /dev/null +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jose/TestKeys.java @@ -0,0 +1,154 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.jose; + +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.ECFieldFp; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.EllipticCurve; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +/** + * TODO + * This class is a straight copy from Spring Security. + * It should be removed when merging this codebase into Spring Security. + * + * @author Joe Grandja + * @since 5.2 + */ +public final class TestKeys { + + public static final KeyFactory kf; + static { + try { + kf = KeyFactory.getInstance("RSA"); + } + catch (NoSuchAlgorithmException ex) { + throw new IllegalStateException(ex); + } + } + public static final String DEFAULT_ENCODED_SECRET_KEY = "bCzY/M48bbkwBEWjmNSIEPfwApcvXOnkCxORBEbPr+4="; + + public static final SecretKey DEFAULT_SECRET_KEY = new SecretKeySpec( + Base64.getDecoder().decode(DEFAULT_ENCODED_SECRET_KEY), "AES"); + + // @formatter:off + public static final String DEFAULT_RSA_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3FlqJr5TRskIQIgdE3Dd" + + "7D9lboWdcTUT8a+fJR7MAvQm7XXNoYkm3v7MQL1NYtDvL2l8CAnc0WdSTINU6IRv" + + "c5Kqo2Q4csNX9SHOmEfzoROjQqahEcve1jBXluoCXdYuYpx4/1tfRgG6ii4Uhxh6" + + "iI8qNMJQX+fLfqhbfYfxBQVRPywBkAbIP4x1EAsbC6FSNmkhCxiMNqEgxaIpY8C2" + + "kJdJ/ZIV+WW4noDdzpKqHcwmB8FsrumlVY/DNVvUSDIipiq9PbP4H99TXN1o746o" + + "RaNa07rq1hoCgMSSy+85SagCoxlmyE+D+of9SsMY8Ol9t0rdzpobBuhyJ/o5dfvj" + + "KwIDAQAB"; + // @formatter:on + + public static final RSAPublicKey DEFAULT_PUBLIC_KEY; + static { + X509EncodedKeySpec spec = new X509EncodedKeySpec(Base64.getDecoder().decode(DEFAULT_RSA_PUBLIC_KEY)); + try { + DEFAULT_PUBLIC_KEY = (RSAPublicKey) kf.generatePublic(spec); + } + catch (InvalidKeySpecException ex) { + throw new IllegalArgumentException(ex); + } + } + + // @formatter:off + public static final String DEFAULT_RSA_PRIVATE_KEY = "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDcWWomvlNGyQhA" + + "iB0TcN3sP2VuhZ1xNRPxr58lHswC9Cbtdc2hiSbe/sxAvU1i0O8vaXwICdzRZ1JM" + + "g1TohG9zkqqjZDhyw1f1Ic6YR/OhE6NCpqERy97WMFeW6gJd1i5inHj/W19GAbqK" + + "LhSHGHqIjyo0wlBf58t+qFt9h/EFBVE/LAGQBsg/jHUQCxsLoVI2aSELGIw2oSDF" + + "oiljwLaQl0n9khX5ZbiegN3OkqodzCYHwWyu6aVVj8M1W9RIMiKmKr09s/gf31Nc" + + "3WjvjqhFo1rTuurWGgKAxJLL7zlJqAKjGWbIT4P6h/1Kwxjw6X23St3OmhsG6HIn" + + "+jl1++MrAgMBAAECggEBAMf820wop3pyUOwI3aLcaH7YFx5VZMzvqJdNlvpg1jbE" + + "E2Sn66b1zPLNfOIxLcBG8x8r9Ody1Bi2Vsqc0/5o3KKfdgHvnxAB3Z3dPh2WCDek" + + "lCOVClEVoLzziTuuTdGO5/CWJXdWHcVzIjPxmK34eJXioiLaTYqN3XKqKMdpD0ZG" + + "mtNTGvGf+9fQ4i94t0WqIxpMpGt7NM4RHy3+Onggev0zLiDANC23mWrTsUgect/7" + + "62TYg8g1bKwLAb9wCBT+BiOuCc2wrArRLOJgUkj/F4/gtrR9ima34SvWUyoUaKA0" + + "bi4YBX9l8oJwFGHbU9uFGEMnH0T/V0KtIB7qetReywkCgYEA9cFyfBIQrYISV/OA" + + "+Z0bo3vh2aL0QgKrSXZ924cLt7itQAHNZ2ya+e3JRlTczi5mnWfjPWZ6eJB/8MlH" + + "Gpn12o/POEkU+XjZZSPe1RWGt5g0S3lWqyx9toCS9ACXcN9tGbaqcFSVI73zVTRA" + + "8J9grR0fbGn7jaTlTX2tnlOTQ60CgYEA5YjYpEq4L8UUMFkuj+BsS3u0oEBnzuHd" + + "I9LEHmN+CMPosvabQu5wkJXLuqo2TxRnAznsA8R3pCLkdPGoWMCiWRAsCn979TdY" + + "QbqO2qvBAD2Q19GtY7lIu6C35/enQWzJUMQE3WW0OvjLzZ0l/9mA2FBRR+3F9A1d" + + "rBdnmv0c3TcCgYEAi2i+ggVZcqPbtgrLOk5WVGo9F1GqUBvlgNn30WWNTx4zIaEk" + + "HSxtyaOLTxtq2odV7Kr3LGiKxwPpn/T+Ief+oIp92YcTn+VfJVGw4Z3BezqbR8lA" + + "Uf/+HF5ZfpMrVXtZD4Igs3I33Duv4sCuqhEvLWTc44pHifVloozNxYfRfU0CgYBN" + + "HXa7a6cJ1Yp829l62QlJKtx6Ymj95oAnQu5Ez2ROiZMqXRO4nucOjGUP55Orac1a" + + "FiGm+mC/skFS0MWgW8evaHGDbWU180wheQ35hW6oKAb7myRHtr4q20ouEtQMdQIF" + + "snV39G1iyqeeAsf7dxWElydXpRi2b68i3BIgzhzebQKBgQCdUQuTsqV9y/JFpu6H" + + "c5TVvhG/ubfBspI5DhQqIGijnVBzFT//UfIYMSKJo75qqBEyP2EJSmCsunWsAFsM" + + "TszuiGTkrKcZy9G0wJqPztZZl2F2+bJgnA6nBEV7g5PA4Af+QSmaIhRwqGDAuROR" + + "47jndeyIaMTNETEmOnms+as17g=="; + // @formatter:on + + public static final RSAPrivateKey DEFAULT_PRIVATE_KEY; + static { + PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(DEFAULT_RSA_PRIVATE_KEY)); + try { + DEFAULT_PRIVATE_KEY = (RSAPrivateKey) kf.generatePrivate(spec); + } + catch (InvalidKeySpecException ex) { + throw new IllegalArgumentException(ex); + } + } + + public static final KeyPair DEFAULT_RSA_KEY_PAIR = new KeyPair(DEFAULT_PUBLIC_KEY, DEFAULT_PRIVATE_KEY); + + public static final KeyPair DEFAULT_EC_KEY_PAIR = generateEcKeyPair(); + + static KeyPair generateEcKeyPair() { + EllipticCurve ellipticCurve = new EllipticCurve( + new ECFieldFp(new BigInteger( + "115792089210356248762697446949407573530086143415290314195533631308867097853951")), + new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853948"), + new BigInteger("41058363725152142129326129780047268409114441015993725554835256314039467401291")); + ECPoint ecPoint = new ECPoint( + new BigInteger("48439561293906451759052585252797914202762949526041747995844080717082404635286"), + new BigInteger("36134250956749795798585127919587881956611106672985015071877198253568414405109")); + ECParameterSpec ecParameterSpec = new ECParameterSpec(ellipticCurve, ecPoint, + new BigInteger("115792089210356248762697446949407573529996955224135760342422259061068512044369"), 1); + + KeyPair keyPair; + try { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC"); + keyPairGenerator.initialize(ecParameterSpec); + keyPair = keyPairGenerator.generateKeyPair(); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + return keyPair; + } + + private TestKeys() { + } + +} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jose/jws/NimbusJwsEncoderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jose/jws/NimbusJwsEncoderTests.java deleted file mode 100644 index dcc9595..0000000 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jose/jws/NimbusJwsEncoderTests.java +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.security.oauth2.jose.jws; - -import org.junit.Before; -import org.junit.Test; -import org.springframework.security.crypto.key.AsymmetricKey; -import org.springframework.security.crypto.key.CryptoKeySource; -import org.springframework.security.crypto.key.TestCryptoKeys; -import org.springframework.security.oauth2.jose.JoseHeader; -import org.springframework.security.oauth2.jose.JoseHeaderNames; -import org.springframework.security.oauth2.jose.TestJoseHeaders; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.jwt.JwtClaimsSet; -import org.springframework.security.oauth2.jwt.JwtEncodingException; -import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; -import org.springframework.security.oauth2.jwt.TestJwtClaimsSets; - -import java.security.interfaces.RSAPublicKey; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.function.BiConsumer; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** - * Tests for {@link NimbusJwsEncoder}. - * - * @author Joe Grandja - */ -public class NimbusJwsEncoderTests { - private CryptoKeySource keySource; - private NimbusJwsEncoder jwtEncoder; - - @Before - public void setUp() { - this.keySource = mock(CryptoKeySource.class); - this.jwtEncoder = new NimbusJwsEncoder(this.keySource); - } - - @Test - public void constructorWhenKeySourceNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> new NimbusJwsEncoder(null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("keySource cannot be null"); - } - - @Test - public void setJwtCustomizerWhenNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> this.jwtEncoder.setJwtCustomizer(null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("jwtCustomizer cannot be null"); - } - - @Test - public void encodeWhenHeadersNullThenThrowIllegalArgumentException() { - JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build(); - - assertThatThrownBy(() -> this.jwtEncoder.encode(null, jwtClaimsSet)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("headers cannot be null"); - } - - @Test - public void encodeWhenClaimsNullThenThrowIllegalArgumentException() { - JoseHeader joseHeader = TestJoseHeaders.joseHeader().build(); - - assertThatThrownBy(() -> this.jwtEncoder.encode(joseHeader, null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("claims cannot be null"); - } - - @Test - public void encodeWhenUnsupportedKeyThenThrowJwtEncodingException() { - JoseHeader joseHeader = TestJoseHeaders.joseHeader().build(); - JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build(); - - assertThatThrownBy(() -> this.jwtEncoder.encode(joseHeader, jwtClaimsSet)) - .isInstanceOf(JwtEncodingException.class) - .hasMessageContaining("Unsupported key for algorithm 'RS256'"); - } - - @Test - public void encodeWhenUnsupportedKeyAlgorithmThenThrowJwtEncodingException() { - JoseHeader joseHeader = TestJoseHeaders.joseHeader(SignatureAlgorithm.ES256).build(); - JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build(); - - assertThatThrownBy(() -> this.jwtEncoder.encode(joseHeader, jwtClaimsSet)) - .isInstanceOf(JwtEncodingException.class) - .hasMessageContaining("Unsupported key for algorithm 'ES256'"); - } - - @Test - public void encodeWhenUnsupportedKeyTypeThenThrowJwtEncodingException() { - AsymmetricKey ecKey = TestCryptoKeys.ecKey().build(); - when(this.keySource.getKeys()).thenReturn(Collections.singleton(ecKey)); - - JoseHeader joseHeader = TestJoseHeaders.joseHeader(SignatureAlgorithm.ES256).build(); - JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build(); - - assertThatThrownBy(() -> this.jwtEncoder.encode(joseHeader, jwtClaimsSet)) - .isInstanceOf(JwtEncodingException.class) - .hasMessageContaining("Unsupported key type 'EC'"); - } - - @Test - public void encodeWhenSuccessThenDecodes() { - AsymmetricKey rsaKey = TestCryptoKeys.rsaKey().build(); - when(this.keySource.getKeys()).thenReturn(Collections.singleton(rsaKey)); - - JoseHeader joseHeader = TestJoseHeaders.joseHeader() - .headers(headers -> headers.remove(JoseHeaderNames.CRIT)) - .build(); - JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build(); - - Jwt jws = this.jwtEncoder.encode(joseHeader, jwtClaimsSet); - - NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey((RSAPublicKey) rsaKey.getPublicKey()).build(); - jwtDecoder.decode(jws.getTokenValue()); - } - - @Test - public void encodeWhenCustomizerSetThenCalled() { - AsymmetricKey rsaKey = TestCryptoKeys.rsaKey().build(); - when(this.keySource.getKeys()).thenReturn(Collections.singleton(rsaKey)); - - JoseHeader joseHeader = TestJoseHeaders.joseHeader() - .headers(headers -> headers.remove(JoseHeaderNames.CRIT)) - .build(); - JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build(); - - BiConsumer jwtCustomizer = mock(BiConsumer.class); - this.jwtEncoder.setJwtCustomizer(jwtCustomizer); - - this.jwtEncoder.encode(joseHeader, jwtClaimsSet); - - verify(jwtCustomizer).accept(any(JoseHeader.Builder.class), any(JwtClaimsSet.Builder.class)); - } - - @Test - public void encodeWhenMultipleActiveKeysThenUseFirst() { - AsymmetricKey rsaKey1 = TestCryptoKeys.rsaKey().build(); - AsymmetricKey rsaKey2 = TestCryptoKeys.rsaKey().build(); - when(this.keySource.getKeys()).thenReturn( - Stream.of(rsaKey1, rsaKey2) - .collect(Collectors.toCollection(LinkedHashSet::new))); - - JoseHeader joseHeader = TestJoseHeaders.joseHeader() - .headers(headers -> headers.remove(JoseHeaderNames.CRIT)) - .build(); - JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build(); - - Jwt jws = this.jwtEncoder.encode(joseHeader, jwtClaimsSet); - - NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey((RSAPublicKey) rsaKey1.getPublicKey()).build(); - jwtDecoder.decode(jws.getTokenValue()); - } -} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jose/JoseHeaderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jwt/JoseHeaderTests.java similarity index 94% rename from oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jose/JoseHeaderTests.java rename to oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jwt/JoseHeaderTests.java index dbc16e8..be753bb 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jose/JoseHeaderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jwt/JoseHeaderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.security.oauth2.jose; +package org.springframework.security.oauth2.jwt; import org.junit.Test; + import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import static org.assertj.core.api.Assertions.assertThat; @@ -40,14 +41,13 @@ public class JoseHeaderTests { JoseHeader expectedJoseHeader = TestJoseHeaders.joseHeader().build(); JoseHeader joseHeader = JoseHeader.withAlgorithm(expectedJoseHeader.getJwsAlgorithm()) - .jwkSetUri(expectedJoseHeader.getJwkSetUri()) + .jwkSetUri(expectedJoseHeader.getJwkSetUri().toExternalForm()) .jwk(expectedJoseHeader.getJwk()) .keyId(expectedJoseHeader.getKeyId()) - .x509Uri(expectedJoseHeader.getX509Uri()) + .x509Uri(expectedJoseHeader.getX509Uri().toExternalForm()) .x509CertificateChain(expectedJoseHeader.getX509CertificateChain()) .x509SHA1Thumbprint(expectedJoseHeader.getX509SHA1Thumbprint()) .x509SHA256Thumbprint(expectedJoseHeader.getX509SHA256Thumbprint()) - .critical(expectedJoseHeader.getCritical()) .type(expectedJoseHeader.getType()) .contentType(expectedJoseHeader.getContentType()) .headers(headers -> headers.put("custom-header-name", "custom-header-value")) diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwsEncoderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwsEncoderTests.java new file mode 100644 index 0000000..74cb8a6 --- /dev/null +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwsEncoderTests.java @@ -0,0 +1,291 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.jwt; + +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.BiConsumer; + +import com.nimbusds.jose.KeySourceException; +import com.nimbusds.jose.jwk.ECKey; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSelector; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.KeyUse; +import com.nimbusds.jose.jwk.OctetSequenceKey; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; +import org.junit.Before; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import org.springframework.security.oauth2.jose.TestJwks; +import org.springframework.security.oauth2.jose.TestKeys; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link NimbusJwsEncoder}. + * + * @author Joe Grandja + */ +public class NimbusJwsEncoderTests { + + private JWKSource jwkSource; + + private NimbusJwsEncoder jwsEncoder; + + @Before + public void setUp() { + this.jwkSource = mock(JWKSource.class); + this.jwsEncoder = new NimbusJwsEncoder(this.jwkSource); + } + + @Test + public void constructorWhenJwkSourceNullThenThrowIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> new NimbusJwsEncoder(null)) + .withMessage("jwkSource cannot be null"); + } + + @Test + public void setJwtCustomizerWhenNullThenThrowIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.jwsEncoder.setJwtCustomizer(null)) + .withMessage("jwtCustomizer cannot be null"); + } + + @Test + public void encodeWhenHeadersNullThenThrowIllegalArgumentException() { + JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build(); + + assertThatIllegalArgumentException().isThrownBy(() -> this.jwsEncoder.encode(null, jwtClaimsSet)) + .withMessage("headers cannot be null"); + } + + @Test + public void encodeWhenClaimsNullThenThrowIllegalArgumentException() { + JoseHeader joseHeader = TestJoseHeaders.joseHeader().build(); + + assertThatIllegalArgumentException().isThrownBy(() -> this.jwsEncoder.encode(joseHeader, null)) + .withMessage("claims cannot be null"); + } + + @Test + public void encodeWhenCustomizerSetThenCalled() throws Exception { + RSAKey rsaJwk = TestJwks.DEFAULT_RSA_JWK; + given(this.jwkSource.get(any(), any())).willReturn(Collections.singletonList(rsaJwk)); + + BiConsumer jwtCustomizer = mock(BiConsumer.class); + this.jwsEncoder.setJwtCustomizer(jwtCustomizer); + + JoseHeader joseHeader = TestJoseHeaders.joseHeader().build(); + JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build(); + + this.jwsEncoder.encode(joseHeader, jwtClaimsSet); + + verify(jwtCustomizer).accept(any(JoseHeader.Builder.class), any(JwtClaimsSet.Builder.class)); + } + + @Test + public void encodeWhenJwkSelectFailedThenThrowJwtEncodingException() throws Exception { + given(this.jwkSource.get(any(), any())).willThrow(new KeySourceException("key source error")); + + JoseHeader joseHeader = TestJoseHeaders.joseHeader().build(); + JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build(); + + assertThatExceptionOfType(JwtEncodingException.class) + .isThrownBy(() -> this.jwsEncoder.encode(joseHeader, jwtClaimsSet)) + .withMessageContaining("Failed to select a JWK signing key -> key source error"); + } + + @Test + public void encodeWhenJwkMultipleSelectedThenThrowJwtEncodingException() throws Exception { + RSAKey rsaJwk = TestJwks.DEFAULT_RSA_JWK; + given(this.jwkSource.get(any(), any())).willReturn(Arrays.asList(rsaJwk, rsaJwk)); + + JoseHeader joseHeader = TestJoseHeaders.joseHeader().build(); + JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build(); + + assertThatExceptionOfType(JwtEncodingException.class) + .isThrownBy(() -> this.jwsEncoder.encode(joseHeader, jwtClaimsSet)) + .withMessageContaining("Found multiple JWK signing keys for algorithm 'RS256'"); + } + + @Test + public void encodeWhenJwkSelectEmptyThenThrowJwtEncodingException() { + JoseHeader joseHeader = TestJoseHeaders.joseHeader().build(); + JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build(); + + assertThatExceptionOfType(JwtEncodingException.class) + .isThrownBy(() -> this.jwsEncoder.encode(joseHeader, jwtClaimsSet)) + .withMessageContaining("Failed to select a JWK signing key"); + } + + @Test + public void encodeWhenJwkKidNullThenThrowJwtEncodingException() throws Exception { + // @formatter:off + RSAKey rsaJwk = TestJwks.jwk(TestKeys.DEFAULT_PUBLIC_KEY, TestKeys.DEFAULT_PRIVATE_KEY) + .keyID(null) + .build(); + // @formatter:on + + given(this.jwkSource.get(any(), any())).willReturn(Collections.singletonList(rsaJwk)); + + JoseHeader joseHeader = TestJoseHeaders.joseHeader().build(); + JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build(); + + assertThatExceptionOfType(JwtEncodingException.class) + .isThrownBy(() -> this.jwsEncoder.encode(joseHeader, jwtClaimsSet)) + .withMessageContaining("The \"kid\" (key ID) from the selected JWK cannot be empty"); + } + + @Test + public void encodeWhenJwkUseEncryptionThenThrowJwtEncodingException() throws Exception { + // @formatter:off + RSAKey rsaJwk = TestJwks.jwk(TestKeys.DEFAULT_PUBLIC_KEY, TestKeys.DEFAULT_PRIVATE_KEY) + .keyUse(KeyUse.ENCRYPTION) + .build(); + // @formatter:on + + given(this.jwkSource.get(any(), any())).willReturn(Collections.singletonList(rsaJwk)); + + JoseHeader joseHeader = TestJoseHeaders.joseHeader().build(); + JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build(); + + assertThatExceptionOfType(JwtEncodingException.class) + .isThrownBy(() -> this.jwsEncoder.encode(joseHeader, jwtClaimsSet)).withMessageContaining( + "Failed to create a JWS Signer -> The JWK use must be sig (signature) or unspecified"); + } + + @Test + public void encodeWhenSuccessThenDecodes() throws Exception { + RSAKey rsaJwk = TestJwks.DEFAULT_RSA_JWK; + given(this.jwkSource.get(any(), any())).willReturn(Collections.singletonList(rsaJwk)); + + JoseHeader joseHeader = TestJoseHeaders.joseHeader().build(); + JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build(); + + Jwt encodedJws = this.jwsEncoder.encode(joseHeader, jwtClaimsSet); + + // Assert headers/claims were added + assertThat(encodedJws.getHeaders().get(JoseHeaderNames.TYP)).isEqualTo("JWT"); + assertThat(encodedJws.getHeaders().get(JoseHeaderNames.KID)).isEqualTo(rsaJwk.getKeyID()); + assertThat(encodedJws.getId()).isNotNull(); + + NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(rsaJwk.toRSAPublicKey()).build(); + jwtDecoder.decode(encodedJws.getTokenValue()); + } + + @Test + public void encodeWhenKeysRotatedThenNewKeyUsed() throws Exception { + TestJWKSource jwkSource = new TestJWKSource(); + JWKSource jwkSourceDelegate = spy(new JWKSource() { + @Override + public List get(JWKSelector jwkSelector, SecurityContext context) { + return jwkSource.get(jwkSelector, context); + } + }); + NimbusJwsEncoder jwsEncoder = new NimbusJwsEncoder(jwkSourceDelegate); + + JwkListResultCaptor jwkListResultCaptor = new JwkListResultCaptor(); + willAnswer(jwkListResultCaptor).given(jwkSourceDelegate).get(any(), any()); + + JoseHeader joseHeader = TestJoseHeaders.joseHeader().build(); + JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build(); + + Jwt encodedJws = jwsEncoder.encode(joseHeader, jwtClaimsSet); + + JWK jwk1 = jwkListResultCaptor.getResult().get(0); + NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(((RSAKey) jwk1).toRSAPublicKey()).build(); + jwtDecoder.decode(encodedJws.getTokenValue()); + + jwkSource.rotate(); // Trigger key rotation + + encodedJws = jwsEncoder.encode(joseHeader, jwtClaimsSet); + + JWK jwk2 = jwkListResultCaptor.getResult().get(0); + jwtDecoder = NimbusJwtDecoder.withPublicKey(((RSAKey) jwk2).toRSAPublicKey()).build(); + jwtDecoder.decode(encodedJws.getTokenValue()); + + assertThat(jwk1.getKeyID()).isNotEqualTo(jwk2.getKeyID()); + } + + private static final class JwkListResultCaptor implements Answer> { + + private List result; + + private List getResult() { + return this.result; + } + + @SuppressWarnings("unchecked") + @Override + public List answer(InvocationOnMock invocationOnMock) throws Throwable { + this.result = (List) invocationOnMock.callRealMethod(); + return this.result; + } + + } + + private static final class TestJWKSource implements JWKSource { + + private int keyId = 1000; + + private JWKSet jwkSet; + + private TestJWKSource() { + init(); + } + + @Override + public List get(JWKSelector jwkSelector, SecurityContext context) { + return jwkSelector.select(this.jwkSet); + } + + private void init() { + // @formatter:off + RSAKey rsaJwk = TestJwks.jwk(TestKeys.DEFAULT_PUBLIC_KEY, TestKeys.DEFAULT_PRIVATE_KEY) + .keyID("rsa-jwk-" + this.keyId++) + .build(); + ECKey ecJwk = TestJwks.jwk((ECPublicKey) TestKeys.DEFAULT_EC_KEY_PAIR.getPublic(), (ECPrivateKey) TestKeys.DEFAULT_EC_KEY_PAIR.getPrivate()) + .keyID("ec-jwk-" + this.keyId++) + .build(); + OctetSequenceKey secretJwk = TestJwks.jwk(TestKeys.DEFAULT_SECRET_KEY) + .keyID("secret-jwk-" + this.keyId++) + .build(); + // @formatter:on + this.jwkSet = new JWKSet(Arrays.asList(rsaJwk, ecJwk, secretJwk)); + } + + private void rotate() { + init(); + } + + } + +} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jose/TestJoseHeaders.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jwt/TestJoseHeaders.java similarity index 84% rename from oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jose/TestJoseHeaders.java rename to oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jwt/TestJoseHeaders.java index 70f3c15..a85f837 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jose/TestJoseHeaders.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jwt/TestJoseHeaders.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,38 +13,40 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.security.oauth2.jose; - -import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +package org.springframework.security.oauth2.jwt; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.UUID; + +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; /** * @author Joe Grandja */ -public class TestJoseHeaders { +public final class TestJoseHeaders { + + private TestJoseHeaders() { + } public static JoseHeader.Builder joseHeader() { return joseHeader(SignatureAlgorithm.RS256); } public static JoseHeader.Builder joseHeader(SignatureAlgorithm signatureAlgorithm) { + // @formatter:off return JoseHeader.withAlgorithm(signatureAlgorithm) .jwkSetUri("https://provider.com/oauth2/jwks") .jwk(rsaJwk()) - .keyId(UUID.randomUUID().toString()) + .keyId("keyId") .x509Uri("https://provider.com/oauth2/x509") .x509CertificateChain(Arrays.asList("x509Cert1", "x509Cert2")) .x509SHA1Thumbprint("x509SHA1Thumbprint") .x509SHA256Thumbprint("x509SHA256Thumbprint") - .critical(Collections.singleton("custom-header-name")) .type("JWT") .contentType("jwt-content-type") .header("custom-header-name", "custom-header-value"); + // @formatter:on } private static Map rsaJwk() { diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jwt/TestJwtClaimsSets.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jwt/TestJwtClaimsSets.java index 51b994c..6e09c3f 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jwt/TestJwtClaimsSets.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jwt/TestJwtClaimsSets.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,18 +18,21 @@ package org.springframework.security.oauth2.jwt; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Collections; -import java.util.UUID; /** * @author Joe Grandja */ -public class TestJwtClaimsSets { +public final class TestJwtClaimsSets { + + private TestJwtClaimsSets() { + } public static JwtClaimsSet.Builder jwtClaimsSet() { String issuer = "https://provider.com"; Instant issuedAt = Instant.now(); Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS); + // @formatter:off return JwtClaimsSet.builder() .issuer(issuer) .subject("subject") @@ -37,7 +40,8 @@ public class TestJwtClaimsSets { .issuedAt(issuedAt) .notBefore(issuedAt) .expiresAt(expiresAt) - .id(UUID.randomUUID().toString()) + .id("jti") .claim("custom-claim-name", "custom-claim-value"); + // @formatter:on } } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java index eb06765..5c7d79f 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.oidc.OidcIdToken; import org.springframework.security.oauth2.core.oidc.OidcScopes; import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; -import org.springframework.security.oauth2.jose.JoseHeaderNames; +import org.springframework.security.oauth2.jwt.JoseHeaderNames; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtClaimsSet; diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProviderTests.java index 0afb357..4a686fb 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProviderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; -import org.springframework.security.oauth2.jose.JoseHeaderNames; +import org.springframework.security.oauth2.jwt.JoseHeaderNames; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtEncoder; diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProviderTests.java index f9cb980..8339b12 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProviderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.OAuth2RefreshToken2; -import org.springframework.security.oauth2.jose.JoseHeaderNames; +import org.springframework.security.oauth2.jwt.JoseHeaderNames; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtEncoder; diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/JwkSetEndpointFilterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/NimbusJwkSetEndpointFilterTests.java similarity index 63% rename from oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/JwkSetEndpointFilterTests.java rename to oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/NimbusJwkSetEndpointFilterTests.java index b4b3aeb..ff408d4 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/JwkSetEndpointFilterTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/NimbusJwkSetEndpointFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,62 +15,61 @@ */ package org.springframework.security.oauth2.server.authorization.web; -import com.nimbusds.jose.JWSAlgorithm; -import com.nimbusds.jose.jwk.ECKey; -import com.nimbusds.jose.jwk.JWKSet; -import com.nimbusds.jose.jwk.KeyUse; -import com.nimbusds.jose.jwk.RSAKey; -import org.junit.Before; -import org.junit.Test; -import org.springframework.http.MediaType; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.security.crypto.key.AsymmetricKey; -import org.springframework.security.crypto.key.CryptoKeySource; -import org.springframework.security.crypto.key.SymmetricKey; -import org.springframework.security.crypto.key.TestCryptoKeys; +import java.util.Arrays; +import java.util.Collections; import javax.servlet.FilterChain; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import java.util.Collections; -import java.util.HashSet; -import java.util.stream.Collectors; -import java.util.stream.Stream; + +import com.nimbusds.jose.jwk.ECKey; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.KeyUse; +import com.nimbusds.jose.jwk.OctetSequenceKey; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.oauth2.jose.TestJwks; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; /** - * Tests for {@link JwkSetEndpointFilter}. + * Tests for {@link NimbusJwkSetEndpointFilter}. * * @author Joe Grandja */ -public class JwkSetEndpointFilterTests { - private CryptoKeySource keySource; - private JwkSetEndpointFilter filter; +public class NimbusJwkSetEndpointFilterTests { + private JWKSource jwkSource; + private NimbusJwkSetEndpointFilter filter; @Before public void setUp() { - this.keySource = mock(CryptoKeySource.class); - this.filter = new JwkSetEndpointFilter(this.keySource); + this.jwkSource = mock(JWKSource.class); + this.filter = new NimbusJwkSetEndpointFilter(this.jwkSource); } @Test - public void constructorWhenKeySourceNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> new JwkSetEndpointFilter(null)) + public void constructorWhenJwkSourceNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new NimbusJwkSetEndpointFilter(null)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("keySource cannot be null"); + .hasMessage("jwkSource cannot be null"); } @Test public void constructorWhenJwkSetEndpointUriNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> new JwkSetEndpointFilter(this.keySource, null)) + assertThatThrownBy(() -> new NimbusJwkSetEndpointFilter(this.jwkSource, null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("jwkSetEndpointUri cannot be empty"); } @@ -90,7 +89,7 @@ public class JwkSetEndpointFilterTests { @Test public void doFilterWhenJwkSetRequestPostThenNotProcessed() throws Exception { - String requestUri = JwkSetEndpointFilter.DEFAULT_JWK_SET_ENDPOINT_URI; + String requestUri = NimbusJwkSetEndpointFilter.DEFAULT_JWK_SET_ENDPOINT_URI; MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri); request.setServletPath(requestUri); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -103,12 +102,11 @@ public class JwkSetEndpointFilterTests { @Test public void doFilterWhenAsymmetricKeysThenJwkSetResponse() throws Exception { - AsymmetricKey rsaKey = TestCryptoKeys.rsaKey().build(); - AsymmetricKey ecKey = TestCryptoKeys.ecKey().build(); - when(this.keySource.getKeys()).thenReturn( - Stream.of(rsaKey, ecKey).collect(Collectors.toSet())); + RSAKey rsaJwk = TestJwks.DEFAULT_RSA_JWK; + ECKey ecJwk = TestJwks.DEFAULT_EC_JWK; + given(this.jwkSource.get(any(), any())).willReturn(Arrays.asList(rsaJwk, ecJwk)); - String requestUri = JwkSetEndpointFilter.DEFAULT_JWK_SET_ENDPOINT_URI; + String requestUri = NimbusJwkSetEndpointFilter.DEFAULT_JWK_SET_ENDPOINT_URI; MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); request.setServletPath(requestUri); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -123,29 +121,25 @@ public class JwkSetEndpointFilterTests { JWKSet jwkSet = JWKSet.parse(response.getContentAsString()); assertThat(jwkSet.getKeys()).hasSize(2); - RSAKey rsaJwk = (RSAKey) jwkSet.getKeyByKeyId(rsaKey.getId()); - assertThat(rsaJwk).isNotNull(); - assertThat(rsaJwk.toRSAPublicKey()).isEqualTo(rsaKey.getPublicKey()); - assertThat(rsaJwk.toRSAPrivateKey()).isNull(); - assertThat(rsaJwk.getKeyUse()).isEqualTo(KeyUse.SIGNATURE); - assertThat(rsaJwk.getAlgorithm()).isEqualTo(JWSAlgorithm.RS256); + RSAKey rsaJwkResult = (RSAKey) jwkSet.getKeyByKeyId(rsaJwk.getKeyID()); + assertThat(rsaJwkResult).isNotNull(); + assertThat(rsaJwkResult.toRSAPublicKey()).isEqualTo(rsaJwk.toRSAPublicKey()); + assertThat(rsaJwkResult.toRSAPrivateKey()).isNull(); + assertThat(rsaJwkResult.getKeyUse()).isEqualTo(KeyUse.SIGNATURE); - ECKey ecJwk = (ECKey) jwkSet.getKeyByKeyId(ecKey.getId()); - assertThat(ecJwk).isNotNull(); - assertThat(ecJwk.toECPublicKey()).isEqualTo(ecKey.getPublicKey()); - assertThat(ecJwk.toECPublicKey()).isEqualTo(ecKey.getPublicKey()); - assertThat(ecJwk.toECPrivateKey()).isNull(); - assertThat(ecJwk.getKeyUse()).isEqualTo(KeyUse.SIGNATURE); - assertThat(ecJwk.getAlgorithm()).isEqualTo(JWSAlgorithm.ES256); + ECKey ecJwkResult = (ECKey) jwkSet.getKeyByKeyId(ecJwk.getKeyID()); + assertThat(ecJwkResult).isNotNull(); + assertThat(ecJwkResult.toECPublicKey()).isEqualTo(ecJwk.toECPublicKey()); + assertThat(ecJwkResult.toECPrivateKey()).isNull(); + assertThat(ecJwkResult.getKeyUse()).isEqualTo(KeyUse.SIGNATURE); } @Test public void doFilterWhenSymmetricKeysThenJwkSetResponseEmpty() throws Exception { - SymmetricKey secretKey = TestCryptoKeys.secretKey().build(); - when(this.keySource.getKeys()).thenReturn( - new HashSet<>(Collections.singleton(secretKey))); + OctetSequenceKey secretJwk = TestJwks.DEFAULT_SECRET_JWK; + given(this.jwkSource.get(any(), any())).willReturn(Collections.singletonList(secretJwk)); - String requestUri = JwkSetEndpointFilter.DEFAULT_JWK_SET_ENDPOINT_URI; + String requestUri = NimbusJwkSetEndpointFilter.DEFAULT_JWK_SET_ENDPOINT_URI; MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); request.setServletPath(requestUri); MockHttpServletResponse response = new MockHttpServletResponse(); diff --git a/samples/boot/oauth2-integration/authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java b/samples/boot/oauth2-integration/authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java index 270c594..6a95ceb 100644 --- a/samples/boot/oauth2-integration/authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java +++ b/samples/boot/oauth2-integration/authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,12 +15,18 @@ */ package sample.config; +import java.util.UUID; + +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; +import sample.jose.Jwks; + import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; -import org.springframework.security.crypto.key.CryptoKeySource; -import org.springframework.security.crypto.key.StaticKeyGeneratingCryptoKeySource; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.oidc.OidcScopes; @@ -29,8 +35,6 @@ import org.springframework.security.oauth2.server.authorization.client.Registere import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.config.ProviderSettings; -import java.util.UUID; - /** * @author Joe Grandja * @since 0.0.1 @@ -61,8 +65,10 @@ public class AuthorizationServerConfig { // @formatter:on @Bean - public CryptoKeySource keySource() { - return new StaticKeyGeneratingCryptoKeySource(); + public JWKSource jwkSource() { + RSAKey rsaKey = Jwks.generateRsa(); + JWKSet jwkSet = new JWKSet(rsaKey); + return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet); } @Bean diff --git a/samples/boot/oauth2-integration/authorizationserver/src/main/java/sample/jose/Jwks.java b/samples/boot/oauth2-integration/authorizationserver/src/main/java/sample/jose/Jwks.java new file mode 100644 index 0000000..0a02e6c --- /dev/null +++ b/samples/boot/oauth2-integration/authorizationserver/src/main/java/sample/jose/Jwks.java @@ -0,0 +1,74 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample.jose; + +import java.security.KeyPair; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.UUID; + +import javax.crypto.SecretKey; + +import com.nimbusds.jose.jwk.Curve; +import com.nimbusds.jose.jwk.ECKey; +import com.nimbusds.jose.jwk.OctetSequenceKey; +import com.nimbusds.jose.jwk.RSAKey; + +/** + * @author Joe Grandja + * @since 0.1.0 + */ +public final class Jwks { + + private Jwks() { + } + + public static RSAKey generateRsa() { + KeyPair keyPair = KeyGeneratorUtils.generateRsaKey(); + RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); + RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); + // @formatter:off + return new RSAKey.Builder(publicKey) + .privateKey(privateKey) + .keyID(UUID.randomUUID().toString()) + .build(); + // @formatter:on + } + + public static ECKey generateEc() { + KeyPair keyPair = KeyGeneratorUtils.generateEcKey(); + ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic(); + ECPrivateKey privateKey = (ECPrivateKey) keyPair.getPrivate(); + Curve curve = Curve.forECParameterSpec(publicKey.getParams()); + // @formatter:off + return new ECKey.Builder(curve, publicKey) + .privateKey(privateKey) + .keyID(UUID.randomUUID().toString()) + .build(); + // @formatter:on + } + + public static OctetSequenceKey generateSecret() { + SecretKey secretKey = KeyGeneratorUtils.generateSecretKey(); + // @formatter:off + return new OctetSequenceKey.Builder(secretKey) + .keyID(UUID.randomUUID().toString()) + .build(); + // @formatter:on + } +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/crypto/key/KeyGeneratorUtils.java b/samples/boot/oauth2-integration/authorizationserver/src/main/java/sample/jose/KeyGeneratorUtils.java similarity index 95% rename from oauth2-authorization-server/src/main/java/org/springframework/security/crypto/key/KeyGeneratorUtils.java rename to samples/boot/oauth2-integration/authorizationserver/src/main/java/sample/jose/KeyGeneratorUtils.java index 748235e..babaf28 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/crypto/key/KeyGeneratorUtils.java +++ b/samples/boot/oauth2-integration/authorizationserver/src/main/java/sample/jose/KeyGeneratorUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,10 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.security.crypto.key; +package sample.jose; -import javax.crypto.KeyGenerator; -import javax.crypto.SecretKey; import java.math.BigInteger; import java.security.KeyPair; import java.security.KeyPairGenerator; @@ -25,12 +23,18 @@ import java.security.spec.ECParameterSpec; import java.security.spec.ECPoint; import java.security.spec.EllipticCurve; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; + /** * @author Joe Grandja - * @since 0.0.1 + * @since 0.1.0 */ final class KeyGeneratorUtils { + private KeyGeneratorUtils() { + } + static SecretKey generateSecretKey() { SecretKey hmacKey; try {