diff --git a/radar-auth/src/main/java/org/radarcns/auth/authentication/TokenValidator.java b/radar-auth/src/main/java/org/radarcns/auth/authentication/TokenValidator.java index 62be0efdd..da4e83658 100644 --- a/radar-auth/src/main/java/org/radarcns/auth/authentication/TokenValidator.java +++ b/radar-auth/src/main/java/org/radarcns/auth/authentication/TokenValidator.java @@ -127,12 +127,12 @@ public RadarToken validateAccessToken(String token) throws TokenValidationExcept } return new JwtRadarToken(jwt); } catch (SignatureVerificationException sve) { - log.warn("Client presented a token with an incorrect signature, fetching public key" - + " again. Token: {}", token); + log.warn("Client presented a token with an incorrect signature, fetching public " + + "keys again. Token: {}", token); refresh(); return validateAccessToken(token); } catch (JWTVerificationException ex) { - throw new TokenValidationException(ex); + // this verifier does not accept the token, move on to the next one } } throw new TokenValidationException("No registered validator could authenticate this token"); diff --git a/src/main/java/org/radarcns/management/config/LocalKeystoreConfig.java b/src/main/java/org/radarcns/management/config/LocalKeystoreConfig.java index bf0c4a4d2..75accbf98 100644 --- a/src/main/java/org/radarcns/management/config/LocalKeystoreConfig.java +++ b/src/main/java/org/radarcns/management/config/LocalKeystoreConfig.java @@ -1,11 +1,14 @@ package org.radarcns.management.config; -import java.net.URI; -import java.security.KeyPair; -import java.security.interfaces.RSAPublicKey; import org.radarcns.auth.config.ServerConfig; +import org.radarcns.management.security.jwt.RadarKeyStoreKeyFactory; import org.springframework.core.io.ClassPathResource; -import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory; + +import java.net.URI; +import java.security.PublicKey; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; /** * Radar-auth server configuration for using a local keystore. This will load the MP public key @@ -14,22 +17,23 @@ public class LocalKeystoreConfig implements ServerConfig { public static final String RES_MANAGEMENT_PORTAL = "res_ManagementPortal"; - private final RSAPublicKey publicKey; + private final List publicKeys; /** * Constructor will look for the keystore in the classpath at /config/keystore.jks and load * the public key from it. */ - public LocalKeystoreConfig() { - KeyPair keyPair = new KeyStoreKeyFactory( - new ClassPathResource("/config/keystore.jks"), "radarbase".toCharArray()) - .getKeyPair("selfsigned"); - publicKey = (RSAPublicKey) keyPair.getPublic(); + public LocalKeystoreConfig(String keyStorePassword, List checkingKeyAliases) { + RadarKeyStoreKeyFactory keyFactory = new RadarKeyStoreKeyFactory( + new ClassPathResource("/config/keystore.jks"), keyStorePassword.toCharArray()); + publicKeys = checkingKeyAliases.stream() + .map(alias -> keyFactory.getKeyPair(alias).getPublic()) + .collect(Collectors.toList()); } @Override - public URI getPublicKeyEndpoint() { - return null; + public List getPublicKeyEndpoints() { + return Collections.emptyList(); } @Override @@ -38,7 +42,7 @@ public String getResourceName() { } @Override - public RSAPublicKey getPublicKey() { - return publicKey; + public List getPublicKeys() { + return publicKeys; } } diff --git a/src/main/java/org/radarcns/management/config/ManagementPortalProperties.java b/src/main/java/org/radarcns/management/config/ManagementPortalProperties.java index b88857656..2aa7f5f1b 100644 --- a/src/main/java/org/radarcns/management/config/ManagementPortalProperties.java +++ b/src/main/java/org/radarcns/management/config/ManagementPortalProperties.java @@ -2,6 +2,8 @@ import org.springframework.boot.context.properties.ConfigurationProperties; +import java.util.List; + /** * Created by nivethika on 3-10-17. */ @@ -112,6 +114,12 @@ public static class Oauth { private String clientsFile; + private String signingKeyAlias; + + private List checkingKeyAliases; + + private String keyStorePassword; + public String getClientsFile() { return clientsFile; } @@ -119,6 +127,30 @@ public String getClientsFile() { public void setClientsFile(String clientsFile) { this.clientsFile = clientsFile; } + + public String getSigningKeyAlias() { + return signingKeyAlias; + } + + public void setSigningKeyAlias(String signingKeyAlias) { + this.signingKeyAlias = signingKeyAlias; + } + + public List getCheckingKeyAliases() { + return checkingKeyAliases; + } + + public void setCheckingKeyAliases(List checkingKeyAliases) { + this.checkingKeyAliases = checkingKeyAliases; + } + + public String getKeyStorePassword() { + return keyStorePassword; + } + + public void setKeyStorePassword(String keyStorePassword) { + this.keyStorePassword = keyStorePassword; + } } public static class CatalogueServer { @@ -143,5 +175,4 @@ public void setEnableAutoImport(boolean enableAutoImport) { this.enableAutoImport = enableAutoImport; } } - } diff --git a/src/main/java/org/radarcns/management/config/OAuth2ServerConfiguration.java b/src/main/java/org/radarcns/management/config/OAuth2ServerConfiguration.java index 0a32130a9..211019c0e 100644 --- a/src/main/java/org/radarcns/management/config/OAuth2ServerConfiguration.java +++ b/src/main/java/org/radarcns/management/config/OAuth2ServerConfiguration.java @@ -2,11 +2,12 @@ import io.github.jhipster.security.AjaxLogoutSuccessHandler; import io.github.jhipster.security.Http401UnauthorizedEntryPoint; -import java.security.KeyPair; -import java.util.Arrays; -import javax.sql.DataSource; import org.radarcns.auth.authorization.AuthoritiesConstants; import org.radarcns.management.security.ClaimsTokenEnhancer; +import org.radarcns.management.security.jwt.EcdsaVerifier; +import org.radarcns.management.security.jwt.MultiVerifier; +import org.radarcns.management.security.jwt.RadarJwtAccessTokenConverter; +import org.radarcns.management.security.jwt.RadarKeyStoreKeyFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; @@ -23,6 +24,8 @@ import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.Authentication; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.jwt.crypto.sign.RsaVerifier; +import org.springframework.security.jwt.crypto.sign.SignatureVerifier; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; @@ -42,10 +45,17 @@ import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; -import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.filter.CorsFilter; +import javax.sql.DataSource; +import java.security.KeyPair; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPublicKey; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + @Configuration public class OAuth2ServerConfiguration { @@ -167,6 +177,9 @@ protected static class AuthorizationServerConfiguration extends @Autowired private JdbcClientDetailsService jdbcClientDetailsService; + @Autowired + private ManagementPortalProperties managementPortalProperties; + @Bean protected AuthorizationCodeServices authorizationCodeServices() { return new JdbcAuthorizationCodeServices(dataSource); @@ -189,13 +202,32 @@ public TokenStore tokenStore() { @Bean public JwtAccessTokenConverter accessTokenConverter() { - JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); - - KeyPair keyPair = new KeyStoreKeyFactory( - new ClassPathResource("/config/keystore.jks"), "radarbase".toCharArray()) - .getKeyPair("selfsigned"); + RadarJwtAccessTokenConverter converter = new RadarJwtAccessTokenConverter(); + + // set the keypair for signing + RadarKeyStoreKeyFactory kf = new RadarKeyStoreKeyFactory( + new ClassPathResource("/config/keystore.jks"), + managementPortalProperties.getOauth().getKeyStorePassword().toCharArray()); + String signKey = managementPortalProperties.getOauth().getSigningKeyAlias(); + KeyPair keyPair = kf.getKeyPair(signKey); converter.setKeyPair(keyPair); + // get all public keys for verifying and set the converter's verifier to a MultiVerifier + List verifiers = managementPortalProperties.getOauth() + .getCheckingKeyAliases().stream() + .map(alias -> kf.getKeyPair(alias).getPublic()) + .filter(publicKey -> publicKey instanceof RSAPublicKey + || publicKey instanceof ECPublicKey) + .map(publicKey -> { + if (publicKey instanceof RSAPublicKey) { + return new RsaVerifier((RSAPublicKey) publicKey); + } else { + return new EcdsaVerifier((ECPublicKey) publicKey); + } + }) + .collect(Collectors.toList()); + converter.setVerifier(new MultiVerifier(verifiers)); + return converter; } diff --git a/src/main/java/org/radarcns/management/config/SecurityConfiguration.java b/src/main/java/org/radarcns/management/config/SecurityConfiguration.java index 76b94d118..033d7c7b5 100644 --- a/src/main/java/org/radarcns/management/config/SecurityConfiguration.java +++ b/src/main/java/org/radarcns/management/config/SecurityConfiguration.java @@ -3,8 +3,7 @@ import io.github.jhipster.security.AjaxLogoutSuccessHandler; import io.github.jhipster.security.Http401UnauthorizedEntryPoint; -import javax.annotation.PostConstruct; -import javax.servlet.Filter; +import org.radarcns.auth.authentication.TokenValidator; import org.radarcns.management.security.JwtAuthenticationFilter; import org.springframework.beans.factory.BeanInitializationException; import org.springframework.beans.factory.annotation.Autowired; @@ -27,6 +26,9 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.data.repository.query.SecurityEvaluationContextExtension; +import javax.annotation.PostConstruct; +import javax.servlet.Filter; + @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) @@ -41,6 +43,9 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Autowired private ApplicationEventPublisher applicationEventPublisher; + @Autowired + private ManagementPortalProperties managementPortalProperties; + @PostConstruct public void init() { try { @@ -133,6 +138,8 @@ public FilterRegistrationBean jwtAuthenticationFilterRegistration() { } public Filter jwtAuthenticationFilter() { - return new JwtAuthenticationFilter(); + return new JwtAuthenticationFilter(new TokenValidator( + new LocalKeystoreConfig(managementPortalProperties.getOauth().getKeyStorePassword(), + managementPortalProperties.getOauth().getCheckingKeyAliases()))); } } diff --git a/src/main/java/org/radarcns/management/security/JwtAuthenticationFilter.java b/src/main/java/org/radarcns/management/security/JwtAuthenticationFilter.java index cb4fc4bd8..2f21319f9 100644 --- a/src/main/java/org/radarcns/management/security/JwtAuthenticationFilter.java +++ b/src/main/java/org/radarcns/management/security/JwtAuthenticationFilter.java @@ -2,7 +2,6 @@ import org.radarcns.auth.authentication.TokenValidator; import org.radarcns.auth.exception.TokenValidationException; -import org.radarcns.management.config.LocalKeystoreConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; @@ -24,9 +23,13 @@ public class JwtAuthenticationFilter extends GenericFilterBean { private static final Logger log = LoggerFactory.getLogger(JwtAuthenticationFilter.class); - private static TokenValidator validator = new TokenValidator(new LocalKeystoreConfig()); + private final TokenValidator validator; public static final String TOKEN_ATTRIBUTE = "jwt"; + public JwtAuthenticationFilter(TokenValidator validator) { + this.validator = validator; + } + @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { diff --git a/src/main/java/org/radarcns/management/security/jwt/EcdsaSigner.java b/src/main/java/org/radarcns/management/security/jwt/EcdsaSigner.java new file mode 100644 index 000000000..ede43ff17 --- /dev/null +++ b/src/main/java/org/radarcns/management/security/jwt/EcdsaSigner.java @@ -0,0 +1,43 @@ +package org.radarcns.management.security.jwt; + +import org.springframework.security.jwt.crypto.sign.Signer; + +import java.security.GeneralSecurityException; +import java.security.Signature; +import java.security.interfaces.ECPrivateKey; + +/** + * Class that creates ECDSA signatures for use in Spring Security. + */ +public class EcdsaSigner implements Signer { + + public static final String DEFAULT_ALGORITHM = "SHA256withECDSA"; + private final ECPrivateKey privateKey; + private final String algorithm; + + public EcdsaSigner(ECPrivateKey privateKey) { + this(privateKey, DEFAULT_ALGORITHM); + } + + public EcdsaSigner(ECPrivateKey privateKey, String signingAlgorithm) { + this.privateKey = privateKey; + this.algorithm = signingAlgorithm; + } + + @Override + public byte[] sign(byte[] bytes) { + try { + Signature signature = Signature.getInstance(algorithm); + signature.initSign(privateKey); + signature.update(bytes); + return signature.sign(); + } catch (GeneralSecurityException ex) { + throw new SignatureException("Could not provide a signature", ex); + } + } + + @Override + public String algorithm() { + return algorithm; + } +} diff --git a/src/main/java/org/radarcns/management/security/jwt/EcdsaVerifier.java b/src/main/java/org/radarcns/management/security/jwt/EcdsaVerifier.java new file mode 100644 index 000000000..41e562c7c --- /dev/null +++ b/src/main/java/org/radarcns/management/security/jwt/EcdsaVerifier.java @@ -0,0 +1,43 @@ +package org.radarcns.management.security.jwt; + +import org.springframework.security.jwt.crypto.sign.InvalidSignatureException; +import org.springframework.security.jwt.crypto.sign.SignatureVerifier; + +import java.security.GeneralSecurityException; +import java.security.Signature; +import java.security.interfaces.ECPublicKey; + +public class EcdsaVerifier implements SignatureVerifier { + + private final ECPublicKey publicKey; + private final String algorithm; + + public EcdsaVerifier(ECPublicKey publicKey) { + this(publicKey, EcdsaSigner.DEFAULT_ALGORITHM); + } + + public EcdsaVerifier(ECPublicKey publicKey, String algorithm) { + this.publicKey = publicKey; + this.algorithm = algorithm; + } + + @Override + public void verify(byte[] content, byte[] sig) { + try { + Signature signature = Signature.getInstance(algorithm); + signature.initVerify(publicKey); + signature.update(content); + + if (!signature.verify(sig)) { + throw new InvalidSignatureException("EC Signature did not match content"); + } + } catch (GeneralSecurityException ex) { + throw new SignatureException("An error occured verifying the signature", ex); + } + } + + @Override + public String algorithm() { + return algorithm; + } +} diff --git a/src/main/java/org/radarcns/management/security/jwt/MultiVerifier.java b/src/main/java/org/radarcns/management/security/jwt/MultiVerifier.java new file mode 100644 index 000000000..87b3f1dfe --- /dev/null +++ b/src/main/java/org/radarcns/management/security/jwt/MultiVerifier.java @@ -0,0 +1,42 @@ +package org.radarcns.management.security.jwt; + +import org.springframework.security.jwt.crypto.sign.SignatureVerifier; + +import java.util.LinkedList; +import java.util.List; + +/** + * This is a SignatureVerifier that can accept multiple SignatureVerifier. If the signature + * passes any of the supplied verifiers, the signature is assumed to be valid. + */ +public class MultiVerifier implements SignatureVerifier { + + private final List verifiers = new LinkedList<>(); + + /** + * Construct a MultiVerifier from the given list of SignatureVerifiers. + * @param verifiers the list of verifiers to use + */ + public MultiVerifier(List verifiers) { + this.verifiers.addAll(verifiers); + } + + @Override + public void verify(byte[] content, byte[] signature) { + for (SignatureVerifier verifier : verifiers) { + try { + verifier.verify(content, signature); + return; + } catch (RuntimeException ex) { + // try the next verifier + } + } + throw new SignatureException("Signature could not be verified by any of the registered " + + "verifiers"); + } + + @Override + public String algorithm() { + return null; + } +} diff --git a/src/main/java/org/radarcns/management/security/jwt/RadarJwtAccessTokenConverter.java b/src/main/java/org/radarcns/management/security/jwt/RadarJwtAccessTokenConverter.java new file mode 100644 index 000000000..dfd0cfe8c --- /dev/null +++ b/src/main/java/org/radarcns/management/security/jwt/RadarJwtAccessTokenConverter.java @@ -0,0 +1,97 @@ +package org.radarcns.management.security.jwt; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTCreator; +import com.auth0.jwt.algorithms.Algorithm; +import org.springframework.security.crypto.codec.Base64; +import org.springframework.security.jwt.crypto.sign.RsaSigner; +import org.springframework.security.jwt.crypto.sign.RsaVerifier; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; +import org.springframework.util.Assert; + +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.Map; + +/** + * Customized implementation of {@link JwtAccessTokenConverter} for the RADAR-base platform. + * + * This class can accept an EC keypair as well as an RSA keypair for signing. EC signatures + * are significantly smaller than RSA signatures. + */ +public class RadarJwtAccessTokenConverter extends JwtAccessTokenConverter { + + private Algorithm algorithm; + + @Override + public void setKeyPair(KeyPair keyPair) { + PrivateKey privateKey = keyPair.getPrivate(); + Assert.state(privateKey instanceof RSAPrivateKey || privateKey instanceof ECPrivateKey, + "KeyPair must be an RSA or EC keypair"); + if (privateKey instanceof ECPrivateKey) { + algorithm = Algorithm.ECDSA256((ECPublicKey) keyPair.getPublic(), + (ECPrivateKey) keyPair.getPrivate()); + setSigner(new EcdsaSigner((ECPrivateKey) privateKey)); + ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic(); + setVerifier(new EcdsaVerifier(publicKey)); + setVerifierKey("-----BEGIN EC PUBLIC KEY-----\n" + + new String(Base64.encode(publicKey.getEncoded())) + + "\n-----END EC PUBLIC KEY-----"); + } else { + algorithm = Algorithm.RSA256((RSAPublicKey) keyPair.getPublic(), + (RSAPrivateKey) keyPair.getPrivate()); + setSigner(new RsaSigner((RSAPrivateKey) privateKey)); + RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); + setVerifier(new RsaVerifier(publicKey)); + setVerifierKey("-----BEGIN PUBLIC KEY-----\n" + + new String(Base64.encode(publicKey.getEncoded())) + + "\n-----END PUBLIC KEY-----"); + } + } + + protected String encode(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { + // we need to override the encode method as well, Spring security does not know about + // ECDSA so it can not set the 'alg' header claim of the JWT to the correct value; here + // we use the auth0 JWT implementation to create a signed, encoded JWT. + Map claims = getAccessTokenConverter().convertAccessToken(accessToken, + authentication); + + // create a builder and add the JWT defined claims + JWTCreator.Builder builder = JWT.create(); + + // add the string array claims + Arrays.asList("aud", "sources", "roles", "authorities", "scope").stream() + .filter(claims::containsKey) + .forEach(claim -> + builder.withArrayClaim(claim, + ((Collection) claims.get(claim)).toArray(new String[] {}))); + + // add the string claims + Arrays.asList("sub", "iss", "user_name", "client_id", "grant_type", "jti", "ati").stream() + .filter(claims::containsKey) + .forEach(claim -> builder.withClaim(claim, (String) claims.get(claim))); + + // add the date claims, they are in seconds since epoch, we need milliseconds + Arrays.asList("exp", "iat").stream() + .filter(claims::containsKey) + .forEach(claim -> builder.withClaim(claim, new Date( + ((Long) claims.get(claim)) * 1000))); + + String token = builder.sign(algorithm); + return token; + } + + @Override + public boolean isPublic() { + return true; + } +} diff --git a/src/main/java/org/radarcns/management/security/jwt/RadarKeyStoreKeyFactory.java b/src/main/java/org/radarcns/management/security/jwt/RadarKeyStoreKeyFactory.java new file mode 100644 index 000000000..8cfc7412d --- /dev/null +++ b/src/main/java/org/radarcns/management/security/jwt/RadarKeyStoreKeyFactory.java @@ -0,0 +1,61 @@ +package org.radarcns.management.security.jwt; + +import org.springframework.core.io.Resource; + +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.PublicKey; + +/** + * Similar to Spring's + * {@link org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory}. However + * this class does not assume a specific key type, while the Spring factory assumes RSA keys. + */ +public class RadarKeyStoreKeyFactory { + private final Resource resource; + + private final char[] password; + + private KeyStore store; + + private final Object lock = new Object(); + + public RadarKeyStoreKeyFactory(Resource resource, char[] password) { + this.resource = resource; + this.password = password; + } + + /** + * Get a keypair from the store using the store password. + * @param alias the keypair alias + * @return the keypair + */ + public KeyPair getKeyPair(String alias) { + return getKeyPair(alias, password); + } + + /** + * Get a keypair from the store with a given alias and password. + * @param alias the keypair alias + * @param password the keypair password + * @return the keypair + */ + public KeyPair getKeyPair(String alias, char[] password) { + try { + synchronized (lock) { + if (store == null) { + synchronized (lock) { + store = KeyStore.getInstance("jks"); + store.load(resource.getInputStream(), this.password); + } + } + } + PrivateKey key = (PrivateKey) store.getKey(alias, password); + PublicKey publicKey = store.getCertificate(alias).getPublicKey(); + return new KeyPair(publicKey, key); + } catch (Exception e) { + throw new IllegalStateException("Cannot load keys from store: " + resource, e); + } + } +} diff --git a/src/main/java/org/radarcns/management/security/jwt/SignatureException.java b/src/main/java/org/radarcns/management/security/jwt/SignatureException.java new file mode 100644 index 000000000..9ef77fed0 --- /dev/null +++ b/src/main/java/org/radarcns/management/security/jwt/SignatureException.java @@ -0,0 +1,20 @@ +package org.radarcns.management.security.jwt; + +public class SignatureException extends RuntimeException { + + public SignatureException() { + super(); + } + + public SignatureException(String message) { + super(message); + } + + public SignatureException(String message, Throwable ex) { + super(message, ex); + } + + public SignatureException(Throwable ex) { + super(ex); + } +} diff --git a/src/main/resources/config/application.yml b/src/main/resources/config/application.yml index 0658d0fdb..11ba5fe49 100644 --- a/src/main/resources/config/application.yml +++ b/src/main/resources/config/application.yml @@ -91,6 +91,12 @@ info: managementportal: mail: # specific JHipster mail property, for standard properties see MailProperties from: ManagementPortal@localhost + oauth: + keyStorePassword: radarbase + signingKeyAlias: radarbase-managementportal-ec + checkingKeyAliases: + - radarbase-managementportal-ec + - radarbase-managementportal-rsa # =================================================================== # JHipster specific properties diff --git a/src/test/java/org/radarcns/management/cucumber/stepdefs/UserStepDefs.java b/src/test/java/org/radarcns/management/cucumber/stepdefs/UserStepDefs.java index 79838125c..7d89c468b 100644 --- a/src/test/java/org/radarcns/management/cucumber/stepdefs/UserStepDefs.java +++ b/src/test/java/org/radarcns/management/cucumber/stepdefs/UserStepDefs.java @@ -27,7 +27,7 @@ public class UserStepDefs extends StepDefs { @Before public void setup() throws ServletException { - JwtAuthenticationFilter filter = new JwtAuthenticationFilter(); + JwtAuthenticationFilter filter = OAuthHelper.createAuthenticationFilter(); filter.init(new MockFilterConfig()); this.restUserMockMvc = MockMvcBuilders.standaloneSetup(userResource) .addFilter(filter).defaultRequest(get("/").with(OAuthHelper.bearerToken())).build(); diff --git a/src/test/java/org/radarcns/management/security/JwtAuthenticationFilterIntTest.java b/src/test/java/org/radarcns/management/security/JwtAuthenticationFilterIntTest.java new file mode 100644 index 000000000..fc56999e3 --- /dev/null +++ b/src/test/java/org/radarcns/management/security/JwtAuthenticationFilterIntTest.java @@ -0,0 +1,87 @@ +package org.radarcns.management.security; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.radarcns.management.ManagementPortalTestApp; +import org.radarcns.management.service.ProjectService; +import org.radarcns.management.web.rest.OAuthHelper; +import org.radarcns.management.web.rest.ProjectResource; +import org.radarcns.management.web.rest.errors.ExceptionTranslator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.web.PageableHandlerMethodArgumentResolver; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.mock.web.MockFilterConfig; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = ManagementPortalTestApp.class) +@WithMockUser +public class JwtAuthenticationFilterIntTest { + + @Autowired + private HttpServletRequest servletRequest; + + @Autowired + private ProjectService projectService; + + @Autowired + private MappingJackson2HttpMessageConverter jacksonMessageConverter; + + @Autowired + private PageableHandlerMethodArgumentResolver pageableArgumentResolver; + + @Autowired + private ExceptionTranslator exceptionTranslator; + + private MockMvc rsaRestProjectMockMvc; + + private MockMvc ecRestProjectMockMvc; + + @Before + public void setUp() throws ServletException { + MockitoAnnotations.initMocks(this); + ProjectResource projectResource = new ProjectResource(); + ReflectionTestUtils.setField(projectResource, "projectService", projectService); + ReflectionTestUtils.setField(projectResource, "servletRequest", servletRequest); + + JwtAuthenticationFilter filter = OAuthHelper.createAuthenticationFilter(); + filter.init(new MockFilterConfig()); + + this.rsaRestProjectMockMvc = MockMvcBuilders.standaloneSetup(projectResource) + .setCustomArgumentResolvers(pageableArgumentResolver) + .setControllerAdvice(exceptionTranslator) + .setMessageConverters(jacksonMessageConverter) + .addFilter(filter) + .defaultRequest(get("/").with(OAuthHelper.rsaBearerToken())).build(); + + this.ecRestProjectMockMvc = MockMvcBuilders.standaloneSetup(projectResource) + .setCustomArgumentResolvers(pageableArgumentResolver) + .setControllerAdvice(exceptionTranslator) + .setMessageConverters(jacksonMessageConverter) + .addFilter(filter) + .defaultRequest(get("/").with(OAuthHelper.bearerToken())).build(); + } + + @Test + public void testMultipleSigningKeys() throws Exception { + // Check that we can get the project list with both RSA and EC signed token. We are testing + // acceptance of the tokens, so no test on the content of the response is performed here. + rsaRestProjectMockMvc.perform(get("/api/projects?sort=id,desc")) + .andExpect(status().isOk()); + ecRestProjectMockMvc.perform(get("/api/projects?sort=id,desc")) + .andExpect(status().isOk()); + } +} diff --git a/src/test/java/org/radarcns/management/web/rest/AuditResourceIntTest.java b/src/test/java/org/radarcns/management/web/rest/AuditResourceIntTest.java index f15c73729..7208985f1 100644 --- a/src/test/java/org/radarcns/management/web/rest/AuditResourceIntTest.java +++ b/src/test/java/org/radarcns/management/web/rest/AuditResourceIntTest.java @@ -1,16 +1,5 @@ package org.radarcns.management.web.rest; -import static org.hamcrest.Matchers.hasItem; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -34,6 +23,18 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.transaction.annotation.Transactional; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import static org.hamcrest.Matchers.hasItem; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + /** * Test class for the AuditResource REST controller. * @@ -83,7 +84,7 @@ public void setUp() throws ServletException { ReflectionTestUtils.setField(auditResource, "auditEventService", auditEventService); ReflectionTestUtils.setField(auditResource, "servletRequest", servletRequest); - JwtAuthenticationFilter filter = new JwtAuthenticationFilter(); + JwtAuthenticationFilter filter = OAuthHelper.createAuthenticationFilter(); filter.init(new MockFilterConfig()); this.restAuditMockMvc = MockMvcBuilders.standaloneSetup(auditResource) diff --git a/src/test/java/org/radarcns/management/web/rest/OAuthClientsResourceIntTest.java b/src/test/java/org/radarcns/management/web/rest/OAuthClientsResourceIntTest.java index d5b8fd798..210f9fb0e 100644 --- a/src/test/java/org/radarcns/management/web/rest/OAuthClientsResourceIntTest.java +++ b/src/test/java/org/radarcns/management/web/rest/OAuthClientsResourceIntTest.java @@ -111,7 +111,7 @@ public void setUp() throws Exception { ReflectionTestUtils.setField(oauthClientsResource, "clientDetailsService", clientDetailsService); - JwtAuthenticationFilter filter = new JwtAuthenticationFilter(); + JwtAuthenticationFilter filter = OAuthHelper.createAuthenticationFilter(); filter.init(new MockFilterConfig()); this.restProjectMockMvc = MockMvcBuilders.standaloneSetup(oauthClientsResource) diff --git a/src/test/java/org/radarcns/management/web/rest/OAuthHelper.java b/src/test/java/org/radarcns/management/web/rest/OAuthHelper.java index 621534cb3..aff60c749 100644 --- a/src/test/java/org/radarcns/management/web/rest/OAuthHelper.java +++ b/src/test/java/org/radarcns/management/web/rest/OAuthHelper.java @@ -3,23 +3,34 @@ import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.DecodedJWT; +import org.radarcns.auth.authentication.TokenValidator; +import org.radarcns.auth.authorization.Permission; +import org.radarcns.management.config.LocalKeystoreConfig; +import org.radarcns.management.security.JwtAuthenticationFilter; +import org.springframework.test.web.servlet.request.RequestPostProcessor; + import java.io.InputStream; import java.security.KeyStore; import java.security.cert.Certificate; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import java.time.Instant; +import java.util.Arrays; import java.util.Date; import java.util.stream.Collectors; -import org.radarcns.auth.authorization.Permission; -import org.springframework.test.web.servlet.request.RequestPostProcessor; /** * Created by dverbeec on 29/06/2017. */ public class OAuthHelper { - public static String VALID_TOKEN; + public static String VALID_EC_TOKEN; public static DecodedJWT SUPER_USER_TOKEN; + public static String VALID_RSA_TOKEN; + public static String TEST_KEYSTORE_PASSWORD = "radarbase"; + public static String TEST_SIGNKEY_ALIAS = "ec"; + public static String TEST_CHECKKEY_ALIAS = "selfsigned"; public static final String[] SCOPES = allScopes(); public static final String[] AUTHORITIES = {"ROLE_SYS_ADMIN"}; @@ -45,7 +56,19 @@ public class OAuthHelper { */ public static RequestPostProcessor bearerToken() { return mockRequest -> { - mockRequest.addHeader("Authorization", "Bearer " + VALID_TOKEN); + mockRequest.addHeader("Authorization", "Bearer " + VALID_EC_TOKEN); + return mockRequest; + }; + } + + /** + * Create a request post processor that adds a valid RSA bearer token to requests for use with + * MockMVC. + * @return the request post processor + */ + public static RequestPostProcessor rsaBearerToken() { + return mockRequest -> { + mockRequest.addHeader("Authorization", "Bearer " + VALID_RSA_TOKEN); return mockRequest; }; } @@ -58,27 +81,36 @@ public static void setUp() throws Exception { KeyStore ks = KeyStore.getInstance("JKS"); InputStream keyStream = OAuthHelper.class .getClassLoader().getResourceAsStream("config/keystore.jks"); - ks.load(keyStream, "radarbase".toCharArray()); - RSAPrivateKey privateKey = (RSAPrivateKey) ks.getKey("selfsigned", - "radarbase".toCharArray()); - Certificate cert = ks.getCertificate("selfsigned"); - RSAPublicKey publicKey = (RSAPublicKey) cert.getPublicKey(); + ks.load(keyStream, TEST_KEYSTORE_PASSWORD.toCharArray()); - keyStream.close(); - initVars(Algorithm.RSA256(publicKey, privateKey)); - } + // get the EC keypair for signing + ECPrivateKey privateKey = (ECPrivateKey) ks.getKey(TEST_SIGNKEY_ALIAS, + TEST_KEYSTORE_PASSWORD.toCharArray()); + Certificate cert = ks.getCertificate(TEST_SIGNKEY_ALIAS); + ECPublicKey publicKey = (ECPublicKey) cert.getPublicKey(); - private static void initVars(Algorithm algorithm) { - Instant exp = Instant.now().plusSeconds(30 * 60); - Instant iat = Instant.now(); + // also get an RSA keypair to test accepting multiple keys + RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) ks.getKey(TEST_CHECKKEY_ALIAS, + TEST_KEYSTORE_PASSWORD.toCharArray()); + RSAPublicKey rsaPublicKey = (RSAPublicKey) ks.getCertificate(TEST_CHECKKEY_ALIAS) + .getPublicKey(); - initValidToken(algorithm, exp, iat); + keyStream.close(); + VALID_EC_TOKEN = createValidToken(Algorithm.ECDSA256(publicKey, privateKey)); + SUPER_USER_TOKEN = JWT.decode(VALID_EC_TOKEN); + VALID_RSA_TOKEN = createValidToken(Algorithm.RSA256(rsaPublicKey, rsaPrivateKey)); } + public static JwtAuthenticationFilter createAuthenticationFilter() { + return new JwtAuthenticationFilter(new TokenValidator( + new LocalKeystoreConfig(TEST_KEYSTORE_PASSWORD, Arrays.asList(TEST_SIGNKEY_ALIAS, + TEST_CHECKKEY_ALIAS)))); + } - - private static void initValidToken(Algorithm algorithm, Instant exp, Instant iat) { - VALID_TOKEN = JWT.create() + private static String createValidToken(Algorithm algorithm) { + Instant exp = Instant.now().plusSeconds(30 * 60); + Instant iat = Instant.now(); + return JWT.create() .withIssuer(ISS) .withIssuedAt(Date.from(iat)) .withExpiresAt(Date.from(exp)) @@ -93,7 +125,6 @@ private static void initValidToken(Algorithm algorithm, Instant exp, Instant iat .withClaim("jti", JTI) .withClaim("grant_type", "password") .sign(algorithm); - SUPER_USER_TOKEN = JWT.decode(VALID_TOKEN); } private static String[] allScopes() { diff --git a/src/test/java/org/radarcns/management/web/rest/ProjectResourceIntTest.java b/src/test/java/org/radarcns/management/web/rest/ProjectResourceIntTest.java index 050d5dac5..a7d06b00c 100644 --- a/src/test/java/org/radarcns/management/web/rest/ProjectResourceIntTest.java +++ b/src/test/java/org/radarcns/management/web/rest/ProjectResourceIntTest.java @@ -118,7 +118,7 @@ public void setUp() throws ServletException { ReflectionTestUtils.setField(projectResource, "projectService", projectService); ReflectionTestUtils.setField(projectResource, "servletRequest", servletRequest); - JwtAuthenticationFilter filter = new JwtAuthenticationFilter(); + JwtAuthenticationFilter filter = OAuthHelper.createAuthenticationFilter(); filter.init(new MockFilterConfig()); this.restProjectMockMvc = MockMvcBuilders.standaloneSetup(projectResource) diff --git a/src/test/java/org/radarcns/management/web/rest/SourceDataResourceIntTest.java b/src/test/java/org/radarcns/management/web/rest/SourceDataResourceIntTest.java index bb98000ef..34119134e 100644 --- a/src/test/java/org/radarcns/management/web/rest/SourceDataResourceIntTest.java +++ b/src/test/java/org/radarcns/management/web/rest/SourceDataResourceIntTest.java @@ -109,7 +109,7 @@ public void setUp() throws ServletException { ReflectionTestUtils.setField(sourceDataResource, "sourceDataService", sourceDataService); ReflectionTestUtils.setField(sourceDataResource, "servletRequest", servletRequest); - JwtAuthenticationFilter filter = new JwtAuthenticationFilter(); + JwtAuthenticationFilter filter = OAuthHelper.createAuthenticationFilter(); filter.init(new MockFilterConfig()); this.restSourceDataMockMvc = MockMvcBuilders.standaloneSetup(sourceDataResource) diff --git a/src/test/java/org/radarcns/management/web/rest/SourceResourceIntTest.java b/src/test/java/org/radarcns/management/web/rest/SourceResourceIntTest.java index 669c4f088..6dd56a262 100644 --- a/src/test/java/org/radarcns/management/web/rest/SourceResourceIntTest.java +++ b/src/test/java/org/radarcns/management/web/rest/SourceResourceIntTest.java @@ -106,7 +106,7 @@ public void setUp() throws ServletException { ReflectionTestUtils.setField(sourceResource, "sourceService", sourceService); ReflectionTestUtils.setField(sourceResource, "sourceRepository", sourceRepository); - JwtAuthenticationFilter filter = new JwtAuthenticationFilter(); + JwtAuthenticationFilter filter = OAuthHelper.createAuthenticationFilter(); filter.init(new MockFilterConfig()); this.restDeviceMockMvc = MockMvcBuilders.standaloneSetup(sourceResource) .setCustomArgumentResolvers(pageableArgumentResolver) diff --git a/src/test/java/org/radarcns/management/web/rest/SourceTypeResourceIntTest.java b/src/test/java/org/radarcns/management/web/rest/SourceTypeResourceIntTest.java index e4eed3d39..a0431866a 100644 --- a/src/test/java/org/radarcns/management/web/rest/SourceTypeResourceIntTest.java +++ b/src/test/java/org/radarcns/management/web/rest/SourceTypeResourceIntTest.java @@ -111,7 +111,7 @@ public void setUp() throws ServletException { sourceTypeRepository); ReflectionTestUtils.setField(sourceTypeResource, "servletRequest", servletRequest); - JwtAuthenticationFilter filter = new JwtAuthenticationFilter(); + JwtAuthenticationFilter filter = OAuthHelper.createAuthenticationFilter(); filter.init(new MockFilterConfig()); this.restSourceTypeMockMvc = MockMvcBuilders.standaloneSetup(sourceTypeResource) diff --git a/src/test/java/org/radarcns/management/web/rest/SubjectResourceIntTest.java b/src/test/java/org/radarcns/management/web/rest/SubjectResourceIntTest.java index 30e47b1f7..59ca5dc06 100644 --- a/src/test/java/org/radarcns/management/web/rest/SubjectResourceIntTest.java +++ b/src/test/java/org/radarcns/management/web/rest/SubjectResourceIntTest.java @@ -115,7 +115,7 @@ public void setUp() throws ServletException { ReflectionTestUtils.setField(subjectResource, "sourceTypeService", sourceTypeService); ReflectionTestUtils.setField(subjectResource, "servletRequest", servletRequest); - JwtAuthenticationFilter filter = new JwtAuthenticationFilter(); + JwtAuthenticationFilter filter = OAuthHelper.createAuthenticationFilter(); filter.init(new MockFilterConfig()); this.restSubjectMockMvc = MockMvcBuilders.standaloneSetup(subjectResource) diff --git a/src/test/java/org/radarcns/management/web/rest/UserResourceIntTest.java b/src/test/java/org/radarcns/management/web/rest/UserResourceIntTest.java index dbd90d4fd..c9fe580aa 100644 --- a/src/test/java/org/radarcns/management/web/rest/UserResourceIntTest.java +++ b/src/test/java/org/radarcns/management/web/rest/UserResourceIntTest.java @@ -115,7 +115,7 @@ public void setUp() throws ServletException { ReflectionTestUtils.setField(userResource, "subjectRepository", subjectRepository); ReflectionTestUtils.setField(userResource, "servletRequest", servletRequest); - JwtAuthenticationFilter filter = new JwtAuthenticationFilter(); + JwtAuthenticationFilter filter = OAuthHelper.createAuthenticationFilter(); filter.init(new MockFilterConfig()); this.restUserMockMvc = MockMvcBuilders.standaloneSetup(userResource) diff --git a/src/test/resources/config/application.yml b/src/test/resources/config/application.yml index aabc96e2a..1625bfa48 100644 --- a/src/test/resources/config/application.yml +++ b/src/test/resources/config/application.yml @@ -95,3 +95,11 @@ jhipster: # =================================================================== application: + +managementportal: + oauth: + keyStorePassword: radarbase + signingKeyAlias: ec + checkingKeyAliases: + - ec + - selfsigned diff --git a/src/test/resources/config/keystore.jks b/src/test/resources/config/keystore.jks index 041300b5c..aa6d01b64 100644 Binary files a/src/test/resources/config/keystore.jks and b/src/test/resources/config/keystore.jks differ