Remove CryptoKeySource
Closes gh-196
This commit is contained in:
parent
4b37606807
commit
12f4001c9d
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -15,18 +15,28 @@
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer;
|
||||
import org.springframework.security.crypto.key.CryptoKeySource;
|
||||
import org.springframework.security.oauth2.jose.jws.NimbusJwsEncoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwsEncoder;
|
||||
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationProvider;
|
||||
@ -36,12 +46,12 @@ import org.springframework.security.oauth2.server.authorization.authentication.O
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.web.JwkSetEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcProviderConfigurationEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.NimbusJwkSetEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientAuthenticationFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenRevocationEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcProviderConfigurationEndpointFilter;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
|
||||
import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint;
|
||||
@ -55,12 +65,6 @@ import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* An {@link AbstractHttpConfigurer} for OAuth 2.0 Authorization Server support.
|
||||
*
|
||||
@ -73,7 +77,7 @@ import java.util.Map;
|
||||
* @see OAuth2AuthorizationEndpointFilter
|
||||
* @see OAuth2TokenEndpointFilter
|
||||
* @see OAuth2TokenRevocationEndpointFilter
|
||||
* @see JwkSetEndpointFilter
|
||||
* @see NimbusJwkSetEndpointFilter
|
||||
* @see OidcProviderConfigurationEndpointFilter
|
||||
* @see OAuth2ClientAuthenticationFilter
|
||||
*/
|
||||
@ -92,7 +96,7 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
|
||||
private final RequestMatcher tokenRevocationEndpointMatcher = new AntPathRequestMatcher(
|
||||
OAuth2TokenRevocationEndpointFilter.DEFAULT_TOKEN_REVOCATION_ENDPOINT_URI, HttpMethod.POST.name());
|
||||
private final RequestMatcher jwkSetEndpointMatcher = new AntPathRequestMatcher(
|
||||
JwkSetEndpointFilter.DEFAULT_JWK_SET_ENDPOINT_URI, HttpMethod.GET.name());
|
||||
NimbusJwkSetEndpointFilter.DEFAULT_JWK_SET_ENDPOINT_URI, HttpMethod.GET.name());
|
||||
private final RequestMatcher oidcProviderConfigurationEndpointMatcher = new AntPathRequestMatcher(
|
||||
OidcProviderConfigurationEndpointFilter.DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI, HttpMethod.GET.name());
|
||||
|
||||
@ -120,18 +124,6 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the source for cryptographic keys.
|
||||
*
|
||||
* @param keySource the source for cryptographic keys
|
||||
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2AuthorizationServerConfigurer<B> keySource(CryptoKeySource keySource) {
|
||||
Assert.notNull(keySource, "keySource cannot be null");
|
||||
this.getBuilder().setSharedObject(CryptoKeySource.class, keySource);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the provider settings.
|
||||
*
|
||||
@ -219,8 +211,9 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
|
||||
builder.addFilterBefore(postProcess(oidcProviderConfigurationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
|
||||
}
|
||||
|
||||
JwkSetEndpointFilter jwkSetEndpointFilter = new JwkSetEndpointFilter(
|
||||
getKeySource(builder),
|
||||
JWKSource<SecurityContext> jwkSource = getJwkSource(builder);
|
||||
NimbusJwkSetEndpointFilter jwkSetEndpointFilter = new NimbusJwkSetEndpointFilter(
|
||||
jwkSource,
|
||||
providerSettings.jwkSetEndpoint());
|
||||
builder.addFilterBefore(postProcess(jwkSetEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
|
||||
|
||||
@ -284,21 +277,27 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
|
||||
}
|
||||
|
||||
private static <B extends HttpSecurityBuilder<B>> JwtEncoder getJwtEncoder(B builder) {
|
||||
JwtEncoder jwtEncoder = getOptionalBean(builder, JwtEncoder.class);
|
||||
JwtEncoder jwtEncoder = builder.getSharedObject(JwtEncoder.class);
|
||||
if (jwtEncoder == null) {
|
||||
CryptoKeySource keySource = getKeySource(builder);
|
||||
jwtEncoder = new NimbusJwsEncoder(keySource);
|
||||
jwtEncoder = getOptionalBean(builder, JwtEncoder.class);
|
||||
if (jwtEncoder == null) {
|
||||
JWKSource<SecurityContext> jwkSource = getJwkSource(builder);
|
||||
jwtEncoder = new NimbusJwsEncoder(jwkSource);
|
||||
}
|
||||
builder.setSharedObject(JwtEncoder.class, jwtEncoder);
|
||||
}
|
||||
return jwtEncoder;
|
||||
}
|
||||
|
||||
private static <B extends HttpSecurityBuilder<B>> CryptoKeySource getKeySource(B builder) {
|
||||
CryptoKeySource keySource = builder.getSharedObject(CryptoKeySource.class);
|
||||
if (keySource == null) {
|
||||
keySource = getBean(builder, CryptoKeySource.class);
|
||||
builder.setSharedObject(CryptoKeySource.class, keySource);
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <B extends HttpSecurityBuilder<B>> JWKSource<SecurityContext> getJwkSource(B builder) {
|
||||
JWKSource<SecurityContext> jwkSource = builder.getSharedObject(JWKSource.class);
|
||||
if (jwkSource == null) {
|
||||
ResolvableType type = ResolvableType.forClassWithGenerics(JWKSource.class, SecurityContext.class);
|
||||
jwkSource = getBean(builder, type);
|
||||
builder.setSharedObject(JWKSource.class, jwkSource);
|
||||
}
|
||||
return keySource;
|
||||
return jwkSource;
|
||||
}
|
||||
|
||||
private static <B extends HttpSecurityBuilder<B>> ProviderSettings getProviderSettings(B builder) {
|
||||
@ -317,6 +316,19 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
|
||||
return builder.getSharedObject(ApplicationContext.class).getBean(type);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <B extends HttpSecurityBuilder<B>, T> T getBean(B builder, ResolvableType type) {
|
||||
ApplicationContext context = builder.getSharedObject(ApplicationContext.class);
|
||||
String[] names = context.getBeanNamesForType(type);
|
||||
if (names.length == 1) {
|
||||
return (T) context.getBean(names[0]);
|
||||
}
|
||||
if (names.length > 1) {
|
||||
throw new NoUniqueBeanDefinitionException(type, names);
|
||||
}
|
||||
throw new NoSuchBeanDefinitionException(type);
|
||||
}
|
||||
|
||||
private static <B extends HttpSecurityBuilder<B>, T> T getOptionalBean(B builder, Class<T> type) {
|
||||
Map<String, T> beansMap = BeanFactoryUtils.beansOfTypeIncludingAncestors(
|
||||
builder.getSharedObject(ApplicationContext.class), type);
|
||||
|
@ -1,78 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.crypto.key;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* A {@link CryptoKey} that holds a {@code java.security.PrivateKey}
|
||||
* and {@code java.security.PublicKey} used for asymmetric algorithm's.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.1.0
|
||||
* @see CryptoKey
|
||||
* @see PrivateKey
|
||||
* @see PublicKey
|
||||
*/
|
||||
public final class AsymmetricKey extends CryptoKey<PrivateKey> {
|
||||
private final PublicKey publicKey;
|
||||
|
||||
private AsymmetricKey(PrivateKey privateKey, PublicKey publicKey, String id, Map<String, Object> metadata) {
|
||||
super(privateKey, id, metadata);
|
||||
this.publicKey = publicKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@code java.security.PublicKey}.
|
||||
*
|
||||
* @return the {@code java.security.PublicKey}
|
||||
*/
|
||||
public PublicKey getPublicKey() {
|
||||
return this.publicKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link AsymmetricKey}.
|
||||
*/
|
||||
public static class Builder extends AbstractBuilder<AsymmetricKey, Builder> {
|
||||
private PublicKey publicKey;
|
||||
|
||||
Builder(PrivateKey privateKey, PublicKey publicKey) {
|
||||
super(privateKey);
|
||||
Assert.notNull(publicKey, "publicKey cannot be null");
|
||||
this.publicKey = publicKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the {@link AsymmetricKey}.
|
||||
*
|
||||
* @return the {@link AsymmetricKey}
|
||||
*/
|
||||
@Override
|
||||
public AsymmetricKey build() {
|
||||
if (!StringUtils.hasText(this.id)) {
|
||||
this.id = UUID.randomUUID().toString();
|
||||
}
|
||||
return new AsymmetricKey((PrivateKey) this.key, this.publicKey, this.id, this.metadata);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,231 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.crypto.key;
|
||||
|
||||
import org.springframework.security.oauth2.server.authorization.Version;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.io.Serializable;
|
||||
import java.security.Key;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* A holder of a {@code java.security.Key} used for cryptographic operations.
|
||||
*
|
||||
* @param <K> the type of {@code java.security.Key}
|
||||
* @author Joe Grandja
|
||||
* @since 0.1.0
|
||||
* @see CryptoKeySource
|
||||
*/
|
||||
public class CryptoKey<K extends Key> implements Serializable {
|
||||
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
|
||||
private final K key;
|
||||
private final String id;
|
||||
private final Map<String, Object> metadata;
|
||||
|
||||
/**
|
||||
* Constructs a {@code CryptoKey} using the provided parameters.
|
||||
*
|
||||
* @param key the {@code java.security.Key}
|
||||
* @param id the logical identifier for the {@code key}
|
||||
*/
|
||||
protected CryptoKey(K key, String id) {
|
||||
this(key, id, Collections.emptyMap());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a {@code CryptoKey} using the provided parameters.
|
||||
*
|
||||
* @param key the {@code java.security.Key}
|
||||
* @param id the logical identifier for the {@code key}
|
||||
* @param metadata the metadata describing the {@code key}
|
||||
*/
|
||||
protected CryptoKey(K key, String id, Map<String, Object> metadata) {
|
||||
Assert.notNull(key, "key cannot be null");
|
||||
Assert.hasText(id, "id cannot be empty");
|
||||
Assert.notNull(metadata, "metadata cannot be null");
|
||||
this.key = key;
|
||||
this.id = id;
|
||||
this.metadata = Collections.unmodifiableMap(new LinkedHashMap<>(metadata));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a type of {@code java.security.Key},
|
||||
* e.g. {@code javax.crypto.SecretKey} or {@code java.security.PrivateKey}.
|
||||
*
|
||||
* @return the type of {@code java.security.Key}
|
||||
*/
|
||||
public final K getKey() {
|
||||
return this.key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the logical identifier for this key.
|
||||
*
|
||||
* @return the logical identifier for this key
|
||||
*/
|
||||
public final String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the metadata value associated to this key.
|
||||
*
|
||||
* @param name the name of the metadata
|
||||
* @param <T> the type of the metadata
|
||||
* @return the metadata value, or {@code null} if not available
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public final <T> T getMetadata(String name) {
|
||||
Assert.hasText(name, "name cannot be empty");
|
||||
return (T) this.metadata.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the metadata associated to this key.
|
||||
*
|
||||
* @return a {@code Map} of the metadata
|
||||
*/
|
||||
public final Map<String, Object> getMetadata() {
|
||||
return this.metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the algorithm for this key.
|
||||
*
|
||||
* @return the algorithm for this key
|
||||
*/
|
||||
public final String getAlgorithm() {
|
||||
return getKey().getAlgorithm();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
CryptoKey<?> that = (CryptoKey<?>) obj;
|
||||
return Objects.equals(this.id, that.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(this.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link SymmetricKey} via {@link SymmetricKey.Builder}.
|
||||
*
|
||||
* @param secretKey the {@code javax.crypto.SecretKey}
|
||||
* @return the {@link SymmetricKey.Builder}
|
||||
*/
|
||||
public static SymmetricKey.Builder symmetric(SecretKey secretKey) {
|
||||
return new SymmetricKey.Builder(secretKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link AsymmetricKey} via {@link AsymmetricKey.Builder}.
|
||||
*
|
||||
* @param privateKey the {@code java.security.PrivateKey}
|
||||
* @param publicKey the {@code java.security.PublicKey}
|
||||
* @return the {@link AsymmetricKey.Builder}
|
||||
*/
|
||||
public static AsymmetricKey.Builder asymmetric(PrivateKey privateKey, PublicKey publicKey) {
|
||||
return new AsymmetricKey.Builder(privateKey, publicKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base builder for {@link CryptoKey}.
|
||||
*
|
||||
* @param <T> the type of {@link CryptoKey}
|
||||
* @param <B> the type of {@link AbstractBuilder}
|
||||
*/
|
||||
protected abstract static class AbstractBuilder<T extends CryptoKey<?>, B extends AbstractBuilder<T, B>> implements Serializable {
|
||||
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
|
||||
protected Key key;
|
||||
protected String id;
|
||||
protected Map<String, Object> metadata = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Sub-class constructor.
|
||||
*
|
||||
* @param key the {@code java.security.Key}
|
||||
*/
|
||||
protected AbstractBuilder(Key key) {
|
||||
Assert.notNull(key, "key cannot be null");
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the logical identifier for this key.
|
||||
*
|
||||
* @param id the logical identifier for this key
|
||||
* @return the type of {@link AbstractBuilder}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public B id(String id) {
|
||||
this.id = id;
|
||||
return (B) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds metadata for this key.
|
||||
*
|
||||
* @param name the name of the metadata
|
||||
* @param value the value of the metadata
|
||||
* @return the type of {@link AbstractBuilder}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public B metadata(String name, Object value) {
|
||||
Assert.hasText(name, "name cannot be empty");
|
||||
Assert.notNull(value, "value cannot be null");
|
||||
this.metadata.put(name, value);
|
||||
return (B) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@code Consumer} of the metadata {@code Map}
|
||||
* allowing the ability to add, replace, or remove.
|
||||
*
|
||||
* @param metadataConsumer a {@link Consumer} of the metadata {@code Map}
|
||||
* @return the type of {@link AbstractBuilder}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public B metadata(Consumer<Map<String, Object>> metadataConsumer) {
|
||||
metadataConsumer.accept(this.metadata);
|
||||
return (B) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the {@link CryptoKey}.
|
||||
*
|
||||
* @return the {@link CryptoKey}
|
||||
*/
|
||||
protected abstract T build();
|
||||
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.crypto.key;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A source for {@link CryptoKey}'s.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.1.0
|
||||
* @see CryptoKey
|
||||
* @see SymmetricKey
|
||||
* @see AsymmetricKey
|
||||
*/
|
||||
public interface CryptoKeySource {
|
||||
|
||||
/**
|
||||
* Returns a {@code Set} of {@link CryptoKey}'s.
|
||||
*
|
||||
* @return a {@code Set} of {@link CryptoKey}'s
|
||||
*/
|
||||
Set<CryptoKey<?>> getKeys();
|
||||
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.crypto.key;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.security.KeyPair;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.springframework.security.crypto.key.KeyGeneratorUtils.generateRsaKey;
|
||||
import static org.springframework.security.crypto.key.KeyGeneratorUtils.generateSecretKey;
|
||||
|
||||
/**
|
||||
* An implementation of a {@link CryptoKeySource} that generates the {@link CryptoKey}'s when constructed.
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> This implementation should ONLY be used during development/testing.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.1.0
|
||||
* @see CryptoKeySource
|
||||
*/
|
||||
public final class StaticKeyGeneratingCryptoKeySource implements CryptoKeySource {
|
||||
private final Map<String, CryptoKey<?>> keys;
|
||||
|
||||
public StaticKeyGeneratingCryptoKeySource() {
|
||||
this.keys = Collections.unmodifiableMap(generateKeys());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<CryptoKey<?>> getKeys() {
|
||||
return new HashSet<>(this.keys.values());
|
||||
}
|
||||
|
||||
private static Map<String, CryptoKey<?>> generateKeys() {
|
||||
KeyPair rsaKeyPair = generateRsaKey();
|
||||
AsymmetricKey asymmetricKey = CryptoKey.asymmetric(rsaKeyPair.getPrivate(), rsaKeyPair.getPublic()).build();
|
||||
|
||||
SecretKey hmacKey = generateSecretKey();
|
||||
SymmetricKey symmetricKey = CryptoKey.symmetric(hmacKey).build();
|
||||
|
||||
return Stream.of(asymmetricKey, symmetricKey)
|
||||
.collect(Collectors.toMap(CryptoKey::getId, v -> v));
|
||||
}
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.crypto.key;
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* A {@link CryptoKey} that holds a {@code javax.crypto.SecretKey}
|
||||
* used for symmetric algorithm's.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.1.0
|
||||
* @see CryptoKey
|
||||
* @see SecretKey
|
||||
*/
|
||||
public final class SymmetricKey extends CryptoKey<SecretKey> {
|
||||
|
||||
private SymmetricKey(SecretKey key, String id, Map<String, Object> metadata) {
|
||||
super(key, id, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link SymmetricKey}.
|
||||
*/
|
||||
public static class Builder extends AbstractBuilder<SymmetricKey, Builder> {
|
||||
|
||||
Builder(SecretKey secretKey) {
|
||||
super(secretKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the {@link SymmetricKey}.
|
||||
*
|
||||
* @return the {@link SymmetricKey}
|
||||
*/
|
||||
@Override
|
||||
public SymmetricKey build() {
|
||||
if (!StringUtils.hasText(this.id)) {
|
||||
this.id = UUID.randomUUID().toString();
|
||||
}
|
||||
return new SymmetricKey((SecretKey) this.key, this.id, this.metadata);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -13,30 +13,19 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.jose;
|
||||
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.util.Assert;
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.springframework.security.oauth2.jose.JoseHeaderNames.ALG;
|
||||
import static org.springframework.security.oauth2.jose.JoseHeaderNames.CRIT;
|
||||
import static org.springframework.security.oauth2.jose.JoseHeaderNames.CTY;
|
||||
import static org.springframework.security.oauth2.jose.JoseHeaderNames.JKU;
|
||||
import static org.springframework.security.oauth2.jose.JoseHeaderNames.JWK;
|
||||
import static org.springframework.security.oauth2.jose.JoseHeaderNames.KID;
|
||||
import static org.springframework.security.oauth2.jose.JoseHeaderNames.TYP;
|
||||
import static org.springframework.security.oauth2.jose.JoseHeaderNames.X5C;
|
||||
import static org.springframework.security.oauth2.jose.JoseHeaderNames.X5T;
|
||||
import static org.springframework.security.oauth2.jose.JoseHeaderNames.X5T_S256;
|
||||
import static org.springframework.security.oauth2.jose.JoseHeaderNames.X5U;
|
||||
import org.springframework.security.oauth2.core.converter.ClaimConversionService;
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* The JOSE header is a JSON object representing the header parameters of a JSON Web Token,
|
||||
@ -55,16 +44,16 @@ public final class JoseHeader {
|
||||
private final Map<String, Object> headers;
|
||||
|
||||
private JoseHeader(Map<String, Object> headers) {
|
||||
this.headers = Collections.unmodifiableMap(new LinkedHashMap<>(headers));
|
||||
this.headers = Collections.unmodifiableMap(new HashMap<>(headers));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JWS algorithm used to digitally sign the JWS.
|
||||
* Returns the {@link JwsAlgorithm JWS algorithm} used to digitally sign the JWS.
|
||||
*
|
||||
* @return the JWS algorithm
|
||||
*/
|
||||
public JwsAlgorithm getJwsAlgorithm() {
|
||||
return getHeader(ALG);
|
||||
return getHeader(JoseHeaderNames.ALG);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -73,8 +62,8 @@ public final class JoseHeader {
|
||||
*
|
||||
* @return the JWK Set URL
|
||||
*/
|
||||
public String getJwkSetUri() {
|
||||
return getHeader(JKU);
|
||||
public URL getJwkSetUri() {
|
||||
return getHeader(JoseHeaderNames.JKU);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -84,7 +73,7 @@ public final class JoseHeader {
|
||||
* @return the JSON Web Key
|
||||
*/
|
||||
public Map<String, Object> getJwk() {
|
||||
return getHeader(JWK);
|
||||
return getHeader(JoseHeaderNames.JWK);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -93,7 +82,7 @@ public final class JoseHeader {
|
||||
* @return the key ID
|
||||
*/
|
||||
public String getKeyId() {
|
||||
return getHeader(KID);
|
||||
return getHeader(JoseHeaderNames.KID);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -102,8 +91,8 @@ public final class JoseHeader {
|
||||
*
|
||||
* @return the X.509 URL
|
||||
*/
|
||||
public String getX509Uri() {
|
||||
return getHeader(X5U);
|
||||
public URL getX509Uri() {
|
||||
return getHeader(JoseHeaderNames.X5U);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -113,7 +102,7 @@ public final class JoseHeader {
|
||||
* @return the X.509 certificate chain
|
||||
*/
|
||||
public List<String> getX509CertificateChain() {
|
||||
return getHeader(X5C);
|
||||
return getHeader(JoseHeaderNames.X5C);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -123,7 +112,7 @@ public final class JoseHeader {
|
||||
* @return the X.509 certificate SHA-1 thumbprint
|
||||
*/
|
||||
public String getX509SHA1Thumbprint() {
|
||||
return getHeader(X5T);
|
||||
return getHeader(JoseHeaderNames.X5T);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -133,7 +122,7 @@ public final class JoseHeader {
|
||||
* @return the X.509 certificate SHA-256 thumbprint
|
||||
*/
|
||||
public String getX509SHA256Thumbprint() {
|
||||
return getHeader(X5T_S256);
|
||||
return getHeader(JoseHeaderNames.X5T_S256);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -143,7 +132,7 @@ public final class JoseHeader {
|
||||
* @return the critical headers
|
||||
*/
|
||||
public Set<String> getCritical() {
|
||||
return getHeader(CRIT);
|
||||
return getHeader(JoseHeaderNames.CRIT);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -152,7 +141,7 @@ public final class JoseHeader {
|
||||
* @return the type header
|
||||
*/
|
||||
public String getType() {
|
||||
return getHeader(TYP);
|
||||
return getHeader(JoseHeaderNames.TYP);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -161,7 +150,7 @@ public final class JoseHeader {
|
||||
* @return the content type header
|
||||
*/
|
||||
public String getContentType() {
|
||||
return getHeader(CTY);
|
||||
return getHeader(JoseHeaderNames.CTY);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -209,12 +198,12 @@ public final class JoseHeader {
|
||||
/**
|
||||
* A builder for {@link JoseHeader}.
|
||||
*/
|
||||
public static class Builder {
|
||||
private final Map<String, Object> headers = new LinkedHashMap<>();
|
||||
public static final class Builder {
|
||||
private final Map<String, Object> headers = new HashMap<>();
|
||||
|
||||
private Builder(JwsAlgorithm jwsAlgorithm) {
|
||||
Assert.notNull(jwsAlgorithm, "jwsAlgorithm cannot be null");
|
||||
header(ALG, jwsAlgorithm);
|
||||
header(JoseHeaderNames.ALG, jwsAlgorithm);
|
||||
}
|
||||
|
||||
private Builder(JoseHeader headers) {
|
||||
@ -230,7 +219,7 @@ public final class JoseHeader {
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder jwkSetUri(String jwkSetUri) {
|
||||
return header(JKU, jwkSetUri);
|
||||
return header(JoseHeaderNames.JKU, jwkSetUri);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -241,7 +230,7 @@ public final class JoseHeader {
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder jwk(Map<String, Object> jwk) {
|
||||
return header(JWK, jwk);
|
||||
return header(JoseHeaderNames.JWK, jwk);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -251,7 +240,7 @@ public final class JoseHeader {
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder keyId(String keyId) {
|
||||
return header(KID, keyId);
|
||||
return header(JoseHeaderNames.KID, keyId);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -262,7 +251,7 @@ public final class JoseHeader {
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder x509Uri(String x509Uri) {
|
||||
return header(X5U, x509Uri);
|
||||
return header(JoseHeaderNames.X5U, x509Uri);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -273,7 +262,7 @@ public final class JoseHeader {
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder x509CertificateChain(List<String> x509CertificateChain) {
|
||||
return header(X5C, x509CertificateChain);
|
||||
return header(JoseHeaderNames.X5C, x509CertificateChain);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -284,7 +273,7 @@ public final class JoseHeader {
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder x509SHA1Thumbprint(String x509SHA1Thumbprint) {
|
||||
return header(X5T, x509SHA1Thumbprint);
|
||||
return header(JoseHeaderNames.X5T, x509SHA1Thumbprint);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -295,7 +284,7 @@ public final class JoseHeader {
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder x509SHA256Thumbprint(String x509SHA256Thumbprint) {
|
||||
return header(X5T_S256, x509SHA256Thumbprint);
|
||||
return header(JoseHeaderNames.X5T_S256, x509SHA256Thumbprint);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -306,7 +295,7 @@ public final class JoseHeader {
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder critical(Set<String> headerNames) {
|
||||
return header(CRIT, headerNames);
|
||||
return header(JoseHeaderNames.CRIT, headerNames);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -316,7 +305,7 @@ public final class JoseHeader {
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder type(String type) {
|
||||
return header(TYP, type);
|
||||
return header(JoseHeaderNames.TYP, type);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -326,7 +315,7 @@ public final class JoseHeader {
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder contentType(String contentType) {
|
||||
return header(CTY, contentType);
|
||||
return header(JoseHeaderNames.CTY, contentType);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -362,7 +351,19 @@ public final class JoseHeader {
|
||||
*/
|
||||
public JoseHeader build() {
|
||||
Assert.notEmpty(this.headers, "headers cannot be empty");
|
||||
convertAsURL(JoseHeaderNames.JKU);
|
||||
convertAsURL(JoseHeaderNames.X5U);
|
||||
return new JoseHeader(this.headers);
|
||||
}
|
||||
|
||||
private void convertAsURL(String header) {
|
||||
Object value = this.headers.get(header);
|
||||
if (value != null) {
|
||||
URL convertedValue = ClaimConversionService.getSharedInstance().convert(value, URL.class);
|
||||
Assert.isTrue(convertedValue != null,
|
||||
() -> "Unable to convert header '" + header + "' of type '" + value.getClass() + "' to URL.");
|
||||
this.headers.put(header, convertedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.jose;
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
/**
|
||||
* The Registered Header Parameter Names defined by the JSON Web Token (JWT),
|
||||
@ -28,69 +28,72 @@ package org.springframework.security.oauth2.jose;
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515#section-4">JWS JOSE Header</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7516#section-4">JWE JOSE Header</a>
|
||||
*/
|
||||
public interface JoseHeaderNames {
|
||||
public final class JoseHeaderNames {
|
||||
|
||||
/**
|
||||
* {@code alg} - the algorithm header identifies the cryptographic algorithm used to secure a JWS or JWE
|
||||
*/
|
||||
String ALG = "alg";
|
||||
public static final String ALG = "alg";
|
||||
|
||||
/**
|
||||
* {@code jku} - the JWK Set URL header is a URI that refers to a resource for a set of JSON-encoded public keys,
|
||||
* one of which corresponds to the key used to digitally sign a JWS or encrypt a JWE
|
||||
*/
|
||||
String JKU = "jku";
|
||||
public static final String JKU = "jku";
|
||||
|
||||
/**
|
||||
* {@code jwk} - the JSON Web Key header is the public key that corresponds to the key
|
||||
* used to digitally sign a JWS or encrypt a JWE
|
||||
*/
|
||||
String JWK = "jwk";
|
||||
public static final String JWK = "jwk";
|
||||
|
||||
/**
|
||||
* {@code kid} - the key ID header is a hint indicating which key was used to secure a JWS or JWE
|
||||
*/
|
||||
String KID = "kid";
|
||||
public static final String KID = "kid";
|
||||
|
||||
/**
|
||||
* {@code x5u} - the X.509 URL header is a URI that refers to a resource for the X.509 public key certificate
|
||||
* or certificate chain corresponding to the key used to digitally sign a JWS or encrypt a JWE
|
||||
*/
|
||||
String X5U = "x5u";
|
||||
public static final String X5U = "x5u";
|
||||
|
||||
/**
|
||||
* {@code x5c} - the X.509 certificate chain header contains the X.509 public key certificate
|
||||
* or certificate chain corresponding to the key used to digitally sign a JWS or encrypt a JWE
|
||||
*/
|
||||
String X5C = "x5c";
|
||||
public static final String X5C = "x5c";
|
||||
|
||||
/**
|
||||
* {@code x5t} - the X.509 certificate SHA-1 thumbprint header is a base64url-encoded SHA-1 thumbprint (a.k.a. digest)
|
||||
* of the DER encoding of the X.509 certificate corresponding to the key used to digitally sign a JWS or encrypt a JWE
|
||||
*/
|
||||
String X5T = "x5t";
|
||||
public static final String X5T = "x5t";
|
||||
|
||||
/**
|
||||
* {@code x5t#S256} - the X.509 certificate SHA-256 thumbprint header is a base64url-encoded SHA-256 thumbprint (a.k.a. digest)
|
||||
* of the DER encoding of the X.509 certificate corresponding to the key used to digitally sign a JWS or encrypt a JWE
|
||||
*/
|
||||
String X5T_S256 = "x5t#S256";
|
||||
public static final String X5T_S256 = "x5t#S256";
|
||||
|
||||
/**
|
||||
* {@code typ} - the type header is used by JWS/JWE applications to declare the media type of a JWS/JWE
|
||||
*/
|
||||
String TYP = "typ";
|
||||
public static final String TYP = "typ";
|
||||
|
||||
/**
|
||||
* {@code cty} - the content type header is used by JWS/JWE applications to declare the media type
|
||||
* of the secured content (the payload)
|
||||
*/
|
||||
String CTY = "cty";
|
||||
public static final String CTY = "cty";
|
||||
|
||||
/**
|
||||
* {@code crit} - the critical header indicates that extensions to the JWS/JWE/JWA specifications
|
||||
* are being used that MUST be understood and processed
|
||||
*/
|
||||
String CRIT = "crit";
|
||||
public static final String CRIT = "crit";
|
||||
|
||||
private JoseHeaderNames() {
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -15,22 +15,14 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.springframework.security.oauth2.jwt.JwtClaimNames.AUD;
|
||||
import static org.springframework.security.oauth2.jwt.JwtClaimNames.EXP;
|
||||
import static org.springframework.security.oauth2.jwt.JwtClaimNames.IAT;
|
||||
import static org.springframework.security.oauth2.jwt.JwtClaimNames.ISS;
|
||||
import static org.springframework.security.oauth2.jwt.JwtClaimNames.JTI;
|
||||
import static org.springframework.security.oauth2.jwt.JwtClaimNames.NBF;
|
||||
import static org.springframework.security.oauth2.jwt.JwtClaimNames.SUB;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* The {@link Jwt JWT} Claims Set is a JSON object representing the claims conveyed by a JSON Web Token.
|
||||
@ -46,7 +38,7 @@ public final class JwtClaimsSet implements JwtClaimAccessor {
|
||||
private final Map<String, Object> claims;
|
||||
|
||||
private JwtClaimsSet(Map<String, Object> claims) {
|
||||
this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims));
|
||||
this.claims = Collections.unmodifiableMap(new HashMap<>(claims));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -76,8 +68,8 @@ public final class JwtClaimsSet implements JwtClaimAccessor {
|
||||
/**
|
||||
* A builder for {@link JwtClaimsSet}.
|
||||
*/
|
||||
public static class Builder {
|
||||
private final Map<String, Object> claims = new LinkedHashMap<>();
|
||||
public static final class Builder {
|
||||
private final Map<String, Object> claims = new HashMap<>();
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
@ -94,7 +86,7 @@ public final class JwtClaimsSet implements JwtClaimAccessor {
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder issuer(String issuer) {
|
||||
return claim(ISS, issuer);
|
||||
return claim(JwtClaimNames.ISS, issuer);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -104,7 +96,7 @@ public final class JwtClaimsSet implements JwtClaimAccessor {
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder subject(String subject) {
|
||||
return claim(SUB, subject);
|
||||
return claim(JwtClaimNames.SUB, subject);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -114,7 +106,7 @@ public final class JwtClaimsSet implements JwtClaimAccessor {
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder audience(List<String> audience) {
|
||||
return claim(AUD, audience);
|
||||
return claim(JwtClaimNames.AUD, audience);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -125,7 +117,7 @@ public final class JwtClaimsSet implements JwtClaimAccessor {
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder expiresAt(Instant expiresAt) {
|
||||
return claim(EXP, expiresAt);
|
||||
return claim(JwtClaimNames.EXP, expiresAt);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -136,7 +128,7 @@ public final class JwtClaimsSet implements JwtClaimAccessor {
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder notBefore(Instant notBefore) {
|
||||
return claim(NBF, notBefore);
|
||||
return claim(JwtClaimNames.NBF, notBefore);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -146,7 +138,7 @@ public final class JwtClaimsSet implements JwtClaimAccessor {
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder issuedAt(Instant issuedAt) {
|
||||
return claim(IAT, issuedAt);
|
||||
return claim(JwtClaimNames.IAT, issuedAt);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -156,7 +148,7 @@ public final class JwtClaimsSet implements JwtClaimAccessor {
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder id(String jti) {
|
||||
return claim(JTI, jti);
|
||||
return claim(JwtClaimNames.JTI, jti);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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.
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -13,54 +13,49 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.jose.jws;
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
import java.net.URL;
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.nimbusds.jose.JOSEException;
|
||||
import com.nimbusds.jose.JOSEObjectType;
|
||||
import com.nimbusds.jose.JWSAlgorithm;
|
||||
import com.nimbusds.jose.JWSHeader;
|
||||
import com.nimbusds.jose.JWSSigner;
|
||||
import com.nimbusds.jose.KeyLengthException;
|
||||
import com.nimbusds.jose.crypto.MACSigner;
|
||||
import com.nimbusds.jose.crypto.RSASSASigner;
|
||||
import com.nimbusds.jose.KeySourceException;
|
||||
import com.nimbusds.jose.crypto.factories.DefaultJWSSignerFactory;
|
||||
import com.nimbusds.jose.jwk.JWK;
|
||||
import com.nimbusds.jose.jwk.JWKMatcher;
|
||||
import com.nimbusds.jose.jwk.JWKSelector;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import com.nimbusds.jose.produce.JWSSignerFactory;
|
||||
import com.nimbusds.jose.util.Base64;
|
||||
import com.nimbusds.jose.util.Base64URL;
|
||||
import com.nimbusds.jwt.JWTClaimsSet;
|
||||
import com.nimbusds.jwt.SignedJWT;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.crypto.key.AsymmetricKey;
|
||||
import org.springframework.security.crypto.key.CryptoKey;
|
||||
import org.springframework.security.crypto.key.CryptoKeySource;
|
||||
import org.springframework.security.oauth2.jose.JoseHeader;
|
||||
import org.springframework.security.oauth2.jose.JoseHeaderNames;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncodingException;
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.security.PrivateKey;
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* An implementation of a {@link JwtEncoder} that encodes a JSON Web Token (JWT)
|
||||
* using the JSON Web Signature (JWS) Compact Serialization format.
|
||||
* The private/secret key used for signing the JWS is obtained
|
||||
* from the {@link CryptoKeySource} supplied via the constructor.
|
||||
* An implementation of a {@link JwtEncoder} that encodes a JSON Web Token (JWT) using the
|
||||
* JSON Web Signature (JWS) Compact Serialization format. The private/secret key used for
|
||||
* signing the JWS is supplied by the {@code com.nimbusds.jose.jwk.source.JWKSource}
|
||||
* provided via the constructor.
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> This implementation uses the Nimbus JOSE + JWT SDK.
|
||||
@ -68,50 +63,46 @@ import java.util.stream.Collectors;
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.1
|
||||
* @see JwtEncoder
|
||||
* @see CryptoKeySource
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519">JSON Web Token (JWT)</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515">JSON Web Signature (JWS)</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515#section-3.1">JWS Compact Serialization</a>
|
||||
* @see <a target="_blank" href="https://connect2id.com/products/nimbus-jose-jwt">Nimbus JOSE + JWT SDK</a>
|
||||
* @see com.nimbusds.jose.jwk.source.JWKSource
|
||||
* @see com.nimbusds.jose.jwk.JWK
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519">JSON Web Token
|
||||
* (JWT)</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515">JSON Web Signature
|
||||
* (JWS)</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515#section-3.1">JWS
|
||||
* Compact Serialization</a>
|
||||
* @see <a target="_blank" href="https://connect2id.com/products/nimbus-jose-jwt">Nimbus
|
||||
* JOSE + JWT SDK</a>
|
||||
*/
|
||||
public final class NimbusJwsEncoder implements JwtEncoder {
|
||||
private static final String ENCODING_ERROR_MESSAGE_TEMPLATE =
|
||||
"An error occurred while attempting to encode the Jwt: %s";
|
||||
private static final String RSA_KEY_TYPE = "RSA";
|
||||
private static final String EC_KEY_TYPE = "EC";
|
||||
private static final Map<JwsAlgorithm, String> jcaKeyAlgorithmMappings = new HashMap<JwsAlgorithm, String>() {
|
||||
{
|
||||
put(MacAlgorithm.HS256, "HmacSHA256");
|
||||
put(MacAlgorithm.HS384, "HmacSHA384");
|
||||
put(MacAlgorithm.HS512, "HmacSHA512");
|
||||
put(SignatureAlgorithm.RS256, RSA_KEY_TYPE);
|
||||
put(SignatureAlgorithm.RS384, RSA_KEY_TYPE);
|
||||
put(SignatureAlgorithm.RS512, RSA_KEY_TYPE);
|
||||
put(SignatureAlgorithm.ES256, EC_KEY_TYPE);
|
||||
put(SignatureAlgorithm.ES384, EC_KEY_TYPE);
|
||||
put(SignatureAlgorithm.ES512, EC_KEY_TYPE);
|
||||
}
|
||||
|
||||
private static final String ENCODING_ERROR_MESSAGE_TEMPLATE = "An error occurred while attempting to encode the Jwt: %s";
|
||||
|
||||
private static final Converter<JoseHeader, JWSHeader> JWS_HEADER_CONVERTER = new JwsHeaderConverter();
|
||||
|
||||
private static final Converter<JwtClaimsSet, JWTClaimsSet> JWT_CLAIMS_SET_CONVERTER = new JwtClaimsSetConverter();
|
||||
|
||||
private static final JWSSignerFactory JWS_SIGNER_FACTORY = new DefaultJWSSignerFactory();
|
||||
|
||||
private final Map<JWK, JWSSigner> jwsSigners = new ConcurrentHashMap<>();
|
||||
|
||||
private final JWKSource<SecurityContext> jwkSource;
|
||||
|
||||
private BiConsumer<JoseHeader.Builder, JwtClaimsSet.Builder> jwtCustomizer = (headers, claims) -> {
|
||||
};
|
||||
private static final Converter<JoseHeader, JWSHeader> jwsHeaderConverter = new JwsHeaderConverter();
|
||||
private static final Converter<JwtClaimsSet, JWTClaimsSet> jwtClaimsSetConverter = new JwtClaimsSetConverter();
|
||||
private final CryptoKeySource keySource;
|
||||
private BiConsumer<JoseHeader.Builder, JwtClaimsSet.Builder> jwtCustomizer = (headers, claims) -> {};
|
||||
|
||||
/**
|
||||
* Constructs a {@code NimbusJwsEncoder} using the provided parameters.
|
||||
*
|
||||
* @param keySource the source for cryptographic keys
|
||||
* @param jwkSource the {@code com.nimbusds.jose.jwk.source.JWKSource}
|
||||
*/
|
||||
public NimbusJwsEncoder(CryptoKeySource keySource) {
|
||||
Assert.notNull(keySource, "keySource cannot be null");
|
||||
this.keySource = keySource;
|
||||
public NimbusJwsEncoder(JWKSource<SecurityContext> jwkSource) {
|
||||
Assert.notNull(jwkSource, "jwkSource cannot be null");
|
||||
this.jwkSource = jwkSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link Jwt} customizer to be provided the
|
||||
* {@link JoseHeader.Builder} and {@link JwtClaimsSet.Builder}
|
||||
* allowing for further customizations.
|
||||
*
|
||||
* Sets the {@link Jwt} customizer to be provided the {@link JoseHeader.Builder} and
|
||||
* {@link JwtClaimsSet.Builder} allowing for further customizations.
|
||||
* @param jwtCustomizer the {@link Jwt} customizer to be provided the
|
||||
* {@link JoseHeader.Builder} and {@link JwtClaimsSet.Builder}
|
||||
*/
|
||||
@ -125,78 +116,85 @@ public final class NimbusJwsEncoder implements JwtEncoder {
|
||||
Assert.notNull(headers, "headers cannot be null");
|
||||
Assert.notNull(claims, "claims cannot be null");
|
||||
|
||||
CryptoKey<?> cryptoKey = selectKey(headers);
|
||||
if (cryptoKey == null) {
|
||||
throw new JwtEncodingException(String.format(
|
||||
ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Unsupported key for algorithm '" + headers.getJwsAlgorithm().getName() + "'"));
|
||||
}
|
||||
|
||||
JWSSigner jwsSigner;
|
||||
if (AsymmetricKey.class.isAssignableFrom(cryptoKey.getClass())) {
|
||||
if (!cryptoKey.getAlgorithm().equals(RSA_KEY_TYPE)) {
|
||||
throw new JwtEncodingException(String.format(
|
||||
ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Unsupported key type '" + cryptoKey.getAlgorithm() + "'"));
|
||||
}
|
||||
PrivateKey privateKey = (PrivateKey) cryptoKey.getKey();
|
||||
jwsSigner = new RSASSASigner(privateKey);
|
||||
} else {
|
||||
SecretKey secretKey = (SecretKey) cryptoKey.getKey();
|
||||
try {
|
||||
jwsSigner = new MACSigner(secretKey);
|
||||
} catch (KeyLengthException ex) {
|
||||
throw new JwtEncodingException(String.format(
|
||||
ENCODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex);
|
||||
}
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
JoseHeader.Builder headersBuilder = JoseHeader.from(headers)
|
||||
.type(JOSEObjectType.JWT.getType())
|
||||
.keyId(cryptoKey.getId());
|
||||
.type(JOSEObjectType.JWT.getType());
|
||||
JwtClaimsSet.Builder claimsBuilder = JwtClaimsSet.from(claims)
|
||||
.id(UUID.randomUUID().toString());
|
||||
// @formatter:on
|
||||
|
||||
this.jwtCustomizer.accept(headersBuilder, claimsBuilder);
|
||||
|
||||
headers = headersBuilder.build();
|
||||
JWK jwk = selectJwk(headersBuilder);
|
||||
if (jwk == null) {
|
||||
throw new JwtEncodingException(
|
||||
String.format(ENCODING_ERROR_MESSAGE_TEMPLATE, "Failed to select a JWK signing key"));
|
||||
}
|
||||
else if (!StringUtils.hasText(jwk.getKeyID())) {
|
||||
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"The \"kid\" (key ID) from the selected JWK cannot be empty"));
|
||||
}
|
||||
|
||||
headers = headersBuilder.keyId(jwk.getKeyID()).build();
|
||||
claims = claimsBuilder.build();
|
||||
|
||||
JWSHeader jwsHeader = jwsHeaderConverter.convert(headers);
|
||||
JWTClaimsSet jwtClaimsSet = jwtClaimsSetConverter.convert(claims);
|
||||
JWSHeader jwsHeader = JWS_HEADER_CONVERTER.convert(headers);
|
||||
JWTClaimsSet jwtClaimsSet = JWT_CLAIMS_SET_CONVERTER.convert(claims);
|
||||
|
||||
SignedJWT signedJWT = new SignedJWT(jwsHeader, jwtClaimsSet);
|
||||
JWSSigner jwsSigner = this.jwsSigners.computeIfAbsent(jwk, (key) -> {
|
||||
try {
|
||||
signedJWT.sign(jwsSigner);
|
||||
} catch (JOSEException ex) {
|
||||
throw new JwtEncodingException(String.format(
|
||||
ENCODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex);
|
||||
return JWS_SIGNER_FACTORY.createJWSSigner(key);
|
||||
}
|
||||
String jws = signedJWT.serialize();
|
||||
catch (JOSEException ex) {
|
||||
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Failed to create a JWS Signer -> " + ex.getMessage()), ex);
|
||||
}
|
||||
});
|
||||
|
||||
return new Jwt(jws, claims.getIssuedAt(), claims.getExpiresAt(),
|
||||
headers.getHeaders(), claims.getClaims());
|
||||
SignedJWT signedJwt = new SignedJWT(jwsHeader, jwtClaimsSet);
|
||||
try {
|
||||
signedJwt.sign(jwsSigner);
|
||||
}
|
||||
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());
|
||||
}
|
||||
|
||||
private CryptoKey<?> selectKey(JoseHeader headers) {
|
||||
JwsAlgorithm jwsAlgorithm = headers.getJwsAlgorithm();
|
||||
String keyAlgorithm = jcaKeyAlgorithmMappings.get(jwsAlgorithm);
|
||||
if (!StringUtils.hasText(keyAlgorithm)) {
|
||||
return null;
|
||||
private JWK selectJwk(JoseHeader.Builder headersBuilder) {
|
||||
final AtomicReference<JWSAlgorithm> jwsAlgorithm = new AtomicReference<>();
|
||||
headersBuilder.headers((h) -> {
|
||||
JwsAlgorithm jwsAlg = (JwsAlgorithm) h.get(JoseHeaderNames.ALG);
|
||||
jwsAlgorithm.set(JWSAlgorithm.parse(jwsAlg.getName()));
|
||||
});
|
||||
JWSHeader jwsHeader = new JWSHeader(jwsAlgorithm.get());
|
||||
JWKSelector jwkSelector = new JWKSelector(JWKMatcher.forJWSHeader(jwsHeader));
|
||||
|
||||
List<JWK> jwks;
|
||||
try {
|
||||
jwks = this.jwkSource.get(jwkSelector, null);
|
||||
}
|
||||
catch (KeySourceException ex) {
|
||||
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Failed to select a JWK signing key -> " + ex.getMessage()), ex);
|
||||
}
|
||||
|
||||
return this.keySource.getKeys().stream()
|
||||
.filter(key -> key.getAlgorithm().equals(keyAlgorithm))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (jwks.size() > 1) {
|
||||
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Found multiple JWK signing keys for algorithm '" + jwsAlgorithm.get().getName() + "'"));
|
||||
}
|
||||
|
||||
return !jwks.isEmpty() ? jwks.get(0) : null;
|
||||
}
|
||||
|
||||
private static class JwsHeaderConverter implements Converter<JoseHeader, JWSHeader> {
|
||||
|
||||
@Override
|
||||
public JWSHeader convert(JoseHeader headers) {
|
||||
JWSHeader.Builder builder = new JWSHeader.Builder(
|
||||
JWSAlgorithm.parse(headers.getJwsAlgorithm().getName()));
|
||||
JWSHeader.Builder builder = new JWSHeader.Builder(JWSAlgorithm.parse(headers.getJwsAlgorithm().getName()));
|
||||
|
||||
Set<String> critical = headers.getCritical();
|
||||
if (!CollectionUtils.isEmpty(critical)) {
|
||||
@ -208,14 +206,14 @@ public final class NimbusJwsEncoder implements JwtEncoder {
|
||||
builder.contentType(contentType);
|
||||
}
|
||||
|
||||
String jwkSetUri = headers.getJwkSetUri();
|
||||
if (StringUtils.hasText(jwkSetUri)) {
|
||||
URL jwkSetUri = headers.getJwkSetUri();
|
||||
if (jwkSetUri != null) {
|
||||
try {
|
||||
builder.jwkURL(new URI(jwkSetUri));
|
||||
} catch (Exception ex) {
|
||||
throw new JwtEncodingException(String.format(
|
||||
ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Failed to convert '" + JoseHeaderNames.JKU + "' JOSE header"), ex);
|
||||
builder.jwkURL(jwkSetUri.toURI());
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Failed to convert '" + JoseHeaderNames.JKU + "' JOSE header to a URI"), ex);
|
||||
}
|
||||
}
|
||||
|
||||
@ -223,9 +221,9 @@ public final class NimbusJwsEncoder implements JwtEncoder {
|
||||
if (!CollectionUtils.isEmpty(jwk)) {
|
||||
try {
|
||||
builder.jwk(JWK.parse(jwk));
|
||||
} catch (Exception ex) {
|
||||
throw new JwtEncodingException(String.format(
|
||||
ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Failed to convert '" + JoseHeaderNames.JWK + "' JOSE header"), ex);
|
||||
}
|
||||
}
|
||||
@ -242,10 +240,7 @@ public final class NimbusJwsEncoder implements JwtEncoder {
|
||||
|
||||
List<String> x509CertificateChain = headers.getX509CertificateChain();
|
||||
if (!CollectionUtils.isEmpty(x509CertificateChain)) {
|
||||
builder.x509CertChain(
|
||||
x509CertificateChain.stream()
|
||||
.map(Base64::new)
|
||||
.collect(Collectors.toList()));
|
||||
builder.x509CertChain(x509CertificateChain.stream().map(Base64::new).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
String x509SHA1Thumbprint = headers.getX509SHA1Thumbprint();
|
||||
@ -258,19 +253,19 @@ public final class NimbusJwsEncoder implements JwtEncoder {
|
||||
builder.x509CertSHA256Thumbprint(new Base64URL(x509SHA256Thumbprint));
|
||||
}
|
||||
|
||||
String x509Uri = headers.getX509Uri();
|
||||
if (StringUtils.hasText(x509Uri)) {
|
||||
URL x509Uri = headers.getX509Uri();
|
||||
if (x509Uri != null) {
|
||||
try {
|
||||
builder.x509CertURL(new URI(x509Uri));
|
||||
} catch (Exception ex) {
|
||||
throw new JwtEncodingException(String.format(
|
||||
ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Failed to convert '" + JoseHeaderNames.X5U + "' JOSE header"), ex);
|
||||
builder.x509CertURL(x509Uri.toURI());
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Failed to convert '" + JoseHeaderNames.X5U + "' JOSE header to a URI"), ex);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> customHeaders = headers.getHeaders().entrySet().stream()
|
||||
.filter(header -> !JWSHeader.getRegisteredParameterNames().contains(header.getKey()))
|
||||
.filter((header) -> !JWSHeader.getRegisteredParameterNames().contains(header.getKey()))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
if (!CollectionUtils.isEmpty(customHeaders)) {
|
||||
builder.customParams(customHeaders);
|
||||
@ -278,6 +273,7 @@ public final class NimbusJwsEncoder implements JwtEncoder {
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class JwtClaimsSetConverter implements Converter<JwtClaimsSet, JWTClaimsSet> {
|
||||
@ -322,7 +318,7 @@ public final class NimbusJwsEncoder implements JwtEncoder {
|
||||
}
|
||||
|
||||
Map<String, Object> customClaims = claims.getClaims().entrySet().stream()
|
||||
.filter(claim -> !JWTClaimsSet.getRegisteredNames().contains(claim.getKey()))
|
||||
.filter((claim) -> !JWTClaimsSet.getRegisteredNames().contains(claim.getKey()))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
if (!CollectionUtils.isEmpty(customClaims)) {
|
||||
customClaims.forEach(builder::claim);
|
||||
@ -330,5 +326,7 @@ public final class NimbusJwsEncoder implements JwtEncoder {
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -15,70 +15,65 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.web;
|
||||
|
||||
import com.nimbusds.jose.JWSAlgorithm;
|
||||
import com.nimbusds.jose.jwk.Curve;
|
||||
import com.nimbusds.jose.jwk.ECKey;
|
||||
import com.nimbusds.jose.jwk.JWK;
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.KeyUse;
|
||||
import com.nimbusds.jose.jwk.RSAKey;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.crypto.key.AsymmetricKey;
|
||||
import org.springframework.security.crypto.key.CryptoKeySource;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
import java.security.interfaces.ECPublicKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKMatcher;
|
||||
import com.nimbusds.jose.jwk.JWKSelector;
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
/**
|
||||
* A {@code Filter} that processes JWK Set requests.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.1
|
||||
* @see CryptoKeySource
|
||||
* @see com.nimbusds.jose.jwk.source.JWKSource
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7517">JSON Web Key (JWK)</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7517#section-5">Section 5 JWK Set Format</a>
|
||||
*/
|
||||
public class JwkSetEndpointFilter extends OncePerRequestFilter {
|
||||
public class NimbusJwkSetEndpointFilter extends OncePerRequestFilter {
|
||||
/**
|
||||
* The default endpoint {@code URI} for JWK Set requests.
|
||||
*/
|
||||
public static final String DEFAULT_JWK_SET_ENDPOINT_URI = "/oauth2/jwks";
|
||||
|
||||
private final CryptoKeySource keySource;
|
||||
private final JWKSource<SecurityContext> jwkSource;
|
||||
private final JWKSelector jwkSelector;
|
||||
private final RequestMatcher requestMatcher;
|
||||
|
||||
/**
|
||||
* Constructs a {@code JwkSetEndpointFilter} using the provided parameters.
|
||||
*
|
||||
* @param keySource the source for cryptographic keys
|
||||
* Constructs a {@code NimbusJwkSetEndpointFilter} using the provided parameters.
|
||||
* @param jwkSource the {@code com.nimbusds.jose.jwk.source.JWKSource}
|
||||
*/
|
||||
public JwkSetEndpointFilter(CryptoKeySource keySource) {
|
||||
this(keySource, DEFAULT_JWK_SET_ENDPOINT_URI);
|
||||
public NimbusJwkSetEndpointFilter(JWKSource<SecurityContext> jwkSource) {
|
||||
this(jwkSource, DEFAULT_JWK_SET_ENDPOINT_URI);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a {@code JwkSetEndpointFilter} using the provided parameters.
|
||||
* Constructs a {@code NimbusJwkSetEndpointFilter} using the provided parameters.
|
||||
*
|
||||
* @param keySource the source for cryptographic keys
|
||||
* @param jwkSource the {@code com.nimbusds.jose.jwk.source.JWKSource}
|
||||
* @param jwkSetEndpointUri the endpoint {@code URI} for JWK Set requests
|
||||
*/
|
||||
public JwkSetEndpointFilter(CryptoKeySource keySource, String jwkSetEndpointUri) {
|
||||
Assert.notNull(keySource, "keySource cannot be null");
|
||||
public NimbusJwkSetEndpointFilter(JWKSource<SecurityContext> jwkSource, String jwkSetEndpointUri) {
|
||||
Assert.notNull(jwkSource, "jwkSource cannot be null");
|
||||
Assert.hasText(jwkSetEndpointUri, "jwkSetEndpointUri cannot be empty");
|
||||
this.keySource = keySource;
|
||||
this.jwkSource = jwkSource;
|
||||
this.jwkSelector = new JWKSelector(new JWKMatcher.Builder().publicOnly(true).build());
|
||||
this.requestMatcher = new AntPathRequestMatcher(jwkSetEndpointUri, HttpMethod.GET.name());
|
||||
}
|
||||
|
||||
@ -91,43 +86,17 @@ public class JwkSetEndpointFilter extends OncePerRequestFilter {
|
||||
return;
|
||||
}
|
||||
|
||||
JWKSet jwkSet = buildJwkSet();
|
||||
JWKSet jwkSet;
|
||||
try {
|
||||
jwkSet = new JWKSet(this.jwkSource.get(this.jwkSelector, null));
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException("Failed to select the JWK public key(s) -> " + ex.getMessage(), ex);
|
||||
}
|
||||
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
try (Writer writer = response.getWriter()) {
|
||||
writer.write(jwkSet.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private JWKSet buildJwkSet() {
|
||||
return new JWKSet(
|
||||
this.keySource.getKeys().stream()
|
||||
.filter(key -> AsymmetricKey.class.isAssignableFrom(key.getClass()))
|
||||
.map(AsymmetricKey.class::cast)
|
||||
.map(this::convert)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList())
|
||||
);
|
||||
}
|
||||
|
||||
private JWK convert(AsymmetricKey asymmetricKey) {
|
||||
JWK jwk = null;
|
||||
if (asymmetricKey.getPublicKey() instanceof RSAPublicKey) {
|
||||
RSAPublicKey publicKey = (RSAPublicKey) asymmetricKey.getPublicKey();
|
||||
jwk = new RSAKey.Builder(publicKey)
|
||||
.keyUse(KeyUse.SIGNATURE)
|
||||
.algorithm(JWSAlgorithm.RS256)
|
||||
.keyID(asymmetricKey.getId())
|
||||
.build();
|
||||
} else if (asymmetricKey.getPublicKey() instanceof ECPublicKey) {
|
||||
ECPublicKey publicKey = (ECPublicKey) asymmetricKey.getPublicKey();
|
||||
Curve curve = Curve.forECParameterSpec(publicKey.getParams());
|
||||
jwk = new ECKey.Builder(curve, publicKey)
|
||||
.keyUse(KeyUse.SIGNATURE)
|
||||
.algorithm(JWSAlgorithm.ES256)
|
||||
.keyID(asymmetricKey.getId())
|
||||
.build();
|
||||
}
|
||||
return jwk;
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -15,11 +15,20 @@
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
@ -27,16 +36,15 @@ import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||
import org.springframework.security.config.test.SpringTestRule;
|
||||
import org.springframework.security.crypto.key.CryptoKeySource;
|
||||
import org.springframework.security.crypto.key.StaticKeyGeneratingCryptoKeySource;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
||||
import org.springframework.security.oauth2.jose.JoseHeader;
|
||||
import org.springframework.security.oauth2.jose.jws.NimbusJwsEncoder;
|
||||
import org.springframework.security.oauth2.jose.TestJwks;
|
||||
import org.springframework.security.oauth2.jwt.JoseHeader;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwsEncoder;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
|
||||
@ -54,11 +62,6 @@ import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
@ -90,7 +93,7 @@ public class OAuth2AuthorizationCodeGrantTests {
|
||||
|
||||
private static RegisteredClientRepository registeredClientRepository;
|
||||
private static OAuth2AuthorizationService authorizationService;
|
||||
private static CryptoKeySource keySource;
|
||||
private static JWKSource<SecurityContext> jwkSource;
|
||||
private static NimbusJwsEncoder jwtEncoder;
|
||||
private static BiConsumer<JoseHeader.Builder, JwtClaimsSet.Builder> jwtCustomizer;
|
||||
|
||||
@ -104,8 +107,9 @@ public class OAuth2AuthorizationCodeGrantTests {
|
||||
public static void init() {
|
||||
registeredClientRepository = mock(RegisteredClientRepository.class);
|
||||
authorizationService = mock(OAuth2AuthorizationService.class);
|
||||
keySource = new StaticKeyGeneratingCryptoKeySource();
|
||||
jwtEncoder = new NimbusJwsEncoder(keySource);
|
||||
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
|
||||
jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
|
||||
jwtEncoder = new NimbusJwsEncoder(jwkSource);
|
||||
jwtCustomizer = mock(BiConsumer.class);
|
||||
jwtEncoder.setJwtCustomizer(jwtCustomizer);
|
||||
}
|
||||
@ -298,8 +302,8 @@ public class OAuth2AuthorizationCodeGrantTests {
|
||||
}
|
||||
|
||||
@Bean
|
||||
CryptoKeySource keySource() {
|
||||
return keySource;
|
||||
JWKSource<SecurityContext> jwkSource() {
|
||||
return jwkSource;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -15,10 +15,18 @@
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
@ -26,10 +34,9 @@ import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||
import org.springframework.security.config.test.SpringTestRule;
|
||||
import org.springframework.security.crypto.key.CryptoKeySource;
|
||||
import org.springframework.security.crypto.key.StaticKeyGeneratingCryptoKeySource;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.jose.TestJwks;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
@ -38,10 +45,6 @@ import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenE
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@ -61,7 +64,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
public class OAuth2ClientCredentialsGrantTests {
|
||||
private static RegisteredClientRepository registeredClientRepository;
|
||||
private static OAuth2AuthorizationService authorizationService;
|
||||
private static CryptoKeySource keySource;
|
||||
private static JWKSource<SecurityContext> jwkSource;
|
||||
|
||||
@Rule
|
||||
public final SpringTestRule spring = new SpringTestRule();
|
||||
@ -73,7 +76,8 @@ public class OAuth2ClientCredentialsGrantTests {
|
||||
public static void init() {
|
||||
registeredClientRepository = mock(RegisteredClientRepository.class);
|
||||
authorizationService = mock(OAuth2AuthorizationService.class);
|
||||
keySource = new StaticKeyGeneratingCryptoKeySource();
|
||||
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
|
||||
jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
|
||||
}
|
||||
|
||||
@Before
|
||||
@ -159,8 +163,8 @@ public class OAuth2ClientCredentialsGrantTests {
|
||||
}
|
||||
|
||||
@Bean
|
||||
CryptoKeySource keySource() {
|
||||
return keySource;
|
||||
JWKSource<SecurityContext> jwkSource() {
|
||||
return jwkSource;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -15,10 +15,18 @@
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
@ -26,10 +34,9 @@ import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||
import org.springframework.security.config.test.SpringTestRule;
|
||||
import org.springframework.security.crypto.key.CryptoKeySource;
|
||||
import org.springframework.security.crypto.key.StaticKeyGeneratingCryptoKeySource;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.jose.TestJwks;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
|
||||
@ -42,10 +49,6 @@ import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
@ -67,7 +70,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
public class OAuth2RefreshTokenGrantTests {
|
||||
private static RegisteredClientRepository registeredClientRepository;
|
||||
private static OAuth2AuthorizationService authorizationService;
|
||||
private static CryptoKeySource keySource;
|
||||
private static JWKSource<SecurityContext> jwkSource;
|
||||
|
||||
@Rule
|
||||
public final SpringTestRule spring = new SpringTestRule();
|
||||
@ -79,7 +82,8 @@ public class OAuth2RefreshTokenGrantTests {
|
||||
public static void init() {
|
||||
registeredClientRepository = mock(RegisteredClientRepository.class);
|
||||
authorizationService = mock(OAuth2AuthorizationService.class);
|
||||
keySource = new StaticKeyGeneratingCryptoKeySource();
|
||||
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
|
||||
jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
|
||||
}
|
||||
|
||||
@Before
|
||||
@ -153,8 +157,8 @@ public class OAuth2RefreshTokenGrantTests {
|
||||
}
|
||||
|
||||
@Bean
|
||||
CryptoKeySource keySource() {
|
||||
return keySource;
|
||||
JWKSource<SecurityContext> jwkSource() {
|
||||
return jwkSource;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -15,11 +15,19 @@
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
@ -27,12 +35,11 @@ import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||
import org.springframework.security.config.test.SpringTestRule;
|
||||
import org.springframework.security.crypto.key.CryptoKeySource;
|
||||
import org.springframework.security.crypto.key.StaticKeyGeneratingCryptoKeySource;
|
||||
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames2;
|
||||
import org.springframework.security.oauth2.jose.TestJwks;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
|
||||
@ -46,10 +53,6 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@ -66,7 +69,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
public class OAuth2TokenRevocationTests {
|
||||
private static RegisteredClientRepository registeredClientRepository;
|
||||
private static OAuth2AuthorizationService authorizationService;
|
||||
private static CryptoKeySource keySource;
|
||||
private static JWKSource<SecurityContext> jwkSource;
|
||||
|
||||
@Rule
|
||||
public final SpringTestRule spring = new SpringTestRule();
|
||||
@ -78,7 +81,8 @@ public class OAuth2TokenRevocationTests {
|
||||
public static void init() {
|
||||
registeredClientRepository = mock(RegisteredClientRepository.class);
|
||||
authorizationService = mock(OAuth2AuthorizationService.class);
|
||||
keySource = new StaticKeyGeneratingCryptoKeySource();
|
||||
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
|
||||
jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
|
||||
}
|
||||
|
||||
@Before
|
||||
@ -181,8 +185,8 @@ public class OAuth2TokenRevocationTests {
|
||||
}
|
||||
|
||||
@Bean
|
||||
CryptoKeySource keySource() {
|
||||
return keySource;
|
||||
JWKSource<SecurityContext> jwkSource() {
|
||||
return jwkSource;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -15,11 +15,19 @@
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
@ -27,12 +35,11 @@ import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||
import org.springframework.security.config.test.SpringTestRule;
|
||||
import org.springframework.security.crypto.key.CryptoKeySource;
|
||||
import org.springframework.security.crypto.key.StaticKeyGeneratingCryptoKeySource;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.jose.TestJwks;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.TokenType;
|
||||
@ -40,20 +47,16 @@ import org.springframework.security.oauth2.server.authorization.client.Registere
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
|
||||
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcProviderConfigurationEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2AuthorizationCode;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcProviderConfigurationEndpointFilter;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
@ -80,7 +83,7 @@ public class OidcTests {
|
||||
private static final String issuerUrl = "https://example.com/issuer1";
|
||||
private static RegisteredClientRepository registeredClientRepository;
|
||||
private static OAuth2AuthorizationService authorizationService;
|
||||
private static CryptoKeySource keySource;
|
||||
private static JWKSource<SecurityContext> jwkSource;
|
||||
|
||||
@Rule
|
||||
public final SpringTestRule spring = new SpringTestRule();
|
||||
@ -92,7 +95,8 @@ public class OidcTests {
|
||||
public static void init() {
|
||||
registeredClientRepository = mock(RegisteredClientRepository.class);
|
||||
authorizationService = mock(OAuth2AuthorizationService.class);
|
||||
keySource = new StaticKeyGeneratingCryptoKeySource();
|
||||
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
|
||||
jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
|
||||
}
|
||||
|
||||
@Before
|
||||
@ -224,8 +228,8 @@ public class OidcTests {
|
||||
}
|
||||
|
||||
@Bean
|
||||
CryptoKeySource keySource() {
|
||||
return keySource;
|
||||
JWKSource<SecurityContext> jwkSource() {
|
||||
return jwkSource;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,113 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.crypto.key;
|
||||
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.security.KeyPair;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.springframework.security.crypto.key.KeyGeneratorUtils.generateRsaKey;
|
||||
import static org.springframework.security.crypto.key.KeyGeneratorUtils.generateSecretKey;
|
||||
|
||||
/**
|
||||
* Tests for {@link CryptoKey}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
public class CryptoKeyTests {
|
||||
private static SecretKey secretKey;
|
||||
private static KeyPair rsaKeyPair;
|
||||
|
||||
@BeforeClass
|
||||
public static void init() {
|
||||
secretKey = generateSecretKey();
|
||||
rsaKeyPair = generateRsaKey();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void symmetricWhenSecretKeyNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> CryptoKey.symmetric(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("key cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void metadataWhenNameNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> CryptoKey.symmetric(secretKey).metadata(null, "value"))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("name cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void metadataWhenValueNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> CryptoKey.symmetric(secretKey).metadata("name", null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("value cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void symmetricWhenAllAttributesProvidedThenAllAttributesAreSet() {
|
||||
Map<String, Object> keyMetadata = new HashMap<>();
|
||||
keyMetadata.put("name1", "value1");
|
||||
keyMetadata.put("name2", "value2");
|
||||
|
||||
SymmetricKey symmetricKey = CryptoKey.symmetric(secretKey)
|
||||
.id("id")
|
||||
.metadata(metadata -> metadata.putAll(keyMetadata))
|
||||
.build();
|
||||
|
||||
assertThat(symmetricKey.getKey()).isEqualTo(secretKey);
|
||||
assertThat(symmetricKey.getId()).isEqualTo("id");
|
||||
assertThat(symmetricKey.getMetadata()).isEqualTo(keyMetadata);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void asymmetricWhenPrivateKeyNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> CryptoKey.asymmetric(null, rsaKeyPair.getPublic()))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("key cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void asymmetricWhenPublicKeyNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> CryptoKey.asymmetric(rsaKeyPair.getPrivate(), null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("publicKey cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void asymmetricWhenAllAttributesProvidedThenAllAttributesAreSet() {
|
||||
Map<String, Object> keyMetadata = new HashMap<>();
|
||||
keyMetadata.put("name1", "value1");
|
||||
keyMetadata.put("name2", "value2");
|
||||
|
||||
AsymmetricKey asymmetricKey = CryptoKey.asymmetric(rsaKeyPair.getPrivate(), rsaKeyPair.getPublic())
|
||||
.id("id")
|
||||
.metadata(metadata -> metadata.putAll(keyMetadata))
|
||||
.build();
|
||||
|
||||
assertThat(asymmetricKey.getKey()).isEqualTo(rsaKeyPair.getPrivate());
|
||||
assertThat(asymmetricKey.getPublicKey()).isEqualTo(rsaKeyPair.getPublic());
|
||||
assertThat(asymmetricKey.getId()).isEqualTo("id");
|
||||
assertThat(asymmetricKey.getMetadata()).isEqualTo(keyMetadata);
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
@ -1,177 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.jose.jws;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.security.crypto.key.AsymmetricKey;
|
||||
import org.springframework.security.crypto.key.CryptoKeySource;
|
||||
import org.springframework.security.crypto.key.TestCryptoKeys;
|
||||
import org.springframework.security.oauth2.jose.JoseHeader;
|
||||
import org.springframework.security.oauth2.jose.JoseHeaderNames;
|
||||
import org.springframework.security.oauth2.jose.TestJoseHeaders;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncodingException;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.TestJwtClaimsSets;
|
||||
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Tests for {@link NimbusJwsEncoder}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
public class NimbusJwsEncoderTests {
|
||||
private CryptoKeySource keySource;
|
||||
private NimbusJwsEncoder jwtEncoder;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
this.keySource = mock(CryptoKeySource.class);
|
||||
this.jwtEncoder = new NimbusJwsEncoder(this.keySource);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenKeySourceNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> new NimbusJwsEncoder(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("keySource cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setJwtCustomizerWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> this.jwtEncoder.setJwtCustomizer(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("jwtCustomizer cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenHeadersNullThenThrowIllegalArgumentException() {
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
assertThatThrownBy(() -> this.jwtEncoder.encode(null, jwtClaimsSet))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("headers cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenClaimsNullThenThrowIllegalArgumentException() {
|
||||
JoseHeader joseHeader = TestJoseHeaders.joseHeader().build();
|
||||
|
||||
assertThatThrownBy(() -> this.jwtEncoder.encode(joseHeader, null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("claims cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenUnsupportedKeyThenThrowJwtEncodingException() {
|
||||
JoseHeader joseHeader = TestJoseHeaders.joseHeader().build();
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
assertThatThrownBy(() -> this.jwtEncoder.encode(joseHeader, jwtClaimsSet))
|
||||
.isInstanceOf(JwtEncodingException.class)
|
||||
.hasMessageContaining("Unsupported key for algorithm 'RS256'");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenUnsupportedKeyAlgorithmThenThrowJwtEncodingException() {
|
||||
JoseHeader joseHeader = TestJoseHeaders.joseHeader(SignatureAlgorithm.ES256).build();
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
assertThatThrownBy(() -> this.jwtEncoder.encode(joseHeader, jwtClaimsSet))
|
||||
.isInstanceOf(JwtEncodingException.class)
|
||||
.hasMessageContaining("Unsupported key for algorithm 'ES256'");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenUnsupportedKeyTypeThenThrowJwtEncodingException() {
|
||||
AsymmetricKey ecKey = TestCryptoKeys.ecKey().build();
|
||||
when(this.keySource.getKeys()).thenReturn(Collections.singleton(ecKey));
|
||||
|
||||
JoseHeader joseHeader = TestJoseHeaders.joseHeader(SignatureAlgorithm.ES256).build();
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
assertThatThrownBy(() -> this.jwtEncoder.encode(joseHeader, jwtClaimsSet))
|
||||
.isInstanceOf(JwtEncodingException.class)
|
||||
.hasMessageContaining("Unsupported key type 'EC'");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenSuccessThenDecodes() {
|
||||
AsymmetricKey rsaKey = TestCryptoKeys.rsaKey().build();
|
||||
when(this.keySource.getKeys()).thenReturn(Collections.singleton(rsaKey));
|
||||
|
||||
JoseHeader joseHeader = TestJoseHeaders.joseHeader()
|
||||
.headers(headers -> headers.remove(JoseHeaderNames.CRIT))
|
||||
.build();
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
Jwt jws = this.jwtEncoder.encode(joseHeader, jwtClaimsSet);
|
||||
|
||||
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey((RSAPublicKey) rsaKey.getPublicKey()).build();
|
||||
jwtDecoder.decode(jws.getTokenValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenCustomizerSetThenCalled() {
|
||||
AsymmetricKey rsaKey = TestCryptoKeys.rsaKey().build();
|
||||
when(this.keySource.getKeys()).thenReturn(Collections.singleton(rsaKey));
|
||||
|
||||
JoseHeader joseHeader = TestJoseHeaders.joseHeader()
|
||||
.headers(headers -> headers.remove(JoseHeaderNames.CRIT))
|
||||
.build();
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
BiConsumer<JoseHeader.Builder, JwtClaimsSet.Builder> jwtCustomizer = mock(BiConsumer.class);
|
||||
this.jwtEncoder.setJwtCustomizer(jwtCustomizer);
|
||||
|
||||
this.jwtEncoder.encode(joseHeader, jwtClaimsSet);
|
||||
|
||||
verify(jwtCustomizer).accept(any(JoseHeader.Builder.class), any(JwtClaimsSet.Builder.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenMultipleActiveKeysThenUseFirst() {
|
||||
AsymmetricKey rsaKey1 = TestCryptoKeys.rsaKey().build();
|
||||
AsymmetricKey rsaKey2 = TestCryptoKeys.rsaKey().build();
|
||||
when(this.keySource.getKeys()).thenReturn(
|
||||
Stream.of(rsaKey1, rsaKey2)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new)));
|
||||
|
||||
JoseHeader joseHeader = TestJoseHeaders.joseHeader()
|
||||
.headers(headers -> headers.remove(JoseHeaderNames.CRIT))
|
||||
.build();
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
Jwt jws = this.jwtEncoder.encode(joseHeader, jwtClaimsSet);
|
||||
|
||||
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey((RSAPublicKey) rsaKey1.getPublicKey()).build();
|
||||
jwtDecoder.decode(jws.getTokenValue());
|
||||
}
|
||||
}
|
@ -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"))
|
@ -0,0 +1,291 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
import java.security.interfaces.ECPrivateKey;
|
||||
import java.security.interfaces.ECPublicKey;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import com.nimbusds.jose.KeySourceException;
|
||||
import com.nimbusds.jose.jwk.ECKey;
|
||||
import com.nimbusds.jose.jwk.JWK;
|
||||
import com.nimbusds.jose.jwk.JWKSelector;
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.KeyUse;
|
||||
import com.nimbusds.jose.jwk.OctetSequenceKey;
|
||||
import com.nimbusds.jose.jwk.RSAKey;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
|
||||
import org.springframework.security.oauth2.jose.TestJwks;
|
||||
import org.springframework.security.oauth2.jose.TestKeys;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.willAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link NimbusJwsEncoder}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
public class NimbusJwsEncoderTests {
|
||||
|
||||
private JWKSource<SecurityContext> jwkSource;
|
||||
|
||||
private NimbusJwsEncoder jwsEncoder;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
this.jwkSource = mock(JWKSource.class);
|
||||
this.jwsEncoder = new NimbusJwsEncoder(this.jwkSource);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenJwkSourceNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new NimbusJwsEncoder(null))
|
||||
.withMessage("jwkSource cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setJwtCustomizerWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.jwsEncoder.setJwtCustomizer(null))
|
||||
.withMessage("jwtCustomizer cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenHeadersNullThenThrowIllegalArgumentException() {
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.jwsEncoder.encode(null, jwtClaimsSet))
|
||||
.withMessage("headers cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenClaimsNullThenThrowIllegalArgumentException() {
|
||||
JoseHeader joseHeader = TestJoseHeaders.joseHeader().build();
|
||||
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.jwsEncoder.encode(joseHeader, null))
|
||||
.withMessage("claims cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenCustomizerSetThenCalled() throws Exception {
|
||||
RSAKey rsaJwk = TestJwks.DEFAULT_RSA_JWK;
|
||||
given(this.jwkSource.get(any(), any())).willReturn(Collections.singletonList(rsaJwk));
|
||||
|
||||
BiConsumer<JoseHeader.Builder, JwtClaimsSet.Builder> jwtCustomizer = mock(BiConsumer.class);
|
||||
this.jwsEncoder.setJwtCustomizer(jwtCustomizer);
|
||||
|
||||
JoseHeader joseHeader = TestJoseHeaders.joseHeader().build();
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
this.jwsEncoder.encode(joseHeader, jwtClaimsSet);
|
||||
|
||||
verify(jwtCustomizer).accept(any(JoseHeader.Builder.class), any(JwtClaimsSet.Builder.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenJwkSelectFailedThenThrowJwtEncodingException() throws Exception {
|
||||
given(this.jwkSource.get(any(), any())).willThrow(new KeySourceException("key source error"));
|
||||
|
||||
JoseHeader joseHeader = TestJoseHeaders.joseHeader().build();
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
assertThatExceptionOfType(JwtEncodingException.class)
|
||||
.isThrownBy(() -> this.jwsEncoder.encode(joseHeader, jwtClaimsSet))
|
||||
.withMessageContaining("Failed to select a JWK signing key -> key source error");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenJwkMultipleSelectedThenThrowJwtEncodingException() throws Exception {
|
||||
RSAKey rsaJwk = TestJwks.DEFAULT_RSA_JWK;
|
||||
given(this.jwkSource.get(any(), any())).willReturn(Arrays.asList(rsaJwk, rsaJwk));
|
||||
|
||||
JoseHeader joseHeader = TestJoseHeaders.joseHeader().build();
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
assertThatExceptionOfType(JwtEncodingException.class)
|
||||
.isThrownBy(() -> this.jwsEncoder.encode(joseHeader, jwtClaimsSet))
|
||||
.withMessageContaining("Found multiple JWK signing keys for algorithm 'RS256'");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenJwkSelectEmptyThenThrowJwtEncodingException() {
|
||||
JoseHeader joseHeader = TestJoseHeaders.joseHeader().build();
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
assertThatExceptionOfType(JwtEncodingException.class)
|
||||
.isThrownBy(() -> this.jwsEncoder.encode(joseHeader, jwtClaimsSet))
|
||||
.withMessageContaining("Failed to select a JWK signing key");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenJwkKidNullThenThrowJwtEncodingException() throws Exception {
|
||||
// @formatter:off
|
||||
RSAKey rsaJwk = TestJwks.jwk(TestKeys.DEFAULT_PUBLIC_KEY, TestKeys.DEFAULT_PRIVATE_KEY)
|
||||
.keyID(null)
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
given(this.jwkSource.get(any(), any())).willReturn(Collections.singletonList(rsaJwk));
|
||||
|
||||
JoseHeader joseHeader = TestJoseHeaders.joseHeader().build();
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
assertThatExceptionOfType(JwtEncodingException.class)
|
||||
.isThrownBy(() -> this.jwsEncoder.encode(joseHeader, jwtClaimsSet))
|
||||
.withMessageContaining("The \"kid\" (key ID) from the selected JWK cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenJwkUseEncryptionThenThrowJwtEncodingException() throws Exception {
|
||||
// @formatter:off
|
||||
RSAKey rsaJwk = TestJwks.jwk(TestKeys.DEFAULT_PUBLIC_KEY, TestKeys.DEFAULT_PRIVATE_KEY)
|
||||
.keyUse(KeyUse.ENCRYPTION)
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
given(this.jwkSource.get(any(), any())).willReturn(Collections.singletonList(rsaJwk));
|
||||
|
||||
JoseHeader joseHeader = TestJoseHeaders.joseHeader().build();
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
assertThatExceptionOfType(JwtEncodingException.class)
|
||||
.isThrownBy(() -> this.jwsEncoder.encode(joseHeader, jwtClaimsSet)).withMessageContaining(
|
||||
"Failed to create a JWS Signer -> The JWK use must be sig (signature) or unspecified");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenSuccessThenDecodes() throws Exception {
|
||||
RSAKey rsaJwk = TestJwks.DEFAULT_RSA_JWK;
|
||||
given(this.jwkSource.get(any(), any())).willReturn(Collections.singletonList(rsaJwk));
|
||||
|
||||
JoseHeader joseHeader = TestJoseHeaders.joseHeader().build();
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
Jwt encodedJws = this.jwsEncoder.encode(joseHeader, jwtClaimsSet);
|
||||
|
||||
// Assert headers/claims were added
|
||||
assertThat(encodedJws.getHeaders().get(JoseHeaderNames.TYP)).isEqualTo("JWT");
|
||||
assertThat(encodedJws.getHeaders().get(JoseHeaderNames.KID)).isEqualTo(rsaJwk.getKeyID());
|
||||
assertThat(encodedJws.getId()).isNotNull();
|
||||
|
||||
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(rsaJwk.toRSAPublicKey()).build();
|
||||
jwtDecoder.decode(encodedJws.getTokenValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenKeysRotatedThenNewKeyUsed() throws Exception {
|
||||
TestJWKSource jwkSource = new TestJWKSource();
|
||||
JWKSource<SecurityContext> jwkSourceDelegate = spy(new JWKSource<SecurityContext>() {
|
||||
@Override
|
||||
public List<JWK> get(JWKSelector jwkSelector, SecurityContext context) {
|
||||
return jwkSource.get(jwkSelector, context);
|
||||
}
|
||||
});
|
||||
NimbusJwsEncoder jwsEncoder = new NimbusJwsEncoder(jwkSourceDelegate);
|
||||
|
||||
JwkListResultCaptor jwkListResultCaptor = new JwkListResultCaptor();
|
||||
willAnswer(jwkListResultCaptor).given(jwkSourceDelegate).get(any(), any());
|
||||
|
||||
JoseHeader joseHeader = TestJoseHeaders.joseHeader().build();
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
Jwt encodedJws = jwsEncoder.encode(joseHeader, jwtClaimsSet);
|
||||
|
||||
JWK jwk1 = jwkListResultCaptor.getResult().get(0);
|
||||
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(((RSAKey) jwk1).toRSAPublicKey()).build();
|
||||
jwtDecoder.decode(encodedJws.getTokenValue());
|
||||
|
||||
jwkSource.rotate(); // Trigger key rotation
|
||||
|
||||
encodedJws = jwsEncoder.encode(joseHeader, jwtClaimsSet);
|
||||
|
||||
JWK jwk2 = jwkListResultCaptor.getResult().get(0);
|
||||
jwtDecoder = NimbusJwtDecoder.withPublicKey(((RSAKey) jwk2).toRSAPublicKey()).build();
|
||||
jwtDecoder.decode(encodedJws.getTokenValue());
|
||||
|
||||
assertThat(jwk1.getKeyID()).isNotEqualTo(jwk2.getKeyID());
|
||||
}
|
||||
|
||||
private static final class JwkListResultCaptor implements Answer<List<JWK>> {
|
||||
|
||||
private List<JWK> result;
|
||||
|
||||
private List<JWK> getResult() {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public List<JWK> answer(InvocationOnMock invocationOnMock) throws Throwable {
|
||||
this.result = (List<JWK>) invocationOnMock.callRealMethod();
|
||||
return this.result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class TestJWKSource implements JWKSource<SecurityContext> {
|
||||
|
||||
private int keyId = 1000;
|
||||
|
||||
private JWKSet jwkSet;
|
||||
|
||||
private TestJWKSource() {
|
||||
init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<JWK> get(JWKSelector jwkSelector, SecurityContext context) {
|
||||
return jwkSelector.select(this.jwkSet);
|
||||
}
|
||||
|
||||
private void init() {
|
||||
// @formatter:off
|
||||
RSAKey rsaJwk = TestJwks.jwk(TestKeys.DEFAULT_PUBLIC_KEY, TestKeys.DEFAULT_PRIVATE_KEY)
|
||||
.keyID("rsa-jwk-" + this.keyId++)
|
||||
.build();
|
||||
ECKey ecJwk = TestJwks.jwk((ECPublicKey) TestKeys.DEFAULT_EC_KEY_PAIR.getPublic(), (ECPrivateKey) TestKeys.DEFAULT_EC_KEY_PAIR.getPrivate())
|
||||
.keyID("ec-jwk-" + this.keyId++)
|
||||
.build();
|
||||
OctetSequenceKey secretJwk = TestJwks.jwk(TestKeys.DEFAULT_SECRET_KEY)
|
||||
.keyID("secret-jwk-" + this.keyId++)
|
||||
.build();
|
||||
// @formatter:on
|
||||
this.jwkSet = new JWKSet(Arrays.asList(rsaJwk, ecJwk, secretJwk));
|
||||
}
|
||||
|
||||
private void rotate() {
|
||||
init();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -13,38 +13,40 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.jose;
|
||||
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
public class TestJoseHeaders {
|
||||
public final class TestJoseHeaders {
|
||||
|
||||
private TestJoseHeaders() {
|
||||
}
|
||||
|
||||
public static JoseHeader.Builder joseHeader() {
|
||||
return joseHeader(SignatureAlgorithm.RS256);
|
||||
}
|
||||
|
||||
public static JoseHeader.Builder joseHeader(SignatureAlgorithm signatureAlgorithm) {
|
||||
// @formatter:off
|
||||
return JoseHeader.withAlgorithm(signatureAlgorithm)
|
||||
.jwkSetUri("https://provider.com/oauth2/jwks")
|
||||
.jwk(rsaJwk())
|
||||
.keyId(UUID.randomUUID().toString())
|
||||
.keyId("keyId")
|
||||
.x509Uri("https://provider.com/oauth2/x509")
|
||||
.x509CertificateChain(Arrays.asList("x509Cert1", "x509Cert2"))
|
||||
.x509SHA1Thumbprint("x509SHA1Thumbprint")
|
||||
.x509SHA256Thumbprint("x509SHA256Thumbprint")
|
||||
.critical(Collections.singleton("custom-header-name"))
|
||||
.type("JWT")
|
||||
.contentType("jwt-content-type")
|
||||
.header("custom-header-name", "custom-header-value");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private static Map<String, Object> rsaJwk() {
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -15,62 +15,61 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.web;
|
||||
|
||||
import com.nimbusds.jose.JWSAlgorithm;
|
||||
import com.nimbusds.jose.jwk.ECKey;
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.KeyUse;
|
||||
import com.nimbusds.jose.jwk.RSAKey;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.crypto.key.AsymmetricKey;
|
||||
import org.springframework.security.crypto.key.CryptoKeySource;
|
||||
import org.springframework.security.crypto.key.SymmetricKey;
|
||||
import org.springframework.security.crypto.key.TestCryptoKeys;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import com.nimbusds.jose.jwk.ECKey;
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.KeyUse;
|
||||
import com.nimbusds.jose.jwk.OctetSequenceKey;
|
||||
import com.nimbusds.jose.jwk.RSAKey;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.oauth2.jose.TestJwks;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Tests for {@link JwkSetEndpointFilter}.
|
||||
* Tests for {@link NimbusJwkSetEndpointFilter}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
public class JwkSetEndpointFilterTests {
|
||||
private CryptoKeySource keySource;
|
||||
private JwkSetEndpointFilter filter;
|
||||
public class NimbusJwkSetEndpointFilterTests {
|
||||
private JWKSource<SecurityContext> jwkSource;
|
||||
private NimbusJwkSetEndpointFilter filter;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
this.keySource = mock(CryptoKeySource.class);
|
||||
this.filter = new JwkSetEndpointFilter(this.keySource);
|
||||
this.jwkSource = mock(JWKSource.class);
|
||||
this.filter = new NimbusJwkSetEndpointFilter(this.jwkSource);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenKeySourceNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> new JwkSetEndpointFilter(null))
|
||||
public void constructorWhenJwkSourceNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> new NimbusJwkSetEndpointFilter(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("keySource cannot be null");
|
||||
.hasMessage("jwkSource cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenJwkSetEndpointUriNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> new JwkSetEndpointFilter(this.keySource, null))
|
||||
assertThatThrownBy(() -> new NimbusJwkSetEndpointFilter(this.jwkSource, null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("jwkSetEndpointUri cannot be empty");
|
||||
}
|
||||
@ -90,7 +89,7 @@ public class JwkSetEndpointFilterTests {
|
||||
|
||||
@Test
|
||||
public void doFilterWhenJwkSetRequestPostThenNotProcessed() throws Exception {
|
||||
String requestUri = JwkSetEndpointFilter.DEFAULT_JWK_SET_ENDPOINT_URI;
|
||||
String requestUri = NimbusJwkSetEndpointFilter.DEFAULT_JWK_SET_ENDPOINT_URI;
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
@ -103,12 +102,11 @@ public class JwkSetEndpointFilterTests {
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAsymmetricKeysThenJwkSetResponse() throws Exception {
|
||||
AsymmetricKey rsaKey = TestCryptoKeys.rsaKey().build();
|
||||
AsymmetricKey ecKey = TestCryptoKeys.ecKey().build();
|
||||
when(this.keySource.getKeys()).thenReturn(
|
||||
Stream.of(rsaKey, ecKey).collect(Collectors.toSet()));
|
||||
RSAKey rsaJwk = TestJwks.DEFAULT_RSA_JWK;
|
||||
ECKey ecJwk = TestJwks.DEFAULT_EC_JWK;
|
||||
given(this.jwkSource.get(any(), any())).willReturn(Arrays.asList(rsaJwk, ecJwk));
|
||||
|
||||
String requestUri = JwkSetEndpointFilter.DEFAULT_JWK_SET_ENDPOINT_URI;
|
||||
String requestUri = NimbusJwkSetEndpointFilter.DEFAULT_JWK_SET_ENDPOINT_URI;
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
@ -123,29 +121,25 @@ public class JwkSetEndpointFilterTests {
|
||||
JWKSet jwkSet = JWKSet.parse(response.getContentAsString());
|
||||
assertThat(jwkSet.getKeys()).hasSize(2);
|
||||
|
||||
RSAKey rsaJwk = (RSAKey) jwkSet.getKeyByKeyId(rsaKey.getId());
|
||||
assertThat(rsaJwk).isNotNull();
|
||||
assertThat(rsaJwk.toRSAPublicKey()).isEqualTo(rsaKey.getPublicKey());
|
||||
assertThat(rsaJwk.toRSAPrivateKey()).isNull();
|
||||
assertThat(rsaJwk.getKeyUse()).isEqualTo(KeyUse.SIGNATURE);
|
||||
assertThat(rsaJwk.getAlgorithm()).isEqualTo(JWSAlgorithm.RS256);
|
||||
RSAKey rsaJwkResult = (RSAKey) jwkSet.getKeyByKeyId(rsaJwk.getKeyID());
|
||||
assertThat(rsaJwkResult).isNotNull();
|
||||
assertThat(rsaJwkResult.toRSAPublicKey()).isEqualTo(rsaJwk.toRSAPublicKey());
|
||||
assertThat(rsaJwkResult.toRSAPrivateKey()).isNull();
|
||||
assertThat(rsaJwkResult.getKeyUse()).isEqualTo(KeyUse.SIGNATURE);
|
||||
|
||||
ECKey ecJwk = (ECKey) jwkSet.getKeyByKeyId(ecKey.getId());
|
||||
assertThat(ecJwk).isNotNull();
|
||||
assertThat(ecJwk.toECPublicKey()).isEqualTo(ecKey.getPublicKey());
|
||||
assertThat(ecJwk.toECPublicKey()).isEqualTo(ecKey.getPublicKey());
|
||||
assertThat(ecJwk.toECPrivateKey()).isNull();
|
||||
assertThat(ecJwk.getKeyUse()).isEqualTo(KeyUse.SIGNATURE);
|
||||
assertThat(ecJwk.getAlgorithm()).isEqualTo(JWSAlgorithm.ES256);
|
||||
ECKey ecJwkResult = (ECKey) jwkSet.getKeyByKeyId(ecJwk.getKeyID());
|
||||
assertThat(ecJwkResult).isNotNull();
|
||||
assertThat(ecJwkResult.toECPublicKey()).isEqualTo(ecJwk.toECPublicKey());
|
||||
assertThat(ecJwkResult.toECPrivateKey()).isNull();
|
||||
assertThat(ecJwkResult.getKeyUse()).isEqualTo(KeyUse.SIGNATURE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenSymmetricKeysThenJwkSetResponseEmpty() throws Exception {
|
||||
SymmetricKey secretKey = TestCryptoKeys.secretKey().build();
|
||||
when(this.keySource.getKeys()).thenReturn(
|
||||
new HashSet<>(Collections.singleton(secretKey)));
|
||||
OctetSequenceKey secretJwk = TestJwks.DEFAULT_SECRET_JWK;
|
||||
given(this.jwkSource.get(any(), any())).willReturn(Collections.singletonList(secretJwk));
|
||||
|
||||
String requestUri = JwkSetEndpointFilter.DEFAULT_JWK_SET_ENDPOINT_URI;
|
||||
String requestUri = NimbusJwkSetEndpointFilter.DEFAULT_JWK_SET_ENDPOINT_URI;
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -15,12 +15,18 @@
|
||||
*/
|
||||
package sample.config;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.RSAKey;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import sample.jose.Jwks;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||
import org.springframework.security.crypto.key.CryptoKeySource;
|
||||
import org.springframework.security.crypto.key.StaticKeyGeneratingCryptoKeySource;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
@ -29,8 +35,6 @@ import org.springframework.security.oauth2.server.authorization.client.Registere
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.1
|
||||
@ -61,8 +65,10 @@ public class AuthorizationServerConfig {
|
||||
// @formatter:on
|
||||
|
||||
@Bean
|
||||
public CryptoKeySource keySource() {
|
||||
return new StaticKeyGeneratingCryptoKeySource();
|
||||
public JWKSource<SecurityContext> jwkSource() {
|
||||
RSAKey rsaKey = Jwks.generateRsa();
|
||||
JWKSet jwkSet = new JWKSet(rsaKey);
|
||||
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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 {
|
Loading…
Reference in New Issue
Block a user