Consolidate to one module
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.config.annotation.web.configuration;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.WebSecurityConfigurer;
|
||||
import org.springframework.security.config.annotation.web.builders.WebSecurity;
|
||||
|
||||
/**
|
||||
* {@link Configuration} for OAuth 2.0 Authorization Server support.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.1
|
||||
*/
|
||||
@Configuration
|
||||
public class OAuth2AuthorizationServerConfiguration {
|
||||
|
||||
@Bean
|
||||
public WebSecurityConfigurer<WebSecurity> defaultOAuth2AuthorizationServerSecurity() {
|
||||
return new OAuth2AuthorizationServerSecurity();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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.config.annotation.web.configuration;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
|
||||
import static org.springframework.security.config.Customizer.withDefaults;
|
||||
|
||||
/**
|
||||
* {@link WebSecurityConfigurerAdapter} providing default security configuration for OAuth 2.0 Authorization Server.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.1
|
||||
*/
|
||||
public class OAuth2AuthorizationServerSecurity extends WebSecurityConfigurerAdapter {
|
||||
|
||||
// @formatter:off
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
|
||||
new OAuth2AuthorizationServerConfigurer<>();
|
||||
|
||||
http
|
||||
.requestMatcher(new OrRequestMatcher(authorizationServerConfigurer.getEndpointMatchers()))
|
||||
.authorizeRequests(authorizeRequests ->
|
||||
authorizeRequests
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin(withDefaults())
|
||||
.csrf(csrf -> csrf.ignoringRequestMatchers(tokenEndpointMatcher()))
|
||||
.apply(authorizationServerConfigurer);
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
private static RequestMatcher tokenEndpointMatcher() {
|
||||
return new AntPathRequestMatcher(
|
||||
OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI,
|
||||
HttpMethod.POST.name());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* 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.config.annotation.web.configurers.oauth2.server.authorization;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
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.keys.KeyManager;
|
||||
import org.springframework.security.oauth2.jose.jws.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;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.web.JwkSetEndpointFilter;
|
||||
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.web.access.intercept.FilterSecurityInterceptor;
|
||||
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
|
||||
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* An {@link AbstractHttpConfigurer} for OAuth 2.0 Authorization Server support.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.1
|
||||
* @see AbstractHttpConfigurer
|
||||
* @see RegisteredClientRepository
|
||||
* @see OAuth2AuthorizationService
|
||||
* @see OAuth2AuthorizationEndpointFilter
|
||||
* @see OAuth2TokenEndpointFilter
|
||||
* @see OAuth2ClientAuthenticationFilter
|
||||
*/
|
||||
public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
extends AbstractHttpConfigurer<OAuth2AuthorizationServerConfigurer<B>, B> {
|
||||
|
||||
private final RequestMatcher authorizationEndpointMatcher = new AntPathRequestMatcher(
|
||||
OAuth2AuthorizationEndpointFilter.DEFAULT_AUTHORIZATION_ENDPOINT_URI, HttpMethod.GET.name());
|
||||
private final RequestMatcher tokenEndpointMatcher = new AntPathRequestMatcher(
|
||||
OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI, HttpMethod.POST.name());
|
||||
private final RequestMatcher jwkSetEndpointMatcher = new AntPathRequestMatcher(
|
||||
JwkSetEndpointFilter.DEFAULT_JWK_SET_ENDPOINT_URI, HttpMethod.GET.name());
|
||||
|
||||
/**
|
||||
* Sets the repository of registered clients.
|
||||
*
|
||||
* @param registeredClientRepository the repository of registered clients
|
||||
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2AuthorizationServerConfigurer<B> registeredClientRepository(RegisteredClientRepository registeredClientRepository) {
|
||||
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
|
||||
this.getBuilder().setSharedObject(RegisteredClientRepository.class, registeredClientRepository);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the authorization service.
|
||||
*
|
||||
* @param authorizationService the authorization service
|
||||
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2AuthorizationServerConfigurer<B> authorizationService(OAuth2AuthorizationService authorizationService) {
|
||||
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||
this.getBuilder().setSharedObject(OAuth2AuthorizationService.class, authorizationService);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the key manager.
|
||||
*
|
||||
* @param keyManager the key manager
|
||||
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2AuthorizationServerConfigurer<B> keyManager(KeyManager keyManager) {
|
||||
Assert.notNull(keyManager, "keyManager cannot be null");
|
||||
this.getBuilder().setSharedObject(KeyManager.class, keyManager);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code List} of {@link RequestMatcher}'s for the authorization server endpoints.
|
||||
*
|
||||
* @return a {@code List} of {@link RequestMatcher}'s for the authorization server endpoints
|
||||
*/
|
||||
public List<RequestMatcher> getEndpointMatchers() {
|
||||
return Arrays.asList(this.authorizationEndpointMatcher,
|
||||
this.tokenEndpointMatcher, this.jwkSetEndpointMatcher);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(B builder) {
|
||||
OAuth2ClientAuthenticationProvider clientAuthenticationProvider =
|
||||
new OAuth2ClientAuthenticationProvider(
|
||||
getRegisteredClientRepository(builder));
|
||||
builder.authenticationProvider(postProcess(clientAuthenticationProvider));
|
||||
|
||||
NimbusJwsEncoder jwtEncoder = new NimbusJwsEncoder(getKeyManager(builder));
|
||||
|
||||
OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider =
|
||||
new OAuth2AuthorizationCodeAuthenticationProvider(
|
||||
getRegisteredClientRepository(builder),
|
||||
getAuthorizationService(builder),
|
||||
jwtEncoder);
|
||||
builder.authenticationProvider(postProcess(authorizationCodeAuthenticationProvider));
|
||||
|
||||
OAuth2ClientCredentialsAuthenticationProvider clientCredentialsAuthenticationProvider =
|
||||
new OAuth2ClientCredentialsAuthenticationProvider(
|
||||
getAuthorizationService(builder),
|
||||
jwtEncoder);
|
||||
builder.authenticationProvider(postProcess(clientCredentialsAuthenticationProvider));
|
||||
|
||||
ExceptionHandlingConfigurer<B> exceptionHandling = builder.getConfigurer(ExceptionHandlingConfigurer.class);
|
||||
if (exceptionHandling != null) {
|
||||
// Register the default AuthenticationEntryPoint for the token endpoint
|
||||
exceptionHandling.defaultAuthenticationEntryPointFor(
|
||||
new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), this.tokenEndpointMatcher);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(B builder) {
|
||||
JwkSetEndpointFilter jwkSetEndpointFilter = new JwkSetEndpointFilter(getKeyManager(builder));
|
||||
builder.addFilterBefore(postProcess(jwkSetEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
|
||||
|
||||
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
|
||||
|
||||
OAuth2ClientAuthenticationFilter clientAuthenticationFilter = new OAuth2ClientAuthenticationFilter(
|
||||
authenticationManager, this.tokenEndpointMatcher);
|
||||
builder.addFilterAfter(postProcess(clientAuthenticationFilter), AbstractPreAuthenticatedProcessingFilter.class);
|
||||
|
||||
OAuth2AuthorizationEndpointFilter authorizationEndpointFilter =
|
||||
new OAuth2AuthorizationEndpointFilter(
|
||||
getRegisteredClientRepository(builder),
|
||||
getAuthorizationService(builder));
|
||||
builder.addFilterBefore(postProcess(authorizationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
|
||||
|
||||
OAuth2TokenEndpointFilter tokenEndpointFilter =
|
||||
new OAuth2TokenEndpointFilter(
|
||||
authenticationManager,
|
||||
getAuthorizationService(builder));
|
||||
builder.addFilterAfter(postProcess(tokenEndpointFilter), FilterSecurityInterceptor.class);
|
||||
}
|
||||
|
||||
private static <B extends HttpSecurityBuilder<B>> RegisteredClientRepository getRegisteredClientRepository(B builder) {
|
||||
RegisteredClientRepository registeredClientRepository = builder.getSharedObject(RegisteredClientRepository.class);
|
||||
if (registeredClientRepository == null) {
|
||||
registeredClientRepository = getRegisteredClientRepositoryBean(builder);
|
||||
builder.setSharedObject(RegisteredClientRepository.class, registeredClientRepository);
|
||||
}
|
||||
return registeredClientRepository;
|
||||
}
|
||||
|
||||
private static <B extends HttpSecurityBuilder<B>> RegisteredClientRepository getRegisteredClientRepositoryBean(B builder) {
|
||||
return builder.getSharedObject(ApplicationContext.class).getBean(RegisteredClientRepository.class);
|
||||
}
|
||||
|
||||
private static <B extends HttpSecurityBuilder<B>> OAuth2AuthorizationService getAuthorizationService(B builder) {
|
||||
OAuth2AuthorizationService authorizationService = builder.getSharedObject(OAuth2AuthorizationService.class);
|
||||
if (authorizationService == null) {
|
||||
authorizationService = getAuthorizationServiceBean(builder);
|
||||
if (authorizationService == null) {
|
||||
authorizationService = new InMemoryOAuth2AuthorizationService();
|
||||
}
|
||||
builder.setSharedObject(OAuth2AuthorizationService.class, authorizationService);
|
||||
}
|
||||
return authorizationService;
|
||||
}
|
||||
|
||||
private static <B extends HttpSecurityBuilder<B>> OAuth2AuthorizationService getAuthorizationServiceBean(B builder) {
|
||||
Map<String, OAuth2AuthorizationService> authorizationServiceMap = BeanFactoryUtils.beansOfTypeIncludingAncestors(
|
||||
builder.getSharedObject(ApplicationContext.class), OAuth2AuthorizationService.class);
|
||||
if (authorizationServiceMap.size() > 1) {
|
||||
throw new NoUniqueBeanDefinitionException(OAuth2AuthorizationService.class, authorizationServiceMap.size(),
|
||||
"Expected single matching bean of type '" + OAuth2AuthorizationService.class.getName() + "' but found " +
|
||||
authorizationServiceMap.size() + ": " + StringUtils.collectionToCommaDelimitedString(authorizationServiceMap.keySet()));
|
||||
}
|
||||
return (!authorizationServiceMap.isEmpty() ? authorizationServiceMap.values().iterator().next() : null);
|
||||
}
|
||||
|
||||
private static <B extends HttpSecurityBuilder<B>> KeyManager getKeyManager(B builder) {
|
||||
KeyManager keyManager = builder.getSharedObject(KeyManager.class);
|
||||
if (keyManager == null) {
|
||||
keyManager = getKeyManagerBean(builder);
|
||||
builder.setSharedObject(KeyManager.class, keyManager);
|
||||
}
|
||||
return keyManager;
|
||||
}
|
||||
|
||||
private static <B extends HttpSecurityBuilder<B>> KeyManager getKeyManagerBean(B builder) {
|
||||
return builder.getSharedObject(ApplicationContext.class).getBean(KeyManager.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.core;
|
||||
|
||||
/**
|
||||
* Internal class used for serialization across Spring Security Authorization Server classes.
|
||||
*
|
||||
* @author Anoop Garlapati
|
||||
* @since 0.0.1
|
||||
*/
|
||||
public final class SpringSecurityCoreVersion2 {
|
||||
private static final int MAJOR = 0;
|
||||
private static final int MINOR = 0;
|
||||
private static final int PATCH = 1;
|
||||
|
||||
/**
|
||||
* Global Serialization value for Spring Security Authorization Server classes.
|
||||
*/
|
||||
public static final long SERIAL_VERSION_UID = getVersion().hashCode();
|
||||
|
||||
public static String getVersion() {
|
||||
return MAJOR + "." + MINOR + "." + PATCH;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.crypto.keys;
|
||||
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
import java.math.BigInteger;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.spec.ECFieldFp;
|
||||
import java.security.spec.ECParameterSpec;
|
||||
import java.security.spec.ECPoint;
|
||||
import java.security.spec.EllipticCurve;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.1
|
||||
*/
|
||||
final class KeyGeneratorUtils {
|
||||
|
||||
static SecretKey generateSecretKey() {
|
||||
SecretKey hmacKey;
|
||||
try {
|
||||
hmacKey = KeyGenerator.getInstance("HmacSha256").generateKey();
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
return hmacKey;
|
||||
}
|
||||
|
||||
static KeyPair generateRsaKey() {
|
||||
KeyPair keyPair;
|
||||
try {
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
|
||||
keyPairGenerator.initialize(2048);
|
||||
keyPair = keyPairGenerator.generateKeyPair();
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
return keyPair;
|
||||
}
|
||||
|
||||
static KeyPair generateEcKey() {
|
||||
EllipticCurve ellipticCurve = new EllipticCurve(
|
||||
new ECFieldFp(
|
||||
new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853951")),
|
||||
new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853948"),
|
||||
new BigInteger("41058363725152142129326129780047268409114441015993725554835256314039467401291"));
|
||||
ECPoint ecPoint = new ECPoint(
|
||||
new BigInteger("48439561293906451759052585252797914202762949526041747995844080717082404635286"),
|
||||
new BigInteger("36134250956749795798585127919587881956611106672985015071877198253568414405109"));
|
||||
ECParameterSpec ecParameterSpec = new ECParameterSpec(
|
||||
ellipticCurve,
|
||||
ecPoint,
|
||||
new BigInteger("115792089210356248762697446949407573529996955224135760342422259061068512044369"),
|
||||
1);
|
||||
|
||||
KeyPair keyPair;
|
||||
try {
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
|
||||
keyPairGenerator.initialize(ecParameterSpec);
|
||||
keyPair = keyPairGenerator.generateKeyPair();
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
return keyPair;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.crypto.keys;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Implementations of this interface are responsible for the management of {@link ManagedKey}(s),
|
||||
* e.g. {@code javax.crypto.SecretKey}, {@code java.security.PrivateKey}, {@code java.security.PublicKey}, etc.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.1
|
||||
* @see ManagedKey
|
||||
*/
|
||||
public interface KeyManager {
|
||||
|
||||
/**
|
||||
* Returns the {@link ManagedKey} identified by the provided {@code keyId},
|
||||
* or {@code null} if not found.
|
||||
*
|
||||
* @param keyId the key ID
|
||||
* @return the {@link ManagedKey}, or {@code null} if not found
|
||||
*/
|
||||
@Nullable
|
||||
ManagedKey findByKeyId(String keyId);
|
||||
|
||||
/**
|
||||
* Returns a {@code Set} of {@link ManagedKey}(s) having the provided key {@code algorithm},
|
||||
* or an empty {@code Set} if not found.
|
||||
*
|
||||
* @param algorithm the key algorithm
|
||||
* @return a {@code Set} of {@link ManagedKey}(s), or an empty {@code Set} if not found
|
||||
*/
|
||||
Set<ManagedKey> findByAlgorithm(String algorithm);
|
||||
|
||||
/**
|
||||
* Returns a {@code Set} of the {@link ManagedKey}(s).
|
||||
*
|
||||
* @return a {@code Set} of the {@link ManagedKey}(s)
|
||||
*/
|
||||
Set<ManagedKey> getKeys();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.crypto.keys;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.core.SpringSecurityCoreVersion2;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.io.Serializable;
|
||||
import java.security.Key;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A {@code java.security.Key} that is managed by a {@link KeyManager}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.1
|
||||
* @see KeyManager
|
||||
*/
|
||||
public final class ManagedKey implements Serializable {
|
||||
private static final long serialVersionUID = SpringSecurityCoreVersion2.SERIAL_VERSION_UID;
|
||||
private Key key;
|
||||
private PublicKey publicKey;
|
||||
private String keyId;
|
||||
private Instant activatedOn;
|
||||
private Instant deactivatedOn;
|
||||
|
||||
private ManagedKey() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if this is a symmetric key, {@code false} otherwise.
|
||||
*
|
||||
* @return {@code true} if this is a symmetric key, {@code false} otherwise
|
||||
*/
|
||||
public boolean isSymmetric() {
|
||||
return SecretKey.class.isAssignableFrom(this.key.getClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if this is a asymmetric key, {@code false} otherwise.
|
||||
*
|
||||
* @return {@code true} if this is a asymmetric key, {@code false} otherwise
|
||||
*/
|
||||
public boolean isAsymmetric() {
|
||||
return PrivateKey.class.isAssignableFrom(this.key.getClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a type of {@code java.security.Key},
|
||||
* e.g. {@code javax.crypto.SecretKey} or {@code java.security.PrivateKey}.
|
||||
*
|
||||
* @param <T> the type of {@code java.security.Key}
|
||||
* @return the type of {@code java.security.Key}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends Key> T getKey() {
|
||||
return (T) this.key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@code java.security.PublicKey} if this is a asymmetric key, {@code null} otherwise.
|
||||
*
|
||||
* @return the {@code java.security.PublicKey} if this is a asymmetric key, {@code null} otherwise
|
||||
*/
|
||||
@Nullable
|
||||
public PublicKey getPublicKey() {
|
||||
return this.publicKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the key ID.
|
||||
*
|
||||
* @return the key ID
|
||||
*/
|
||||
public String getKeyId() {
|
||||
return this.keyId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the time when this key was activated.
|
||||
*
|
||||
* @return the time when this key was activated
|
||||
*/
|
||||
public Instant getActivatedOn() {
|
||||
return this.activatedOn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the time when this key was deactivated, {@code null} if still active.
|
||||
*
|
||||
* @return the time when this key was deactivated, {@code null} if still active
|
||||
*/
|
||||
@Nullable
|
||||
public Instant getDeactivatedOn() {
|
||||
return this.deactivatedOn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if this key is active, {@code false} otherwise.
|
||||
*
|
||||
* @return {@code true} if this key is active, {@code false} otherwise
|
||||
*/
|
||||
public boolean isActive() {
|
||||
return getDeactivatedOn() == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the key algorithm.
|
||||
*
|
||||
* @return the key algorithm
|
||||
*/
|
||||
public String getAlgorithm() {
|
||||
return this.key.getAlgorithm();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
ManagedKey that = (ManagedKey) obj;
|
||||
return Objects.equals(this.keyId, that.keyId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(this.keyId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Builder}, initialized with the provided {@code javax.crypto.SecretKey}.
|
||||
*
|
||||
* @param secretKey the {@code javax.crypto.SecretKey}
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder withSymmetricKey(SecretKey secretKey) {
|
||||
return new Builder(secretKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Builder}, initialized with the provided
|
||||
* {@code java.security.PublicKey} and {@code java.security.PrivateKey}.
|
||||
*
|
||||
* @param publicKey the {@code java.security.PublicKey}
|
||||
* @param privateKey the {@code java.security.PrivateKey}
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder withAsymmetricKey(PublicKey publicKey, PrivateKey privateKey) {
|
||||
return new Builder(publicKey, privateKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link ManagedKey}.
|
||||
*/
|
||||
public static class Builder {
|
||||
private Key key;
|
||||
private PublicKey publicKey;
|
||||
private String keyId;
|
||||
private Instant activatedOn;
|
||||
private Instant deactivatedOn;
|
||||
|
||||
private Builder(SecretKey secretKey) {
|
||||
Assert.notNull(secretKey, "secretKey cannot be null");
|
||||
this.key = secretKey;
|
||||
}
|
||||
|
||||
private Builder(PublicKey publicKey, PrivateKey privateKey) {
|
||||
Assert.notNull(publicKey, "publicKey cannot be null");
|
||||
Assert.notNull(privateKey, "privateKey cannot be null");
|
||||
this.key = privateKey;
|
||||
this.publicKey = publicKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the key ID.
|
||||
*
|
||||
* @param keyId the key ID
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder keyId(String keyId) {
|
||||
this.keyId = keyId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the time when this key was activated.
|
||||
*
|
||||
* @param activatedOn the time when this key was activated
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder activatedOn(Instant activatedOn) {
|
||||
this.activatedOn = activatedOn;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the time when this key was deactivated.
|
||||
*
|
||||
* @param deactivatedOn the time when this key was deactivated
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder deactivatedOn(Instant deactivatedOn) {
|
||||
this.deactivatedOn = deactivatedOn;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new {@link ManagedKey}.
|
||||
*
|
||||
* @return a {@link ManagedKey}
|
||||
*/
|
||||
public ManagedKey build() {
|
||||
Assert.hasText(this.keyId, "keyId cannot be empty");
|
||||
Assert.notNull(this.activatedOn, "activatedOn cannot be null");
|
||||
|
||||
ManagedKey managedKey = new ManagedKey();
|
||||
managedKey.key = this.key;
|
||||
managedKey.publicKey = this.publicKey;
|
||||
managedKey.keyId = this.keyId;
|
||||
managedKey.activatedOn = this.activatedOn;
|
||||
managedKey.deactivatedOn = this.deactivatedOn;
|
||||
return managedKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.crypto.keys;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.security.KeyPair;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.springframework.security.crypto.keys.KeyGeneratorUtils.generateRsaKey;
|
||||
import static org.springframework.security.crypto.keys.KeyGeneratorUtils.generateSecretKey;
|
||||
|
||||
/**
|
||||
* An implementation of a {@link KeyManager} that generates the {@link ManagedKey}(s) when constructed.
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> This implementation should ONLY be used during development/testing.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.1
|
||||
* @see KeyManager
|
||||
*/
|
||||
public final class StaticKeyGeneratingKeyManager implements KeyManager {
|
||||
private final Map<String, ManagedKey> keys;
|
||||
|
||||
public StaticKeyGeneratingKeyManager() {
|
||||
this.keys = Collections.unmodifiableMap(new HashMap<>(generateKeys()));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public ManagedKey findByKeyId(String keyId) {
|
||||
Assert.hasText(keyId, "keyId cannot be empty");
|
||||
return this.keys.get(keyId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ManagedKey> findByAlgorithm(String algorithm) {
|
||||
Assert.hasText(algorithm, "algorithm cannot be empty");
|
||||
return this.keys.values().stream()
|
||||
.filter(managedKey -> managedKey.getAlgorithm().equals(algorithm))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ManagedKey> getKeys() {
|
||||
return new HashSet<>(this.keys.values());
|
||||
}
|
||||
|
||||
private static Map<String, ManagedKey> generateKeys() {
|
||||
KeyPair rsaKeyPair = generateRsaKey();
|
||||
ManagedKey rsaManagedKey = ManagedKey.withAsymmetricKey(rsaKeyPair.getPublic(), rsaKeyPair.getPrivate())
|
||||
.keyId(UUID.randomUUID().toString())
|
||||
.activatedOn(Instant.now())
|
||||
.build();
|
||||
|
||||
SecretKey hmacKey = generateSecretKey();
|
||||
ManagedKey secretManagedKey = ManagedKey.withSymmetricKey(hmacKey)
|
||||
.keyId(UUID.randomUUID().toString())
|
||||
.activatedOn(Instant.now())
|
||||
.build();
|
||||
|
||||
return Stream.of(rsaManagedKey, secretManagedKey)
|
||||
.collect(Collectors.toMap(ManagedKey::getKeyId, v -> v));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.jose;
|
||||
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.springframework.security.oauth2.jose.JoseHeaderNames.ALG;
|
||||
import static org.springframework.security.oauth2.jose.JoseHeaderNames.CRIT;
|
||||
import static org.springframework.security.oauth2.jose.JoseHeaderNames.CTY;
|
||||
import static org.springframework.security.oauth2.jose.JoseHeaderNames.JKU;
|
||||
import static org.springframework.security.oauth2.jose.JoseHeaderNames.JWK;
|
||||
import static org.springframework.security.oauth2.jose.JoseHeaderNames.KID;
|
||||
import static org.springframework.security.oauth2.jose.JoseHeaderNames.TYP;
|
||||
import static org.springframework.security.oauth2.jose.JoseHeaderNames.X5C;
|
||||
import static org.springframework.security.oauth2.jose.JoseHeaderNames.X5T;
|
||||
import static org.springframework.security.oauth2.jose.JoseHeaderNames.X5T_S256;
|
||||
import static org.springframework.security.oauth2.jose.JoseHeaderNames.X5U;
|
||||
|
||||
/**
|
||||
* The JOSE header is a JSON object representing the header parameters of a JSON Web Token,
|
||||
* whether the JWT is a JWS or JWE, that describe the cryptographic operations applied to the JWT
|
||||
* and optionally, additional properties of the JWT.
|
||||
*
|
||||
* @author Anoop Garlapati
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.1
|
||||
* @see Jwt
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519#section-5">JWT JOSE Header</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515#section-4">JWS JOSE Header</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7516#section-4">JWE JOSE Header</a>
|
||||
*/
|
||||
public final class JoseHeader {
|
||||
private final Map<String, Object> headers;
|
||||
|
||||
private JoseHeader(Map<String, Object> headers) {
|
||||
this.headers = Collections.unmodifiableMap(new LinkedHashMap<>(headers));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JWS algorithm used to digitally sign the JWS.
|
||||
*
|
||||
* @return the JWS algorithm
|
||||
*/
|
||||
public JwsAlgorithm getJwsAlgorithm() {
|
||||
return getHeader(ALG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JWK Set URL that refers to the resource of a set of JSON-encoded public keys,
|
||||
* one of which corresponds to the key used to digitally sign the JWS or encrypt the JWE.
|
||||
*
|
||||
* @return the JWK Set URL
|
||||
*/
|
||||
public String getJwkSetUri() {
|
||||
return getHeader(JKU);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON Web Key which is the public key that corresponds to the key
|
||||
* used to digitally sign the JWS or encrypt the JWE.
|
||||
*
|
||||
* @return the JSON Web Key
|
||||
*/
|
||||
public Map<String, Object> getJwk() {
|
||||
return getHeader(JWK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the key ID that is a hint indicating which key was used to secure the JWS or JWE.
|
||||
*
|
||||
* @return the key ID
|
||||
*/
|
||||
public String getKeyId() {
|
||||
return getHeader(KID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the X.509 URL that refers to the resource for the X.509 public key certificate
|
||||
* or certificate chain corresponding to the key used to digitally sign the JWS or encrypt the JWE.
|
||||
*
|
||||
* @return the X.509 URL
|
||||
*/
|
||||
public String getX509Uri() {
|
||||
return getHeader(X5U);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the X.509 certificate chain that contains the X.509 public key certificate
|
||||
* or certificate chain corresponding to the key used to digitally sign the JWS or encrypt the JWE.
|
||||
*
|
||||
* @return the X.509 certificate chain
|
||||
*/
|
||||
public List<String> getX509CertificateChain() {
|
||||
return getHeader(X5C);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the X.509 certificate SHA-1 thumbprint that is a base64url-encoded SHA-1 thumbprint (a.k.a. digest)
|
||||
* of the DER encoding of the X.509 certificate corresponding to the key used to digitally sign the JWS or encrypt the JWE.
|
||||
*
|
||||
* @return the X.509 certificate SHA-1 thumbprint
|
||||
*/
|
||||
public String getX509SHA1Thumbprint() {
|
||||
return getHeader(X5T);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the X.509 certificate SHA-256 thumbprint that is a base64url-encoded SHA-256 thumbprint (a.k.a. digest)
|
||||
* of the DER encoding of the X.509 certificate corresponding to the key used to digitally sign the JWS or encrypt the JWE.
|
||||
*
|
||||
* @return the X.509 certificate SHA-256 thumbprint
|
||||
*/
|
||||
public String getX509SHA256Thumbprint() {
|
||||
return getHeader(X5T_S256);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the critical headers that indicates which extensions to the JWS/JWE/JWA specifications
|
||||
* are being used that MUST be understood and processed.
|
||||
*
|
||||
* @return the critical headers
|
||||
*/
|
||||
public Set<String> getCritical() {
|
||||
return getHeader(CRIT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type header that declares the media type of the JWS/JWE.
|
||||
*
|
||||
* @return the type header
|
||||
*/
|
||||
public String getType() {
|
||||
return getHeader(TYP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content type header that declares the media type of the secured content (the payload).
|
||||
*
|
||||
* @return the content type header
|
||||
*/
|
||||
public String getContentType() {
|
||||
return getHeader(CTY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the headers.
|
||||
*
|
||||
* @return the headers
|
||||
*/
|
||||
public Map<String, Object> getHeaders() {
|
||||
return this.headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the header value.
|
||||
*
|
||||
* @param name the header name
|
||||
* @param <T> the type of the header value
|
||||
* @return the header value
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T getHeader(String name) {
|
||||
Assert.hasText(name, "name cannot be empty");
|
||||
return (T) getHeaders().get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Builder}, initialized with the provided {@link JwsAlgorithm}.
|
||||
*
|
||||
* @param jwsAlgorithm the {@link JwsAlgorithm}
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder withAlgorithm(JwsAlgorithm jwsAlgorithm) {
|
||||
return new Builder(jwsAlgorithm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Builder}, initialized with the provided {@code headers}.
|
||||
*
|
||||
* @param headers the headers
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder from(JoseHeader headers) {
|
||||
return new Builder(headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link JoseHeader}.
|
||||
*/
|
||||
public static class Builder {
|
||||
private final Map<String, Object> headers = new LinkedHashMap<>();
|
||||
|
||||
private Builder(JwsAlgorithm jwsAlgorithm) {
|
||||
Assert.notNull(jwsAlgorithm, "jwsAlgorithm cannot be null");
|
||||
header(ALG, jwsAlgorithm);
|
||||
}
|
||||
|
||||
private Builder(JoseHeader headers) {
|
||||
Assert.notNull(headers, "headers cannot be null");
|
||||
this.headers.putAll(headers.getHeaders());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the JWK Set URL that refers to the resource of a set of JSON-encoded public keys,
|
||||
* one of which corresponds to the key used to digitally sign the JWS or encrypt the JWE.
|
||||
*
|
||||
* @param jwkSetUri the JWK Set URL
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder jwkSetUri(String jwkSetUri) {
|
||||
return header(JKU, jwkSetUri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the JSON Web Key which is the public key that corresponds to the key
|
||||
* used to digitally sign the JWS or encrypt the JWE.
|
||||
*
|
||||
* @param jwk the JSON Web Key
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder jwk(Map<String, Object> jwk) {
|
||||
return header(JWK, jwk);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the key ID that is a hint indicating which key was used to secure the JWS or JWE.
|
||||
*
|
||||
* @param keyId the key ID
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder keyId(String keyId) {
|
||||
return header(KID, keyId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the X.509 URL that refers to the resource for the X.509 public key certificate
|
||||
* or certificate chain corresponding to the key used to digitally sign the JWS or encrypt the JWE.
|
||||
*
|
||||
* @param x509Uri the X.509 URL
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder x509Uri(String x509Uri) {
|
||||
return header(X5U, x509Uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the X.509 certificate chain that contains the X.509 public key certificate
|
||||
* or certificate chain corresponding to the key used to digitally sign the JWS or encrypt the JWE.
|
||||
*
|
||||
* @param x509CertificateChain the X.509 certificate chain
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder x509CertificateChain(List<String> x509CertificateChain) {
|
||||
return header(X5C, x509CertificateChain);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the X.509 certificate SHA-1 thumbprint that is a base64url-encoded SHA-1 thumbprint (a.k.a. digest)
|
||||
* of the DER encoding of the X.509 certificate corresponding to the key used to digitally sign the JWS or encrypt the JWE.
|
||||
*
|
||||
* @param x509SHA1Thumbprint the X.509 certificate SHA-1 thumbprint
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder x509SHA1Thumbprint(String x509SHA1Thumbprint) {
|
||||
return header(X5T, x509SHA1Thumbprint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the X.509 certificate SHA-256 thumbprint that is a base64url-encoded SHA-256 thumbprint (a.k.a. digest)
|
||||
* of the DER encoding of the X.509 certificate corresponding to the key used to digitally sign the JWS or encrypt the JWE.
|
||||
*
|
||||
* @param x509SHA256Thumbprint the X.509 certificate SHA-256 thumbprint
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder x509SHA256Thumbprint(String x509SHA256Thumbprint) {
|
||||
return header(X5T_S256, x509SHA256Thumbprint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the critical headers that indicates which extensions to the JWS/JWE/JWA specifications
|
||||
* are being used that MUST be understood and processed.
|
||||
*
|
||||
* @param headerNames the critical header names
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder critical(Set<String> headerNames) {
|
||||
return header(CRIT, headerNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the type header that declares the media type of the JWS/JWE.
|
||||
*
|
||||
* @param type the type header
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder type(String type) {
|
||||
return header(TYP, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the content type header that declares the media type of the secured content (the payload).
|
||||
*
|
||||
* @param contentType the content type header
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder contentType(String contentType) {
|
||||
return header(CTY, contentType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the header.
|
||||
*
|
||||
* @param name the header name
|
||||
* @param value the header value
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder header(String name, Object value) {
|
||||
Assert.hasText(name, "name cannot be empty");
|
||||
Assert.notNull(value, "value cannot be null");
|
||||
this.headers.put(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@code Consumer} to be provided access to the headers
|
||||
* allowing the ability to add, replace, or remove.
|
||||
*
|
||||
* @param headersConsumer a {@code Consumer} of the headers
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder headers(Consumer<Map<String, Object>> headersConsumer) {
|
||||
headersConsumer.accept(this.headers);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new {@link JoseHeader}.
|
||||
*
|
||||
* @return a {@link JoseHeader}
|
||||
*/
|
||||
public JoseHeader build() {
|
||||
Assert.notEmpty(this.headers, "headers cannot be empty");
|
||||
return new JoseHeader(this.headers);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.jose;
|
||||
|
||||
/**
|
||||
* The Registered Header Parameter Names defined by the JSON Web Token (JWT),
|
||||
* JSON Web Signature (JWS) and JSON Web Encryption (JWE) specifications
|
||||
* that may be contained in the JOSE Header of a JWT.
|
||||
*
|
||||
* @author Anoop Garlapati
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.1
|
||||
* @see JoseHeader
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519#section-5">JWT JOSE Header</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515#section-4">JWS JOSE Header</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7516#section-4">JWE JOSE Header</a>
|
||||
*/
|
||||
public interface JoseHeaderNames {
|
||||
|
||||
/**
|
||||
* {@code alg} - the algorithm header identifies the cryptographic algorithm used to secure a JWS or JWE
|
||||
*/
|
||||
String ALG = "alg";
|
||||
|
||||
/**
|
||||
* {@code jku} - the JWK Set URL header is a URI that refers to a resource for a set of JSON-encoded public keys,
|
||||
* one of which corresponds to the key used to digitally sign a JWS or encrypt a JWE
|
||||
*/
|
||||
String JKU = "jku";
|
||||
|
||||
/**
|
||||
* {@code jwk} - the JSON Web Key header is the public key that corresponds to the key
|
||||
* used to digitally sign a JWS or encrypt a JWE
|
||||
*/
|
||||
String JWK = "jwk";
|
||||
|
||||
/**
|
||||
* {@code kid} - the key ID header is a hint indicating which key was used to secure a JWS or JWE
|
||||
*/
|
||||
String KID = "kid";
|
||||
|
||||
/**
|
||||
* {@code x5u} - the X.509 URL header is a URI that refers to a resource for the X.509 public key certificate
|
||||
* or certificate chain corresponding to the key used to digitally sign a JWS or encrypt a JWE
|
||||
*/
|
||||
String X5U = "x5u";
|
||||
|
||||
/**
|
||||
* {@code x5c} - the X.509 certificate chain header contains the X.509 public key certificate
|
||||
* or certificate chain corresponding to the key used to digitally sign a JWS or encrypt a JWE
|
||||
*/
|
||||
String X5C = "x5c";
|
||||
|
||||
/**
|
||||
* {@code x5t} - the X.509 certificate SHA-1 thumbprint header is a base64url-encoded SHA-1 thumbprint (a.k.a. digest)
|
||||
* of the DER encoding of the X.509 certificate corresponding to the key used to digitally sign a JWS or encrypt a JWE
|
||||
*/
|
||||
String X5T = "x5t";
|
||||
|
||||
/**
|
||||
* {@code x5t#S256} - the X.509 certificate SHA-256 thumbprint header is a base64url-encoded SHA-256 thumbprint (a.k.a. digest)
|
||||
* of the DER encoding of the X.509 certificate corresponding to the key used to digitally sign a JWS or encrypt a JWE
|
||||
*/
|
||||
String X5T_S256 = "x5t#S256";
|
||||
|
||||
/**
|
||||
* {@code typ} - the type header is used by JWS/JWE applications to declare the media type of a JWS/JWE
|
||||
*/
|
||||
String TYP = "typ";
|
||||
|
||||
/**
|
||||
* {@code cty} - the content type header is used by JWS/JWE applications to declare the media type
|
||||
* of the secured content (the payload)
|
||||
*/
|
||||
String CTY = "cty";
|
||||
|
||||
/**
|
||||
* {@code crit} - the critical header indicates that extensions to the JWS/JWE/JWA specifications
|
||||
* are being used that MUST be understood and processed
|
||||
*/
|
||||
String CRIT = "crit";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.jose.jws;
|
||||
|
||||
import com.nimbusds.jose.JOSEException;
|
||||
import com.nimbusds.jose.JOSEObjectType;
|
||||
import com.nimbusds.jose.JWSAlgorithm;
|
||||
import com.nimbusds.jose.JWSHeader;
|
||||
import com.nimbusds.jose.JWSSigner;
|
||||
import com.nimbusds.jose.KeyLengthException;
|
||||
import com.nimbusds.jose.crypto.MACSigner;
|
||||
import com.nimbusds.jose.crypto.RSASSASigner;
|
||||
import com.nimbusds.jose.jwk.JWK;
|
||||
import com.nimbusds.jose.util.Base64;
|
||||
import com.nimbusds.jose.util.Base64URL;
|
||||
import com.nimbusds.jwt.JWTClaimsSet;
|
||||
import com.nimbusds.jwt.SignedJWT;
|
||||
import net.minidev.json.JSONObject;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.crypto.keys.KeyManager;
|
||||
import org.springframework.security.crypto.keys.ManagedKey;
|
||||
import org.springframework.security.oauth2.jose.JoseHeader;
|
||||
import org.springframework.security.oauth2.jose.JoseHeaderNames;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncodingException;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.security.PrivateKey;
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* An implementation of a {@link JwtEncoder} that encodes a JSON Web Token (JWT)
|
||||
* using the JSON Web Signature (JWS) Compact Serialization format.
|
||||
* The private/secret key used for signing the JWS is obtained
|
||||
* from the {@link KeyManager} supplied via the constructor.
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> This implementation uses the Nimbus JOSE + JWT SDK.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.1
|
||||
* @see JwtEncoder
|
||||
* @see KeyManager
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519">JSON Web Token (JWT)</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515">JSON Web Signature (JWS)</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515#section-3.1">JWS Compact Serialization</a>
|
||||
* @see <a target="_blank" href="https://connect2id.com/products/nimbus-jose-jwt">Nimbus JOSE + JWT SDK</a>
|
||||
*/
|
||||
public final class NimbusJwsEncoder implements JwtEncoder {
|
||||
private static final String ENCODING_ERROR_MESSAGE_TEMPLATE =
|
||||
"An error occurred while attempting to encode the Jwt: %s";
|
||||
private static final String RSA_KEY_TYPE = "RSA";
|
||||
private static final String EC_KEY_TYPE = "EC";
|
||||
private static final Map<JwsAlgorithm, String> jcaKeyAlgorithmMappings = new HashMap<JwsAlgorithm, String>() {
|
||||
{
|
||||
put(MacAlgorithm.HS256, "HmacSHA256");
|
||||
put(MacAlgorithm.HS384, "HmacSHA384");
|
||||
put(MacAlgorithm.HS512, "HmacSHA512");
|
||||
put(SignatureAlgorithm.RS256, RSA_KEY_TYPE);
|
||||
put(SignatureAlgorithm.RS384, RSA_KEY_TYPE);
|
||||
put(SignatureAlgorithm.RS512, RSA_KEY_TYPE);
|
||||
put(SignatureAlgorithm.ES256, EC_KEY_TYPE);
|
||||
put(SignatureAlgorithm.ES384, EC_KEY_TYPE);
|
||||
put(SignatureAlgorithm.ES512, EC_KEY_TYPE);
|
||||
}
|
||||
};
|
||||
private static final Converter<JoseHeader, JWSHeader> jwsHeaderConverter = new JwsHeaderConverter();
|
||||
private static final Converter<JwtClaimsSet, JWTClaimsSet> jwtClaimsSetConverter = new JwtClaimsSetConverter();
|
||||
private final KeyManager keyManager;
|
||||
|
||||
/**
|
||||
* Constructs a {@code NimbusJwsEncoder} using the provided parameters.
|
||||
*
|
||||
* @param keyManager the key manager
|
||||
*/
|
||||
public NimbusJwsEncoder(KeyManager keyManager) {
|
||||
Assert.notNull(keyManager, "keyManager cannot be null");
|
||||
this.keyManager = keyManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Jwt encode(JoseHeader headers, JwtClaimsSet claims) throws JwtEncodingException {
|
||||
Assert.notNull(headers, "headers cannot be null");
|
||||
Assert.notNull(claims, "claims cannot be null");
|
||||
|
||||
ManagedKey managedKey = selectKey(headers);
|
||||
if (managedKey == null) {
|
||||
throw new JwtEncodingException(String.format(
|
||||
ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Unsupported key for algorithm '" + headers.getJwsAlgorithm().getName() + "'"));
|
||||
}
|
||||
|
||||
JWSSigner jwsSigner;
|
||||
if (managedKey.isAsymmetric()) {
|
||||
if (!managedKey.getAlgorithm().equals(RSA_KEY_TYPE)) {
|
||||
throw new JwtEncodingException(String.format(
|
||||
ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Unsupported key type '" + managedKey.getAlgorithm() + "'"));
|
||||
}
|
||||
PrivateKey privateKey = managedKey.getKey();
|
||||
jwsSigner = new RSASSASigner(privateKey);
|
||||
} else {
|
||||
SecretKey secretKey = managedKey.getKey();
|
||||
try {
|
||||
jwsSigner = new MACSigner(secretKey);
|
||||
} catch (KeyLengthException ex) {
|
||||
throw new JwtEncodingException(String.format(
|
||||
ENCODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex);
|
||||
}
|
||||
}
|
||||
|
||||
headers = JoseHeader.from(headers)
|
||||
.type(JOSEObjectType.JWT.getType())
|
||||
.keyId(managedKey.getKeyId())
|
||||
.build();
|
||||
JWSHeader jwsHeader = jwsHeaderConverter.convert(headers);
|
||||
|
||||
claims = JwtClaimsSet.from(claims)
|
||||
.id(UUID.randomUUID().toString())
|
||||
.build();
|
||||
JWTClaimsSet jwtClaimsSet = jwtClaimsSetConverter.convert(claims);
|
||||
|
||||
SignedJWT signedJWT = new SignedJWT(jwsHeader, jwtClaimsSet);
|
||||
try {
|
||||
signedJWT.sign(jwsSigner);
|
||||
} catch (JOSEException ex) {
|
||||
throw new JwtEncodingException(String.format(
|
||||
ENCODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex);
|
||||
}
|
||||
String jws = signedJWT.serialize();
|
||||
|
||||
return new Jwt(jws, claims.getIssuedAt(), claims.getExpiresAt(),
|
||||
headers.getHeaders(), claims.getClaims());
|
||||
}
|
||||
|
||||
private ManagedKey selectKey(JoseHeader headers) {
|
||||
JwsAlgorithm jwsAlgorithm = headers.getJwsAlgorithm();
|
||||
String keyAlgorithm = jcaKeyAlgorithmMappings.get(jwsAlgorithm);
|
||||
if (!StringUtils.hasText(keyAlgorithm)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Set<ManagedKey> matchingKeys = this.keyManager.findByAlgorithm(keyAlgorithm);
|
||||
if (CollectionUtils.isEmpty(matchingKeys)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return matchingKeys.stream()
|
||||
.filter(ManagedKey::isActive)
|
||||
.max(this::mostRecentActivated)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private int mostRecentActivated(ManagedKey managedKey1, ManagedKey managedKey2) {
|
||||
return managedKey1.getActivatedOn().isAfter(managedKey2.getActivatedOn()) ? 1 : -1;
|
||||
}
|
||||
|
||||
private static class JwsHeaderConverter implements Converter<JoseHeader, JWSHeader> {
|
||||
|
||||
@Override
|
||||
public JWSHeader convert(JoseHeader headers) {
|
||||
JWSHeader.Builder builder = new JWSHeader.Builder(
|
||||
JWSAlgorithm.parse(headers.getJwsAlgorithm().getName()));
|
||||
|
||||
Set<String> critical = headers.getCritical();
|
||||
if (!CollectionUtils.isEmpty(critical)) {
|
||||
builder.criticalParams(critical);
|
||||
}
|
||||
|
||||
String contentType = headers.getContentType();
|
||||
if (StringUtils.hasText(contentType)) {
|
||||
builder.contentType(contentType);
|
||||
}
|
||||
|
||||
String jwkSetUri = headers.getJwkSetUri();
|
||||
if (StringUtils.hasText(jwkSetUri)) {
|
||||
try {
|
||||
builder.jwkURL(new URI(jwkSetUri));
|
||||
} catch (Exception ex) {
|
||||
throw new JwtEncodingException(String.format(
|
||||
ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Failed to convert '" + JoseHeaderNames.JKU + "' JOSE header"), ex);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> jwk = headers.getJwk();
|
||||
if (!CollectionUtils.isEmpty(jwk)) {
|
||||
try {
|
||||
builder.jwk(JWK.parse(new JSONObject(jwk)));
|
||||
} catch (Exception ex) {
|
||||
throw new JwtEncodingException(String.format(
|
||||
ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Failed to convert '" + JoseHeaderNames.JWK + "' JOSE header"), ex);
|
||||
}
|
||||
}
|
||||
|
||||
String keyId = headers.getKeyId();
|
||||
if (StringUtils.hasText(keyId)) {
|
||||
builder.keyID(keyId);
|
||||
}
|
||||
|
||||
String type = headers.getType();
|
||||
if (StringUtils.hasText(type)) {
|
||||
builder.type(new JOSEObjectType(type));
|
||||
}
|
||||
|
||||
List<String> x509CertificateChain = headers.getX509CertificateChain();
|
||||
if (!CollectionUtils.isEmpty(x509CertificateChain)) {
|
||||
builder.x509CertChain(
|
||||
x509CertificateChain.stream()
|
||||
.map(Base64::new)
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
String x509SHA1Thumbprint = headers.getX509SHA1Thumbprint();
|
||||
if (StringUtils.hasText(x509SHA1Thumbprint)) {
|
||||
builder.x509CertThumbprint(new Base64URL(x509SHA1Thumbprint));
|
||||
}
|
||||
|
||||
String x509SHA256Thumbprint = headers.getX509SHA256Thumbprint();
|
||||
if (StringUtils.hasText(x509SHA256Thumbprint)) {
|
||||
builder.x509CertSHA256Thumbprint(new Base64URL(x509SHA256Thumbprint));
|
||||
}
|
||||
|
||||
String x509Uri = headers.getX509Uri();
|
||||
if (StringUtils.hasText(x509Uri)) {
|
||||
try {
|
||||
builder.x509CertURL(new URI(x509Uri));
|
||||
} catch (Exception ex) {
|
||||
throw new JwtEncodingException(String.format(
|
||||
ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Failed to convert '" + JoseHeaderNames.X5U + "' JOSE header"), ex);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> customHeaders = headers.getHeaders().entrySet().stream()
|
||||
.filter(header -> !JWSHeader.getRegisteredParameterNames().contains(header.getKey()))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
if (!CollectionUtils.isEmpty(customHeaders)) {
|
||||
builder.customParams(customHeaders);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
|
||||
private static class JwtClaimsSetConverter implements Converter<JwtClaimsSet, JWTClaimsSet> {
|
||||
|
||||
@Override
|
||||
public JWTClaimsSet convert(JwtClaimsSet claims) {
|
||||
JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder();
|
||||
|
||||
URL issuer = claims.getIssuer();
|
||||
if (issuer != null) {
|
||||
builder.issuer(issuer.toExternalForm());
|
||||
}
|
||||
|
||||
String subject = claims.getSubject();
|
||||
if (StringUtils.hasText(subject)) {
|
||||
builder.subject(subject);
|
||||
}
|
||||
|
||||
List<String> audience = claims.getAudience();
|
||||
if (!CollectionUtils.isEmpty(audience)) {
|
||||
builder.audience(audience);
|
||||
}
|
||||
|
||||
Instant issuedAt = claims.getIssuedAt();
|
||||
if (issuedAt != null) {
|
||||
builder.issueTime(Date.from(issuedAt));
|
||||
}
|
||||
|
||||
Instant expiresAt = claims.getExpiresAt();
|
||||
if (expiresAt != null) {
|
||||
builder.expirationTime(Date.from(expiresAt));
|
||||
}
|
||||
|
||||
Instant notBefore = claims.getNotBefore();
|
||||
if (notBefore != null) {
|
||||
builder.notBeforeTime(Date.from(notBefore));
|
||||
}
|
||||
|
||||
String jwtId = claims.getId();
|
||||
if (StringUtils.hasText(jwtId)) {
|
||||
builder.jwtID(jwtId);
|
||||
}
|
||||
|
||||
Map<String, Object> customClaims = claims.getClaims().entrySet().stream()
|
||||
.filter(claim -> !JWTClaimsSet.getRegisteredNames().contains(claim.getKey()))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
if (!CollectionUtils.isEmpty(customClaims)) {
|
||||
customClaims.forEach(builder::claim);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.net.URL;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.springframework.security.oauth2.jwt.JwtClaimNames.AUD;
|
||||
import static org.springframework.security.oauth2.jwt.JwtClaimNames.EXP;
|
||||
import static org.springframework.security.oauth2.jwt.JwtClaimNames.IAT;
|
||||
import static org.springframework.security.oauth2.jwt.JwtClaimNames.ISS;
|
||||
import static org.springframework.security.oauth2.jwt.JwtClaimNames.JTI;
|
||||
import static org.springframework.security.oauth2.jwt.JwtClaimNames.NBF;
|
||||
import static org.springframework.security.oauth2.jwt.JwtClaimNames.SUB;
|
||||
|
||||
/**
|
||||
* The {@link Jwt JWT} Claims Set is a JSON object representing the claims conveyed by a JSON Web Token.
|
||||
*
|
||||
* @author Anoop Garlapati
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.1
|
||||
* @see Jwt
|
||||
* @see JwtClaimAccessor
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519#section-4">JWT Claims Set</a>
|
||||
*/
|
||||
public final class JwtClaimsSet implements JwtClaimAccessor {
|
||||
private final Map<String, Object> claims;
|
||||
|
||||
private JwtClaimsSet(Map<String, Object> claims) {
|
||||
this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getClaims() {
|
||||
return this.claims;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Builder}.
|
||||
*
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder withClaims() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Builder}, initialized with the provided {@code claims}.
|
||||
*
|
||||
* @param claims a JWT claims set
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder from(JwtClaimsSet claims) {
|
||||
return new Builder(claims);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link JwtClaimsSet}.
|
||||
*/
|
||||
public static class Builder {
|
||||
private final Map<String, Object> claims = new LinkedHashMap<>();
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
private Builder(JwtClaimsSet claims) {
|
||||
Assert.notNull(claims, "claims cannot be null");
|
||||
this.claims.putAll(claims.getClaims());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the issuer {@code (iss)} claim, which identifies the principal that issued the JWT.
|
||||
*
|
||||
* @param issuer the issuer identifier
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder issuer(URL issuer) {
|
||||
return claim(ISS, issuer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the subject {@code (sub)} claim, which identifies the principal that is the subject of the JWT.
|
||||
*
|
||||
* @param subject the subject identifier
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder subject(String subject) {
|
||||
return claim(SUB, subject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the audience {@code (aud)} claim, which identifies the recipient(s) that the JWT is intended for.
|
||||
*
|
||||
* @param audience the audience that this JWT is intended for
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder audience(List<String> audience) {
|
||||
return claim(AUD, audience);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the expiration time {@code (exp)} claim, which identifies the time
|
||||
* on or after which the JWT MUST NOT be accepted for processing.
|
||||
*
|
||||
* @param expiresAt the time on or after which the JWT MUST NOT be accepted for processing
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder expiresAt(Instant expiresAt) {
|
||||
return claim(EXP, expiresAt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the not before {@code (nbf)} claim, which identifies the time
|
||||
* before which the JWT MUST NOT be accepted for processing.
|
||||
*
|
||||
* @param notBefore the time before which the JWT MUST NOT be accepted for processing
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder notBefore(Instant notBefore) {
|
||||
return claim(NBF, notBefore);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the issued at {@code (iat)} claim, which identifies the time at which the JWT was issued.
|
||||
*
|
||||
* @param issuedAt the time at which the JWT was issued
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder issuedAt(Instant issuedAt) {
|
||||
return claim(IAT, issuedAt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the JWT ID {@code (jti)} claim, which provides a unique identifier for the JWT.
|
||||
*
|
||||
* @param jti the unique identifier for the JWT
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder id(String jti) {
|
||||
return claim(JTI, jti);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the claim.
|
||||
*
|
||||
* @param name the claim name
|
||||
* @param value the claim value
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder claim(String name, Object value) {
|
||||
Assert.hasText(name, "name cannot be empty");
|
||||
Assert.notNull(value, "value cannot be null");
|
||||
this.claims.put(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@code Consumer} to be provided access to the claims set
|
||||
* allowing the ability to add, replace, or remove.
|
||||
*
|
||||
* @param claimsConsumer a {@code Consumer} of the claims set
|
||||
*/
|
||||
public Builder claims(Consumer<Map<String, Object>> claimsConsumer) {
|
||||
claimsConsumer.accept(this.claims);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new {@link JwtClaimsSet}.
|
||||
*
|
||||
* @return a {@link JwtClaimsSet}
|
||||
*/
|
||||
public JwtClaimsSet build() {
|
||||
Assert.notEmpty(this.claims, "claims cannot be empty");
|
||||
return new JwtClaimsSet(this.claims);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
import org.springframework.security.oauth2.jose.JoseHeader;
|
||||
|
||||
/**
|
||||
* Implementations of this interface are responsible for encoding
|
||||
* a JSON Web Token (JWT) to it's compact claims representation format.
|
||||
*
|
||||
* <p>
|
||||
* JWTs may be represented using the JWS Compact Serialization format for a
|
||||
* JSON Web Signature (JWS) structure or JWE Compact Serialization format for a
|
||||
* JSON Web Encryption (JWE) structure. Therefore, implementors are responsible
|
||||
* for signing a JWS and/or encrypting a JWE.
|
||||
*
|
||||
* @author Anoop Garlapati
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.1
|
||||
* @see Jwt
|
||||
* @see JoseHeader
|
||||
* @see JwtClaimsSet
|
||||
* @see JwtDecoder
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519">JSON Web Token (JWT)</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515">JSON Web Signature (JWS)</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7516">JSON Web Encryption (JWE)</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515#section-3.1">JWS Compact Serialization</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7516#section-3.1">JWE Compact Serialization</a>
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface JwtEncoder {
|
||||
|
||||
/**
|
||||
* Encode the JWT to it's compact claims representation format.
|
||||
*
|
||||
* @param headers the JOSE header
|
||||
* @param claims the JWT Claims Set
|
||||
* @return a {@link Jwt}
|
||||
* @throws JwtEncodingException if an error occurs while attempting to encode the JWT
|
||||
*/
|
||||
Jwt encode(JoseHeader headers, JwtClaimsSet claims) throws JwtEncodingException;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
/**
|
||||
* This exception is thrown when an error occurs
|
||||
* while attempting to encode a JSON Web Token (JWT).
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.1
|
||||
*/
|
||||
public class JwtEncodingException extends JwtException {
|
||||
|
||||
/**
|
||||
* Constructs a {@code JwtEncodingException} using the provided parameters.
|
||||
*
|
||||
* @param message the detail message
|
||||
*/
|
||||
public JwtEncodingException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a {@code JwtEncodingException} using the provided parameters.
|
||||
*
|
||||
* @param message the detail message
|
||||
* @param cause the root cause
|
||||
*/
|
||||
public JwtEncodingException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* 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.config.annotation.web.configurers.oauth2.server.authorization;
|
||||
|
||||
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;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||
import org.springframework.security.config.test.SpringTestRule;
|
||||
import org.springframework.security.crypto.keys.KeyManager;
|
||||
import org.springframework.security.crypto.keys.StaticKeyGeneratingKeyManager;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationAttributeNames;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
|
||||
import org.springframework.security.oauth2.server.authorization.TokenType;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
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.web.OAuth2AuthorizationEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
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.hamcrest.CoreMatchers.containsString;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Integration tests for the OAuth 2.0 Authorization Code Grant.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
public class OAuth2AuthorizationCodeGrantTests {
|
||||
private static RegisteredClientRepository registeredClientRepository;
|
||||
private static OAuth2AuthorizationService authorizationService;
|
||||
private static KeyManager keyManager;
|
||||
|
||||
@Rule
|
||||
public final SpringTestRule spring = new SpringTestRule();
|
||||
|
||||
@Autowired
|
||||
private MockMvc mvc;
|
||||
|
||||
@BeforeClass
|
||||
public static void init() {
|
||||
registeredClientRepository = mock(RegisteredClientRepository.class);
|
||||
authorizationService = mock(OAuth2AuthorizationService.class);
|
||||
keyManager = new StaticKeyGeneratingKeyManager();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
reset(registeredClientRepository);
|
||||
reset(authorizationService);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenAuthorizationRequestNotAuthenticatedThenRedirectToLogin() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfiguration.class).autowire();
|
||||
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||
when(registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
MvcResult mvcResult = this.mvc.perform(MockMvcRequestBuilders.get(OAuth2AuthorizationEndpointFilter.DEFAULT_AUTHORIZATION_ENDPOINT_URI)
|
||||
.params(getAuthorizationRequestParameters(registeredClient)))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andReturn();
|
||||
assertThat(mvcResult.getResponse().getRedirectedUrl()).endsWith("/login");
|
||||
|
||||
verify(registeredClientRepository).findByClientId(eq(registeredClient.getClientId()));
|
||||
verifyNoInteractions(authorizationService);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenAuthorizationRequestAuthenticatedThenRedirectToClient() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfiguration.class).autowire();
|
||||
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||
when(registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
MvcResult mvcResult = this.mvc.perform(get(OAuth2AuthorizationEndpointFilter.DEFAULT_AUTHORIZATION_ENDPOINT_URI)
|
||||
.params(getAuthorizationRequestParameters(registeredClient))
|
||||
.with(user("user")))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andReturn();
|
||||
assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://example.com\\?code=.{15,}&state=state");
|
||||
|
||||
verify(registeredClientRepository).findByClientId(eq(registeredClient.getClientId()));
|
||||
verify(authorizationService).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenTokenRequestValidThenResponseIncludesCacheHeaders() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfiguration.class).autowire();
|
||||
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||
when(registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
|
||||
when(authorizationService.findByToken(
|
||||
eq(authorization.getAttribute(OAuth2AuthorizationAttributeNames.CODE)),
|
||||
eq(TokenType.AUTHORIZATION_CODE)))
|
||||
.thenReturn(authorization);
|
||||
|
||||
this.mvc.perform(MockMvcRequestBuilders.post(OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI)
|
||||
.params(getTokenRequestParameters(registeredClient, authorization))
|
||||
.header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth(
|
||||
registeredClient.getClientId(), registeredClient.getClientSecret())))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store")))
|
||||
.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache")));
|
||||
|
||||
verify(registeredClientRepository).findByClientId(eq(registeredClient.getClientId()));
|
||||
verify(authorizationService).findByToken(
|
||||
eq(authorization.getAttribute(OAuth2AuthorizationAttributeNames.CODE)),
|
||||
eq(TokenType.AUTHORIZATION_CODE));
|
||||
verify(authorizationService).save(any());
|
||||
}
|
||||
|
||||
private static MultiValueMap<String, String> getAuthorizationRequestParameters(RegisteredClient registeredClient) {
|
||||
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
|
||||
parameters.set(OAuth2ParameterNames.RESPONSE_TYPE, OAuth2AuthorizationResponseType.CODE.getValue());
|
||||
parameters.set(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
|
||||
parameters.set(OAuth2ParameterNames.REDIRECT_URI, registeredClient.getRedirectUris().iterator().next());
|
||||
parameters.set(OAuth2ParameterNames.SCOPE,
|
||||
StringUtils.collectionToDelimitedString(registeredClient.getScopes(), " "));
|
||||
parameters.set(OAuth2ParameterNames.STATE, "state");
|
||||
return parameters;
|
||||
}
|
||||
|
||||
private static MultiValueMap<String, String> getTokenRequestParameters(RegisteredClient registeredClient,
|
||||
OAuth2Authorization authorization) {
|
||||
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
|
||||
parameters.set(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
|
||||
parameters.set(OAuth2ParameterNames.CODE, authorization.getAttribute(OAuth2AuthorizationAttributeNames.CODE));
|
||||
parameters.set(OAuth2ParameterNames.REDIRECT_URI, registeredClient.getRedirectUris().iterator().next());
|
||||
return parameters;
|
||||
}
|
||||
|
||||
private static String encodeBasicAuth(String clientId, String secret) throws Exception {
|
||||
clientId = URLEncoder.encode(clientId, StandardCharsets.UTF_8.name());
|
||||
secret = URLEncoder.encode(secret, StandardCharsets.UTF_8.name());
|
||||
String credentialsString = clientId + ":" + secret;
|
||||
byte[] encodedBytes = Base64.getEncoder().encode(credentialsString.getBytes(StandardCharsets.UTF_8));
|
||||
return new String(encodedBytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Import(OAuth2AuthorizationServerConfiguration.class)
|
||||
static class AuthorizationServerConfiguration {
|
||||
|
||||
@Bean
|
||||
RegisteredClientRepository registeredClientRepository() {
|
||||
return registeredClientRepository;
|
||||
}
|
||||
|
||||
@Bean
|
||||
OAuth2AuthorizationService authorizationService() {
|
||||
return authorizationService;
|
||||
}
|
||||
|
||||
@Bean
|
||||
KeyManager keyManager() {
|
||||
return keyManager;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* 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.config.annotation.web.configurers.oauth2.server.authorization;
|
||||
|
||||
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;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||
import org.springframework.security.config.test.SpringTestRule;
|
||||
import org.springframework.security.crypto.keys.KeyManager;
|
||||
import org.springframework.security.crypto.keys.StaticKeyGeneratingKeyManager;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
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;
|
||||
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
|
||||
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;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Integration tests for the OAuth 2.0 Client Credentials Grant.
|
||||
*
|
||||
* @author Alexey Nesterov
|
||||
*/
|
||||
public class OAuth2ClientCredentialsGrantTests {
|
||||
private static RegisteredClientRepository registeredClientRepository;
|
||||
private static OAuth2AuthorizationService authorizationService;
|
||||
private static KeyManager keyManager;
|
||||
|
||||
@Rule
|
||||
public final SpringTestRule spring = new SpringTestRule();
|
||||
|
||||
@Autowired
|
||||
private MockMvc mvc;
|
||||
|
||||
@BeforeClass
|
||||
public static void init() {
|
||||
registeredClientRepository = mock(RegisteredClientRepository.class);
|
||||
authorizationService = mock(OAuth2AuthorizationService.class);
|
||||
keyManager = new StaticKeyGeneratingKeyManager();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
reset(registeredClientRepository);
|
||||
reset(authorizationService);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenTokenRequestNotAuthenticatedThenUnauthorized() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfiguration.class).autowire();
|
||||
|
||||
this.mvc.perform(MockMvcRequestBuilders.post(OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI)
|
||||
.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()))
|
||||
.andExpect(status().isUnauthorized());
|
||||
|
||||
verifyNoInteractions(registeredClientRepository);
|
||||
verifyNoInteractions(authorizationService);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenTokenRequestValidThenTokenResponse() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfiguration.class).autowire();
|
||||
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient2().build();
|
||||
when(registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
this.mvc.perform(post(OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI)
|
||||
.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
|
||||
.param(OAuth2ParameterNames.SCOPE, "scope1 scope2")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth(
|
||||
registeredClient.getClientId(), registeredClient.getClientSecret())))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.access_token").isNotEmpty())
|
||||
.andExpect(jsonPath("$.scope").value("scope1 scope2"));
|
||||
|
||||
verify(registeredClientRepository).findByClientId(eq(registeredClient.getClientId()));
|
||||
verify(authorizationService).save(any());
|
||||
}
|
||||
|
||||
private static String encodeBasicAuth(String clientId, String secret) throws Exception {
|
||||
clientId = URLEncoder.encode(clientId, StandardCharsets.UTF_8.name());
|
||||
secret = URLEncoder.encode(secret, StandardCharsets.UTF_8.name());
|
||||
String credentialsString = clientId + ":" + secret;
|
||||
byte[] encodedBytes = Base64.getEncoder().encode(credentialsString.getBytes(StandardCharsets.UTF_8));
|
||||
return new String(encodedBytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Import(OAuth2AuthorizationServerConfiguration.class)
|
||||
static class AuthorizationServerConfiguration {
|
||||
|
||||
@Bean
|
||||
RegisteredClientRepository registeredClientRepository() {
|
||||
return registeredClientRepository;
|
||||
}
|
||||
|
||||
@Bean
|
||||
OAuth2AuthorizationService authorizationService() {
|
||||
return authorizationService;
|
||||
}
|
||||
|
||||
@Bean
|
||||
KeyManager keyManager() {
|
||||
return keyManager;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* Copyright 2002-2017 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.config.test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor;
|
||||
import org.springframework.mock.web.MockServletConfig;
|
||||
import org.springframework.mock.web.MockServletContext;
|
||||
import org.springframework.security.config.util.InMemoryXmlWebApplicationContext;
|
||||
import org.springframework.test.context.web.GenericXmlWebContextLoader;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.RequestPostProcessor;
|
||||
import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcConfigurer;
|
||||
import org.springframework.web.context.ConfigurableWebApplicationContext;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
|
||||
import org.springframework.web.context.support.XmlWebApplicationContext;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.Closeable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.springframework.security.config.BeanIds.SPRING_SECURITY_FILTER_CHAIN;
|
||||
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* This class is a straight copy from Spring Security.
|
||||
* It should be removed when merging this codebase into Spring Security.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @since 5.0
|
||||
*/
|
||||
public class SpringTestContext implements Closeable {
|
||||
private Object test;
|
||||
|
||||
private ConfigurableWebApplicationContext context;
|
||||
|
||||
private List<Filter> filters = new ArrayList<>();
|
||||
|
||||
public void setTest(Object test) {
|
||||
this.test = test;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
this.context.close();
|
||||
} catch(Exception e) {}
|
||||
}
|
||||
|
||||
public SpringTestContext context(ConfigurableWebApplicationContext context) {
|
||||
this.context = context;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SpringTestContext register(Class<?>... classes) {
|
||||
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
|
||||
applicationContext.register(classes);
|
||||
this.context = applicationContext;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SpringTestContext testConfigLocations(String... configLocations) {
|
||||
GenericXmlWebContextLoader loader = new GenericXmlWebContextLoader();
|
||||
String[] locations = loader.processLocations(this.test.getClass(),
|
||||
configLocations);
|
||||
return configLocations(locations);
|
||||
}
|
||||
|
||||
public SpringTestContext configLocations(String... configLocations) {
|
||||
XmlWebApplicationContext context = new XmlWebApplicationContext();
|
||||
context.setConfigLocations(configLocations);
|
||||
this.context = context;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SpringTestContext context(String configuration) {
|
||||
InMemoryXmlWebApplicationContext context = new InMemoryXmlWebApplicationContext(configuration);
|
||||
this.context = context;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SpringTestContext mockMvcAfterSpringSecurityOk() {
|
||||
return addFilter(new OncePerRequestFilter() {
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request,
|
||||
HttpServletResponse response, FilterChain filterChain) {
|
||||
response.setStatus(HttpServletResponse.SC_OK);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private SpringTestContext addFilter(Filter filter) {
|
||||
this.filters.add(filter);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ConfigurableWebApplicationContext getContext() {
|
||||
if (!this.context.isRunning()) {
|
||||
this.context.setServletContext(new MockServletContext());
|
||||
this.context.setServletConfig(new MockServletConfig());
|
||||
this.context.refresh();
|
||||
}
|
||||
return this.context;
|
||||
}
|
||||
|
||||
public void autowire() {
|
||||
this.context.setServletContext(new MockServletContext());
|
||||
this.context.setServletConfig(new MockServletConfig());
|
||||
this.context.refresh();
|
||||
|
||||
if (this.context.containsBean(SPRING_SECURITY_FILTER_CHAIN)) {
|
||||
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
|
||||
.apply(springSecurity())
|
||||
.apply(new AddFilter()).build();
|
||||
this.context.getBeanFactory()
|
||||
.registerResolvableDependency(MockMvc.class, mockMvc);
|
||||
}
|
||||
|
||||
AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor();
|
||||
bpp.setBeanFactory(this.context.getBeanFactory());
|
||||
bpp.processInjection(this.test);
|
||||
}
|
||||
|
||||
private class AddFilter implements MockMvcConfigurer {
|
||||
public RequestPostProcessor beforeMockMvcCreated(
|
||||
ConfigurableMockMvcBuilder<?> builder, WebApplicationContext context) {
|
||||
builder.addFilters(SpringTestContext.this.filters.toArray(new Filter[0]));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2002-2017 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.config.test;
|
||||
|
||||
import org.junit.rules.MethodRule;
|
||||
import org.junit.runners.model.FrameworkMethod;
|
||||
import org.junit.runners.model.Statement;
|
||||
import org.springframework.security.test.context.TestSecurityContextHolder;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* This class is a straight copy from Spring Security.
|
||||
* It should be removed when merging this codebase into Spring Security.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @since 5.0
|
||||
*/
|
||||
public class SpringTestRule extends SpringTestContext implements MethodRule {
|
||||
@Override
|
||||
public Statement apply(Statement base, FrameworkMethod method, Object target) {
|
||||
return new Statement() {
|
||||
public void evaluate() throws Throwable {
|
||||
setTest(target);
|
||||
try {
|
||||
base.evaluate();
|
||||
} finally {
|
||||
TestSecurityContextHolder.clearContext();
|
||||
close();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright 2009-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.config.util;
|
||||
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.support.AbstractXmlApplicationContext;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.security.util.InMemoryResource;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* This class is a straight copy from Spring Security.
|
||||
* It should be removed when merging this codebase into Spring Security.
|
||||
*
|
||||
* @author Luke Taylor
|
||||
* @author Eddú Meléndez
|
||||
*/
|
||||
public class InMemoryXmlApplicationContext extends AbstractXmlApplicationContext {
|
||||
static final String BEANS_OPENING = "<b:beans xmlns='http://www.springframework.org/schema/security'\n"
|
||||
+ " xmlns:context='http://www.springframework.org/schema/context'\n"
|
||||
+ " xmlns:b='http://www.springframework.org/schema/beans'\n"
|
||||
+ " xmlns:aop='http://www.springframework.org/schema/aop'\n"
|
||||
+ " xmlns:mvc='http://www.springframework.org/schema/mvc'\n"
|
||||
+ " xmlns:websocket='http://www.springframework.org/schema/websocket'\n"
|
||||
+ " xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'\n"
|
||||
+ " xsi:schemaLocation='http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-2.5.xsd\n"
|
||||
+ "http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop-2.5.xsd\n"
|
||||
+ "http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd\n"
|
||||
+ "http://www.springframework.org/schema/websocket https://www.springframework.org/schema/websocket/spring-websocket.xsd\n"
|
||||
+ "http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context-2.5.xsd\n"
|
||||
+ "http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security-";
|
||||
static final String BEANS_CLOSE = "</b:beans>\n";
|
||||
|
||||
static final String SPRING_SECURITY_VERSION = "5.4";
|
||||
|
||||
Resource inMemoryXml;
|
||||
|
||||
public InMemoryXmlApplicationContext(String xml) {
|
||||
this(xml, SPRING_SECURITY_VERSION, null);
|
||||
}
|
||||
|
||||
public InMemoryXmlApplicationContext(String xml, ApplicationContext parent) {
|
||||
this(xml, SPRING_SECURITY_VERSION, parent);
|
||||
}
|
||||
|
||||
public InMemoryXmlApplicationContext(String xml, String secVersion, ApplicationContext parent) {
|
||||
String fullXml = BEANS_OPENING + secVersion + ".xsd'>\n" + xml + BEANS_CLOSE;
|
||||
inMemoryXml = new InMemoryResource(fullXml);
|
||||
setAllowBeanDefinitionOverriding(true);
|
||||
setParent(parent);
|
||||
refresh();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DefaultListableBeanFactory createBeanFactory() {
|
||||
return new DefaultListableBeanFactory(getInternalParentBeanFactory()) {
|
||||
@Override
|
||||
protected boolean allowAliasOverriding() {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected Resource[] getConfigResources() {
|
||||
return new Resource[] { inMemoryXml };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2012-2016 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.config.util;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.security.util.InMemoryResource;
|
||||
import org.springframework.web.context.support.AbstractRefreshableWebApplicationContext;
|
||||
|
||||
import static org.springframework.security.config.util.InMemoryXmlApplicationContext.BEANS_CLOSE;
|
||||
import static org.springframework.security.config.util.InMemoryXmlApplicationContext.BEANS_OPENING;
|
||||
import static org.springframework.security.config.util.InMemoryXmlApplicationContext.SPRING_SECURITY_VERSION;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* This class is a straight copy from Spring Security.
|
||||
* It should be removed when merging this codebase into Spring Security.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
public class InMemoryXmlWebApplicationContext extends AbstractRefreshableWebApplicationContext {
|
||||
private Resource inMemoryXml;
|
||||
|
||||
public InMemoryXmlWebApplicationContext(String xml) {
|
||||
this(xml, SPRING_SECURITY_VERSION, null);
|
||||
}
|
||||
|
||||
public InMemoryXmlWebApplicationContext(String xml, ApplicationContext parent) {
|
||||
this(xml, SPRING_SECURITY_VERSION, parent);
|
||||
}
|
||||
|
||||
public InMemoryXmlWebApplicationContext(String xml, String secVersion, ApplicationContext parent) {
|
||||
String fullXml = BEANS_OPENING + secVersion + ".xsd'>\n" + xml + BEANS_CLOSE;
|
||||
inMemoryXml = new InMemoryResource(fullXml);
|
||||
setAllowBeanDefinitionOverriding(true);
|
||||
setParent(parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException {
|
||||
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
|
||||
reader.loadBeanDefinitions(new Resource[] { inMemoryXml });
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.crypto.keys;
|
||||
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.security.Key;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.springframework.security.crypto.keys.KeyGeneratorUtils.generateRsaKey;
|
||||
import static org.springframework.security.crypto.keys.KeyGeneratorUtils.generateSecretKey;
|
||||
|
||||
/**
|
||||
* Tests for {@link ManagedKey}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
public class ManagedKeyTests {
|
||||
private static SecretKey secretKey;
|
||||
private static KeyPair rsaKeyPair;
|
||||
|
||||
@BeforeClass
|
||||
public static void init() {
|
||||
secretKey = generateSecretKey();
|
||||
rsaKeyPair = generateRsaKey();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withSymmetricKeyWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> ManagedKey.withSymmetricKey(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("secretKey cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenKeyIdNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> ManagedKey.withSymmetricKey(secretKey).build())
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("keyId cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenActivatedOnNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> ManagedKey.withSymmetricKey(secretKey).keyId("keyId").build())
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("activatedOn cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenSymmetricKeyAllAttributesProvidedThenAllAttributesAreSet() {
|
||||
ManagedKey expectedManagedKey = TestManagedKeys.secretManagedKey().build();
|
||||
|
||||
ManagedKey managedKey = ManagedKey.withSymmetricKey(expectedManagedKey.getKey())
|
||||
.keyId(expectedManagedKey.getKeyId())
|
||||
.activatedOn(expectedManagedKey.getActivatedOn())
|
||||
.build();
|
||||
|
||||
assertThat(managedKey.isSymmetric()).isTrue();
|
||||
assertThat(managedKey.<Key>getKey()).isInstanceOf(SecretKey.class);
|
||||
assertThat(managedKey.<SecretKey>getKey()).isEqualTo(expectedManagedKey.getKey());
|
||||
assertThat(managedKey.getPublicKey()).isNull();
|
||||
assertThat(managedKey.getKeyId()).isEqualTo(expectedManagedKey.getKeyId());
|
||||
assertThat(managedKey.getActivatedOn()).isEqualTo(expectedManagedKey.getActivatedOn());
|
||||
assertThat(managedKey.getDeactivatedOn()).isEqualTo(expectedManagedKey.getDeactivatedOn());
|
||||
assertThat(managedKey.isActive()).isTrue();
|
||||
assertThat(managedKey.getAlgorithm()).isEqualTo(expectedManagedKey.getAlgorithm());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withAsymmetricKeyWhenPublicKeyNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> ManagedKey.withAsymmetricKey(null, rsaKeyPair.getPrivate()))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("publicKey cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withAsymmetricKeyWhenPrivateKeyNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> ManagedKey.withAsymmetricKey(rsaKeyPair.getPublic(), null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("privateKey cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenAsymmetricKeyAllAttributesProvidedThenAllAttributesAreSet() {
|
||||
ManagedKey expectedManagedKey = TestManagedKeys.rsaManagedKey().build();
|
||||
|
||||
ManagedKey managedKey = ManagedKey.withAsymmetricKey(expectedManagedKey.getPublicKey(), expectedManagedKey.getKey())
|
||||
.keyId(expectedManagedKey.getKeyId())
|
||||
.activatedOn(expectedManagedKey.getActivatedOn())
|
||||
.build();
|
||||
|
||||
assertThat(managedKey.isAsymmetric()).isTrue();
|
||||
assertThat(managedKey.<Key>getKey()).isInstanceOf(PrivateKey.class);
|
||||
assertThat(managedKey.<PrivateKey>getKey()).isEqualTo(expectedManagedKey.getKey());
|
||||
assertThat(managedKey.getPublicKey()).isNotNull();
|
||||
assertThat(managedKey.getKeyId()).isEqualTo(expectedManagedKey.getKeyId());
|
||||
assertThat(managedKey.getActivatedOn()).isEqualTo(expectedManagedKey.getActivatedOn());
|
||||
assertThat(managedKey.getDeactivatedOn()).isEqualTo(expectedManagedKey.getDeactivatedOn());
|
||||
assertThat(managedKey.isActive()).isTrue();
|
||||
assertThat(managedKey.getAlgorithm()).isEqualTo(expectedManagedKey.getAlgorithm());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.crypto.keys;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.springframework.security.crypto.keys.KeyGeneratorUtils.generateEcKey;
|
||||
import static org.springframework.security.crypto.keys.KeyGeneratorUtils.generateRsaKey;
|
||||
import static org.springframework.security.crypto.keys.KeyGeneratorUtils.generateSecretKey;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
public class TestManagedKeys {
|
||||
|
||||
public static ManagedKey.Builder secretManagedKey() {
|
||||
return ManagedKey.withSymmetricKey(generateSecretKey())
|
||||
.keyId(UUID.randomUUID().toString())
|
||||
.activatedOn(Instant.now());
|
||||
}
|
||||
|
||||
public static ManagedKey.Builder rsaManagedKey() {
|
||||
KeyPair rsaKeyPair = generateRsaKey();
|
||||
return ManagedKey.withAsymmetricKey(rsaKeyPair.getPublic(), rsaKeyPair.getPrivate())
|
||||
.keyId(UUID.randomUUID().toString())
|
||||
.activatedOn(Instant.now());
|
||||
}
|
||||
|
||||
public static ManagedKey.Builder ecManagedKey() {
|
||||
KeyPair ecKeyPair = generateEcKey();
|
||||
return ManagedKey.withAsymmetricKey(ecKeyPair.getPublic(), ecKeyPair.getPrivate())
|
||||
.keyId(UUID.randomUUID().toString())
|
||||
.activatedOn(Instant.now());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.jose;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Tests for {@link JoseHeader}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
public class JoseHeaderTests {
|
||||
|
||||
@Test
|
||||
public void withAlgorithmWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> JoseHeader.withAlgorithm(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("jwsAlgorithm cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenAllHeadersProvidedThenAllHeadersAreSet() {
|
||||
JoseHeader expectedJoseHeader = TestJoseHeaders.joseHeader().build();
|
||||
|
||||
JoseHeader joseHeader = JoseHeader.withAlgorithm(expectedJoseHeader.getJwsAlgorithm())
|
||||
.jwkSetUri(expectedJoseHeader.getJwkSetUri())
|
||||
.jwk(expectedJoseHeader.getJwk())
|
||||
.keyId(expectedJoseHeader.getKeyId())
|
||||
.x509Uri(expectedJoseHeader.getX509Uri())
|
||||
.x509CertificateChain(expectedJoseHeader.getX509CertificateChain())
|
||||
.x509SHA1Thumbprint(expectedJoseHeader.getX509SHA1Thumbprint())
|
||||
.x509SHA256Thumbprint(expectedJoseHeader.getX509SHA256Thumbprint())
|
||||
.critical(expectedJoseHeader.getCritical())
|
||||
.type(expectedJoseHeader.getType())
|
||||
.contentType(expectedJoseHeader.getContentType())
|
||||
.headers(headers -> headers.put("custom-header-name", "custom-header-value"))
|
||||
.build();
|
||||
|
||||
assertThat(joseHeader.getJwsAlgorithm()).isEqualTo(expectedJoseHeader.getJwsAlgorithm());
|
||||
assertThat(joseHeader.getJwkSetUri()).isEqualTo(expectedJoseHeader.getJwkSetUri());
|
||||
assertThat(joseHeader.getJwk()).isEqualTo(expectedJoseHeader.getJwk());
|
||||
assertThat(joseHeader.getKeyId()).isEqualTo(expectedJoseHeader.getKeyId());
|
||||
assertThat(joseHeader.getX509Uri()).isEqualTo(expectedJoseHeader.getX509Uri());
|
||||
assertThat(joseHeader.getX509CertificateChain()).isEqualTo(expectedJoseHeader.getX509CertificateChain());
|
||||
assertThat(joseHeader.getX509SHA1Thumbprint()).isEqualTo(expectedJoseHeader.getX509SHA1Thumbprint());
|
||||
assertThat(joseHeader.getX509SHA256Thumbprint()).isEqualTo(expectedJoseHeader.getX509SHA256Thumbprint());
|
||||
assertThat(joseHeader.getCritical()).isEqualTo(expectedJoseHeader.getCritical());
|
||||
assertThat(joseHeader.getType()).isEqualTo(expectedJoseHeader.getType());
|
||||
assertThat(joseHeader.getContentType()).isEqualTo(expectedJoseHeader.getContentType());
|
||||
assertThat(joseHeader.<String>getHeader("custom-header-name")).isEqualTo("custom-header-value");
|
||||
assertThat(joseHeader.getHeaders()).isEqualTo(expectedJoseHeader.getHeaders());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> JoseHeader.from(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("headers cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromWhenHeadersProvidedThenCopied() {
|
||||
JoseHeader expectedJoseHeader = TestJoseHeaders.joseHeader().build();
|
||||
JoseHeader joseHeader = JoseHeader.from(expectedJoseHeader).build();
|
||||
assertThat(joseHeader.getHeaders()).isEqualTo(expectedJoseHeader.getHeaders());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void headerWhenNameNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> JoseHeader.withAlgorithm(SignatureAlgorithm.RS256).header(null, "value"))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("name cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void headerWhenValueNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> JoseHeader.withAlgorithm(SignatureAlgorithm.RS256).header("name", null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("value cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getHeaderWhenNullThenThrowIllegalArgumentException() {
|
||||
JoseHeader joseHeader = TestJoseHeaders.joseHeader().build();
|
||||
|
||||
assertThatThrownBy(() -> joseHeader.getHeader(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("name cannot be empty");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.jose;
|
||||
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
public class TestJoseHeaders {
|
||||
|
||||
public static JoseHeader.Builder joseHeader() {
|
||||
return joseHeader(SignatureAlgorithm.RS256);
|
||||
}
|
||||
|
||||
public static JoseHeader.Builder joseHeader(SignatureAlgorithm signatureAlgorithm) {
|
||||
return JoseHeader.withAlgorithm(signatureAlgorithm)
|
||||
.jwkSetUri("https://provider.com/oauth2/jwks")
|
||||
.jwk(rsaJwk())
|
||||
.keyId(UUID.randomUUID().toString())
|
||||
.x509Uri("https://provider.com/oauth2/x509")
|
||||
.x509CertificateChain(Arrays.asList("x509Cert1", "x509Cert2"))
|
||||
.x509SHA1Thumbprint("x509SHA1Thumbprint")
|
||||
.x509SHA256Thumbprint("x509SHA256Thumbprint")
|
||||
.critical(Collections.singleton("custom-header-name"))
|
||||
.type("JWT")
|
||||
.contentType("jwt-content-type")
|
||||
.header("custom-header-name", "custom-header-value");
|
||||
}
|
||||
|
||||
private static Map<String, Object> rsaJwk() {
|
||||
Map<String, Object> rsaJwk = new HashMap<>();
|
||||
rsaJwk.put("kty", "RSA");
|
||||
rsaJwk.put("n", "modulus");
|
||||
rsaJwk.put("e", "exponent");
|
||||
return rsaJwk;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.jose.jws;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.security.crypto.keys.KeyManager;
|
||||
import org.springframework.security.crypto.keys.ManagedKey;
|
||||
import org.springframework.security.crypto.keys.TestManagedKeys;
|
||||
import org.springframework.security.oauth2.jose.JoseHeader;
|
||||
import org.springframework.security.oauth2.jose.JoseHeaderNames;
|
||||
import org.springframework.security.oauth2.jose.TestJoseHeaders;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncodingException;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.TestJwtClaimsSets;
|
||||
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Collections;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Tests for {@link NimbusJwsEncoder}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
public class NimbusJwsEncoderTests {
|
||||
private KeyManager keyManager;
|
||||
private NimbusJwsEncoder jwtEncoder;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
this.keyManager = mock(KeyManager.class);
|
||||
this.jwtEncoder = new NimbusJwsEncoder(this.keyManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenKeyManagerNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> new NimbusJwsEncoder(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("keyManager cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenHeadersNullThenThrowIllegalArgumentException() {
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
assertThatThrownBy(() -> this.jwtEncoder.encode(null, jwtClaimsSet))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("headers cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenClaimsNullThenThrowIllegalArgumentException() {
|
||||
JoseHeader joseHeader = TestJoseHeaders.joseHeader().build();
|
||||
|
||||
assertThatThrownBy(() -> this.jwtEncoder.encode(joseHeader, null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("claims cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenUnsupportedKeyThenThrowJwtEncodingException() {
|
||||
JoseHeader joseHeader = TestJoseHeaders.joseHeader().build();
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
assertThatThrownBy(() -> this.jwtEncoder.encode(joseHeader, jwtClaimsSet))
|
||||
.isInstanceOf(JwtEncodingException.class)
|
||||
.hasMessageContaining("Unsupported key for algorithm 'RS256'");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenUnsupportedKeyAlgorithmThenThrowJwtEncodingException() {
|
||||
JoseHeader joseHeader = TestJoseHeaders.joseHeader(SignatureAlgorithm.ES256).build();
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
assertThatThrownBy(() -> this.jwtEncoder.encode(joseHeader, jwtClaimsSet))
|
||||
.isInstanceOf(JwtEncodingException.class)
|
||||
.hasMessageContaining("Unsupported key for algorithm 'ES256'");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenUnsupportedKeyTypeThenThrowJwtEncodingException() {
|
||||
ManagedKey managedKey = TestManagedKeys.ecManagedKey().build();
|
||||
when(this.keyManager.findByAlgorithm(any())).thenReturn(Collections.singleton(managedKey));
|
||||
|
||||
JoseHeader joseHeader = TestJoseHeaders.joseHeader(SignatureAlgorithm.ES256).build();
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
assertThatThrownBy(() -> this.jwtEncoder.encode(joseHeader, jwtClaimsSet))
|
||||
.isInstanceOf(JwtEncodingException.class)
|
||||
.hasMessageContaining("Unsupported key type 'EC'");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenSuccessThenDecodes() {
|
||||
ManagedKey managedKey = TestManagedKeys.rsaManagedKey().build();
|
||||
when(this.keyManager.findByAlgorithm(any())).thenReturn(Collections.singleton(managedKey));
|
||||
|
||||
JoseHeader joseHeader = TestJoseHeaders.joseHeader()
|
||||
.headers(headers -> headers.remove(JoseHeaderNames.CRIT))
|
||||
.build();
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
Jwt jws = this.jwtEncoder.encode(joseHeader, jwtClaimsSet);
|
||||
|
||||
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey((RSAPublicKey) managedKey.getPublicKey()).build();
|
||||
jwtDecoder.decode(jws.getTokenValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenMultipleActiveKeysThenUseMostRecent() {
|
||||
ManagedKey managedKeyActivated2DaysAgo = TestManagedKeys.rsaManagedKey()
|
||||
.activatedOn(Instant.now().minus(2, ChronoUnit.DAYS))
|
||||
.build();
|
||||
ManagedKey managedKeyActivated1DayAgo = TestManagedKeys.rsaManagedKey()
|
||||
.activatedOn(Instant.now().minus(1, ChronoUnit.DAYS))
|
||||
.build();
|
||||
ManagedKey managedKeyActivatedToday = TestManagedKeys.rsaManagedKey()
|
||||
.activatedOn(Instant.now())
|
||||
.build();
|
||||
|
||||
when(this.keyManager.findByAlgorithm(any())).thenReturn(
|
||||
Stream.of(managedKeyActivated2DaysAgo, managedKeyActivated1DayAgo, managedKeyActivatedToday)
|
||||
.collect(Collectors.toSet()));
|
||||
|
||||
JoseHeader joseHeader = TestJoseHeaders.joseHeader()
|
||||
.headers(headers -> headers.remove(JoseHeaderNames.CRIT))
|
||||
.build();
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
Jwt jws = this.jwtEncoder.encode(joseHeader, jwtClaimsSet);
|
||||
|
||||
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey((RSAPublicKey) managedKeyActivatedToday.getPublicKey()).build();
|
||||
jwtDecoder.decode(jws.getTokenValue());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Tests for {@link JwtClaimsSet}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
public class JwtClaimsSetTests {
|
||||
|
||||
@Test
|
||||
public void buildWhenClaimsEmptyThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> JwtClaimsSet.withClaims().build())
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("claims cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenAllClaimsProvidedThenAllClaimsAreSet() {
|
||||
JwtClaimsSet expectedJwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
JwtClaimsSet jwtClaimsSet = JwtClaimsSet.withClaims()
|
||||
.issuer(expectedJwtClaimsSet.getIssuer())
|
||||
.subject(expectedJwtClaimsSet.getSubject())
|
||||
.audience(expectedJwtClaimsSet.getAudience())
|
||||
.issuedAt(expectedJwtClaimsSet.getIssuedAt())
|
||||
.notBefore(expectedJwtClaimsSet.getNotBefore())
|
||||
.expiresAt(expectedJwtClaimsSet.getExpiresAt())
|
||||
.id(expectedJwtClaimsSet.getId())
|
||||
.claims(claims -> claims.put("custom-claim-name", "custom-claim-value"))
|
||||
.build();
|
||||
|
||||
assertThat(jwtClaimsSet.getIssuer()).isEqualTo(expectedJwtClaimsSet.getIssuer());
|
||||
assertThat(jwtClaimsSet.getSubject()).isEqualTo(expectedJwtClaimsSet.getSubject());
|
||||
assertThat(jwtClaimsSet.getAudience()).isEqualTo(expectedJwtClaimsSet.getAudience());
|
||||
assertThat(jwtClaimsSet.getIssuedAt()).isEqualTo(expectedJwtClaimsSet.getIssuedAt());
|
||||
assertThat(jwtClaimsSet.getNotBefore()).isEqualTo(expectedJwtClaimsSet.getNotBefore());
|
||||
assertThat(jwtClaimsSet.getExpiresAt()).isEqualTo(expectedJwtClaimsSet.getExpiresAt());
|
||||
assertThat(jwtClaimsSet.getId()).isEqualTo(expectedJwtClaimsSet.getId());
|
||||
assertThat(jwtClaimsSet.<String>getClaim("custom-claim-name")).isEqualTo("custom-claim-value");
|
||||
assertThat(jwtClaimsSet.getClaims()).isEqualTo(expectedJwtClaimsSet.getClaims());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> JwtClaimsSet.from(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("claims cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromWhenClaimsProvidedThenCopied() {
|
||||
JwtClaimsSet expectedJwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
JwtClaimsSet jwtClaimsSet = JwtClaimsSet.from(expectedJwtClaimsSet).build();
|
||||
assertThat(jwtClaimsSet.getClaims()).isEqualTo(expectedJwtClaimsSet.getClaims());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void claimWhenNameNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> JwtClaimsSet.withClaims().claim(null, "value"))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("name cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void claimWhenValueNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> JwtClaimsSet.withClaims().claim("name", null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("value cannot be null");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Collections;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
public class TestJwtClaimsSets {
|
||||
|
||||
public static JwtClaimsSet.Builder jwtClaimsSet() {
|
||||
URL issuer = null;
|
||||
try {
|
||||
issuer = URI.create("https://provider.com").toURL();
|
||||
} catch (MalformedURLException e) { }
|
||||
|
||||
Instant issuedAt = Instant.now();
|
||||
Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS);
|
||||
|
||||
return JwtClaimsSet.withClaims()
|
||||
.issuer(issuer)
|
||||
.subject("subject")
|
||||
.audience(Collections.singletonList("client-1"))
|
||||
.issuedAt(issuedAt)
|
||||
.notBefore(issuedAt)
|
||||
.expiresAt(expiresAt)
|
||||
.id(UUID.randomUUID().toString())
|
||||
.claim("custom-claim-name", "custom-claim-value");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user