From bb7306ce7d0ebdcb434b87b583865ca4e2d62044 Mon Sep 17 00:00:00 2001 From: Denny Verbeeck Date: Thu, 7 Jun 2018 15:20:27 +0200 Subject: [PATCH 01/15] Add approriate http status code and response when a bad token is used --- .../radarcns/management/security/JwtAuthenticationFilter.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/radarcns/management/security/JwtAuthenticationFilter.java b/src/main/java/org/radarcns/management/security/JwtAuthenticationFilter.java index 06f69ed41..cb4fc4bd8 100644 --- a/src/main/java/org/radarcns/management/security/JwtAuthenticationFilter.java +++ b/src/main/java/org/radarcns/management/security/JwtAuthenticationFilter.java @@ -42,6 +42,10 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha chain.doFilter(request, response); } catch (TokenValidationException ex) { log.error(ex.getMessage()); + HttpServletResponse res = (HttpServletResponse) response; + res.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + res.setHeader(HttpHeaders.WWW_AUTHENTICATE, OAuth2AccessToken.BEARER_TYPE); + res.getOutputStream().println("{\"error\": \"" + ex.getMessage() + "\"}"); } } From b06e8df89258e135fe838e392289a0d77fab591c Mon Sep 17 00:00:00 2001 From: Denny Verbeeck Date: Thu, 7 Jun 2018 16:19:43 +0200 Subject: [PATCH 02/15] Allow for multiple public keys and multiple public key endpoints in radar-auth Also allow for EC public keys next to RSA public keys. This will allow clients of the radar-auth library to use any combination of public keys and server public key endpoints, increasing the flexibility. This will also be necessary when MP switches to EC public keys, since there will be a transition period where there are clients with RSA signatures and clients with EC signatures. --- radar-auth/README.md | 31 ++-- .../auth/authentication/TokenValidator.java | 152 +++++++++++------- .../radarcns/auth/config/ServerConfig.java | 11 +- .../auth/config/YamlServerConfig.java | 97 ++++++----- .../authentication/TokenValidatorTest.java | 4 +- .../auth/config/YamlServerConfigTest.java | 42 ++--- .../radarcns/auth/util/TokenTestUtils.java | 6 +- radar-auth/src/test/resources/radar-is-2.yml | 13 +- radar-auth/src/test/resources/radar-is.yml | 13 +- 9 files changed, 225 insertions(+), 144 deletions(-) diff --git a/radar-auth/README.md b/radar-auth/README.md index c1f104130..6a61cbbcb 100644 --- a/radar-auth/README.md +++ b/radar-auth/README.md @@ -15,30 +15,37 @@ compile group: 'org.radarcns', name: 'radar-auth', version: '0.3.6' The library expects the identity server configuration in a file called `radar-is.yml`. Either set the environment variable `RADAR_IS_CONFIG_LOCATION` to the full path of the file, or put the file -somewhere on the classpath. The file should define `resourceName` and either of `publicKeyEndpoint` -or `publicKey`. If both are specified, `publicKey` has the priority. +somewhere on the classpath. The file should define `resourceName` and at least one of +`publicKeyEndpoints` or `publicKeys`. You can specify both, than public keys fetched from the +endpoints as well as public keys defined in the file will be used for validation. -| Variable name | Description | -|---------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `resourceName` | The name of this resource. It has to appear in the `audience` claim of a JWT token in order for the token to be accepted. | -| `publicKeyEndpoint` | Server endpoint that provides the public key of the keypair used to sign the JWTs. The expected response from this endpoint is a JSON structure containing two fields: `alg` and `value`, where `alg` should be equal to `SHA256withRSA`, and `value` should be equal to the public key in PEM format. | -| `publicKey` | PEM formatted public key for JWT validation. You can use YAML [literal style] to conveniently specify a multiline value for a variable. Also handy for testing scenario's where you don't necessarily have access to the public key endpoint. | +| Variable name | Description | +|---------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `resourceName` | The name of this resource. It has to appear in the `audience` claim of a JWT token in order for the token to be accepted. | +| `publicKeyEndpoints` | List of server endpoints that provide the public key of the keypair used to sign the JWTs. The expected response from this endpoint is a JSON structure containing two fields: `alg` and `value`, where `alg` should be equal to `SHA256withRSA` or ``SHA256withECDSA`, and `value` should be equal to the public key in PEM format. | +| `publicKeys` | List of PEM formatted public keys for JWT validation. You can use YAML [literal style] to conveniently specify a multiline value for a variable. Also handy for testing scenario's where you don't necessarily have access to the public key endpoint. Entries can be RSA or EC public keys. | For example: ```yaml resourceName: resource_name -publicKeyEndpoint: http://localhost:8080/oauth/token_key +publicKeyEndpoints: + - http://localhost:8080/oauth/token_key ``` or ```yaml resourceName: res_ManagementPortal -publicKey: |- - -----BEGIN PUBLIC KEY----- - MIICHDANBgkqhkiG9w0BAQEFAAOCAgkAMIICBAKCAfsAqM4o+hVAdF2QATQBmpehSMyhdqKvwh9mrfnxDNtctZYlpiQXMbq4uqRgp98aBy6bMKKr3k0rSXTzr27Y+tdLUWXqbl4y8kKm8rGZo9gTbPyhqPm4f4OIxMRJcuhQ7f8qBY87w9buzClQeUs3h5f+DUVRUfB9FnDtim+ma3mFqYh38TMnrBapCtG+7iVKRFgGv6JWiNTql+oVBPNuUX3koc5/zO6IhrD49vBbsjaRWTJV2xMNll82gPvVLtgQNA2t7iGnUPhfKDj1NInZeg79NzFnWAa9Jtc1r2Q7D68MiJhYZN2QAlZS1GfbELnRAeUmSxT5i3BHu23iz9zluhIhYe1vhA1QWk2HsriGL9w+iFqzYlk5P3GCAE+nfNmM/6GIp1ehzW+/4+xgik5rOakCWw4vewmSBWOrV/XZvT2ZT3AA6zIByWdERyMOVJmd9rqPH1FIDtQk8h2jFTqIvBda727DHXeUB9J4hHQTzQmvOxPMipwDslxWOjnG4nbq6Exme0o/ELMOxt+4APH6KW+LqCNl5jGdbKxySLQyNgfUjhXJ06U1b8JHPheTnWcKO+cMmhyheUkZmLMLK2mlAsR+JJeBDY1/jd7+q6hgymeJzoDoXJj4LARiYZ+StRr/E0+P8DrprWYZPi496VIzwgV8otV9fVz29V501rcCAwEAAQ== - -----END PUBLIC KEY----- +publicKeys: + - |- + -----BEGIN PUBLIC KEY----- + MIICHDANBgkqhkiG9w0BAQEFAAOCAgkAMIICBAKCAfsAqM4o+hVAdF2QATQBmpehSMyhdqKvwh9mrfnxDNtctZYlpiQXMbq4uqRgp98aBy6bMKKr3k0rSXTzr27Y+tdLUWXqbl4y8kKm8rGZo9gTbPyhqPm4f4OIxMRJcuhQ7f8qBY87w9buzClQeUs3h5f+DUVRUfB9FnDtim+ma3mFqYh38TMnrBapCtG+7iVKRFgGv6JWiNTql+oVBPNuUX3koc5/zO6IhrD49vBbsjaRWTJV2xMNll82gPvVLtgQNA2t7iGnUPhfKDj1NInZeg79NzFnWAa9Jtc1r2Q7D68MiJhYZN2QAlZS1GfbELnRAeUmSxT5i3BHu23iz9zluhIhYe1vhA1QWk2HsriGL9w+iFqzYlk5P3GCAE+nfNmM/6GIp1ehzW+/4+xgik5rOakCWw4vewmSBWOrV/XZvT2ZT3AA6zIByWdERyMOVJmd9rqPH1FIDtQk8h2jFTqIvBda727DHXeUB9J4hHQTzQmvOxPMipwDslxWOjnG4nbq6Exme0o/ELMOxt+4APH6KW+LqCNl5jGdbKxySLQyNgfUjhXJ06U1b8JHPheTnWcKO+cMmhyheUkZmLMLK2mlAsR+JJeBDY1/jd7+q6hgymeJzoDoXJj4LARiYZ+StRr/E0+P8DrprWYZPi496VIzwgV8otV9fVz29V501rcCAwEAAQ== + -----END PUBLIC KEY----- + - |- + -----BEGIN EC PUBLIC KEY----- + MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvmBia5inhASHAVrFBB5JAh0ne/aKb6z/sCIuWzKp/azFcD/OPJ2H6RPLn3t7XA4oAa2FR3GB4ZhU7SCh20FUhA== + -----END EC PUBLIC KEY----- ``` Usage 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 4d9b682f2..62be0efdd 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 @@ -8,26 +8,31 @@ import com.auth0.jwt.interfaces.DecodedJWT; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import org.bouncycastle.util.io.pem.PemReader; +import org.radarcns.auth.config.ServerConfig; +import org.radarcns.auth.config.YamlServerConfig; +import org.radarcns.auth.exception.TokenValidationException; +import org.radarcns.auth.token.JwtRadarToken; +import org.radarcns.auth.token.RadarToken; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.InputStream; import java.io.StringReader; +import java.net.URI; import java.net.URLConnection; import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAPublicKey; import java.security.spec.X509EncodedKeySpec; import java.time.Duration; import java.time.Instant; import java.util.Arrays; +import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.stream.Collectors; -import org.bouncycastle.util.io.pem.PemReader; -import org.radarcns.auth.config.ServerConfig; -import org.radarcns.auth.config.YamlServerConfig; -import org.radarcns.auth.exception.TokenValidationException; -import org.radarcns.auth.token.JwtRadarToken; -import org.radarcns.auth.token.RadarToken; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Validates JWT token signed by the Management Portal. It is synchronized and may be used from @@ -44,7 +49,7 @@ public class TokenValidator { JwtRadarToken.GRANT_TYPE_CLAIM, JwtRadarToken.SCOPE_CLAIM); private final ServerConfig config; - private JWTVerifier verifier; + private List verifiers = new LinkedList<>(); // If a client presents a token with an invalid signature, it might be the keypair was changed. // In that case we need to fetch it again, but we don't want a malicious client to be able to @@ -110,39 +115,41 @@ public TokenValidator(ServerConfig config, long fetchTimeout) { * @throws TokenValidationException If the token can not be validated. */ public RadarToken validateAccessToken(String token) throws TokenValidationException { - try { - DecodedJWT jwt = getVerifier().verify(token); - Set claims = jwt.getClaims().keySet(); - Set missing = REQUIRED_CLAIMS.stream() - .filter(c -> !claims.contains(c)) - .collect(Collectors.toSet()); - if (!missing.isEmpty()) { - throw new TokenValidationException("The following required claims were missing " - + "from the token: " + String.join(", ", missing)); + for (JWTVerifier verifier : getVerifiers()) { + try { + DecodedJWT jwt = verifier.verify(token); + Set claims = jwt.getClaims().keySet(); + Set missing = REQUIRED_CLAIMS.stream() + .filter(c -> !claims.contains(c)).collect(Collectors.toSet()); + if (!missing.isEmpty()) { + throw new TokenValidationException("The following required claims were " + + "missing from the token: " + String.join(", ", missing)); + } + return new JwtRadarToken(jwt); + } catch (SignatureVerificationException sve) { + log.warn("Client presented a token with an incorrect signature, fetching public key" + + " again. Token: {}", token); + refresh(); + return validateAccessToken(token); + } catch (JWTVerificationException ex) { + throw new TokenValidationException(ex); } - return new JwtRadarToken(jwt); - } catch (SignatureVerificationException sve) { - log.warn("Client presented a token with an incorrect signature, fetching public key" - + " again. Token: {}", token); - refresh(); - return validateAccessToken(token); - } catch (JWTVerificationException ex) { - throw new TokenValidationException(ex); } + throw new TokenValidationException("No registered validator could authenticate this token"); } - private JWTVerifier getVerifier() { + private List getVerifiers() { synchronized (this) { - if (verifier != null) { - return verifier; + if (!verifiers.isEmpty()) { + return verifiers; } } - JWTVerifier localVerifier = loadVerifier(); + List localVerifiers = loadVerifiers(); synchronized (this) { - verifier = localVerifier; - return verifier; + verifiers = localVerifiers; + return verifiers; } } @@ -151,13 +158,13 @@ private JWTVerifier getVerifier() { * @throws TokenValidationException if the public key could not be refreshed. */ public void refresh() throws TokenValidationException { - JWTVerifier localVerifier = loadVerifier(); + List localVerifiers = loadVerifiers(); synchronized (this) { - this.verifier = localVerifier; + this.verifiers = localVerifiers; } } - private JWTVerifier loadVerifier() throws TokenValidationException { + private List loadVerifiers() throws TokenValidationException { synchronized (this) { // whether successful or not, do not request the key more than once per minute if (Instant.now().isBefore(lastFetch.plus(fetchTimeout))) { @@ -169,47 +176,49 @@ private JWTVerifier loadVerifier() throws TokenValidationException { lastFetch = Instant.now(); } - RSAPublicKey publicKey; - if (config.getPublicKey() == null) { - publicKey = publicKeyFromServer(); - } else { - publicKey = config.getPublicKey(); + List publicKeys = new LinkedList<>(); + if (config.getPublicKeyEndpoints() != null) { + publicKeys.addAll(config.getPublicKeyEndpoints().stream() + .map(this::publicKeyFromServer).collect(Collectors.toList())); } - Algorithm alg = Algorithm.RSA256(publicKey, null); - // we successfully fetched the public key, reset the timer - return JWT.require(alg) + if (config.getPublicKeys() != null) { + publicKeys.addAll(config.getPublicKeys()); + } + + // Create a verifier for each public key we have in our config + return publicKeys.stream().map(key -> JWT.require(publicKeyToAlgorithm(key)) .withAudience(config.getResourceName()) - .build(); + .build()) + .collect(Collectors.toList()); } - private RSAPublicKey publicKeyFromServer() throws TokenValidationException { - log.info("Getting the JWT public key at " + config.getPublicKeyEndpoint()); - + private PublicKey publicKeyFromServer(URI serverUri) throws TokenValidationException { + log.info("Getting the JWT public key at " + serverUri); try { - URLConnection connection = config.getPublicKeyEndpoint().toURL().openConnection(); + URLConnection connection = serverUri.toURL().openConnection(); connection.setRequestProperty("Accept", "application/json"); try (InputStream inputStream = connection.getInputStream()) { ObjectMapper mapper = new ObjectMapper(); JsonNode publicKeyInfo = mapper.readTree(inputStream); - - // We expect RSA algorithm, and deny to trust the public key otherwise, see also + // We expect RSA or ECDSA algorithm, and deny to trust the public key otherwise // https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/ - if (!publicKeyInfo.get("alg").asText().equals("SHA256withRSA")) { - throw new TokenValidationException("The identity server reported the following " - + "signing algorithm: " + publicKeyInfo.get("alg") - + ". Expected SHA256withRSA."); + if (publicKeyInfo.get("alg").asText().equals("SHA256withRSA")) { + return rsaPublicKeyFromString(publicKeyInfo.get("value").asText()); } - - String keyString = publicKeyInfo.get("value").asText(); - return publicKeyFromString(keyString); + if (publicKeyInfo.get("alg").asText().equals("SHA256withECDSA")) { + return ecPublicKeyFromString(publicKeyInfo.get("value").asText()); + } + throw new TokenValidationException("The identity server reported the following " + + "signing algorithm: " + publicKeyInfo.get("alg") + + ". Expected SHA256withRSA or SHA256withECDSA."); } } catch (Exception ex) { throw new TokenValidationException(ex); } } - private RSAPublicKey publicKeyFromString(String keyString) throws TokenValidationException { - log.debug("Parsing public key: " + keyString); + private RSAPublicKey rsaPublicKeyFromString(String keyString) throws TokenValidationException { + log.debug("Parsing RSA public key: " + keyString); try (PemReader pemReader = new PemReader(new StringReader(keyString))) { byte[] keyBytes = pemReader.readPemObject().getContent(); pemReader.close(); @@ -220,4 +229,29 @@ private RSAPublicKey publicKeyFromString(String keyString) throws TokenValidatio throw new TokenValidationException(ex); } } + + private ECPublicKey ecPublicKeyFromString(String keyString) throws TokenValidationException { + log.debug("Parsing EC public key: " + keyString); + try (PemReader pemReader = new PemReader(new StringReader(keyString))) { + byte[] keyBytes = pemReader.readPemObject().getContent(); + pemReader.close(); + X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); + KeyFactory kf = KeyFactory.getInstance("ECDSA"); + return (ECPublicKey) kf.generatePublic(spec); + } catch (Exception ex) { + throw new TokenValidationException(ex); + } + } + + private Algorithm publicKeyToAlgorithm(PublicKey key) throws IllegalArgumentException { + switch (key.getAlgorithm()) { + case "EC": + return Algorithm.ECDSA256((ECPublicKey) key, null); + case "RSA": + return Algorithm.RSA256((RSAPublicKey) key, null); + default: + throw new IllegalArgumentException("Unsupported public key algorithm: {}. " + + "Expected either EC or RSA."); + } + } } diff --git a/radar-auth/src/main/java/org/radarcns/auth/config/ServerConfig.java b/radar-auth/src/main/java/org/radarcns/auth/config/ServerConfig.java index 476c9f0b0..9f0035427 100644 --- a/radar-auth/src/main/java/org/radarcns/auth/config/ServerConfig.java +++ b/radar-auth/src/main/java/org/radarcns/auth/config/ServerConfig.java @@ -1,7 +1,8 @@ package org.radarcns.auth.config; import java.net.URI; -import java.security.interfaces.RSAPublicKey; +import java.security.PublicKey; +import java.util.List; public interface ServerConfig { @@ -9,7 +10,7 @@ public interface ServerConfig { * Get the public key endpoint as a URI. * @return The public key endpoint URI, or null if not defined */ - URI getPublicKeyEndpoint(); + List getPublicKeyEndpoints(); /** * The name of this resource. It should be in the list of allowed resources for the OAuth @@ -19,9 +20,9 @@ public interface ServerConfig { String getResourceName(); /** - * Get the public key set in the config file. - * @return The public key, or null if not defined + * Get the public keys set in the config file. + * @return The public keys, or null if not defined */ - RSAPublicKey getPublicKey(); + List getPublicKeys(); } diff --git a/radar-auth/src/main/java/org/radarcns/auth/config/YamlServerConfig.java b/radar-auth/src/main/java/org/radarcns/auth/config/YamlServerConfig.java index 86854b7bf..90ae8afa5 100644 --- a/radar-auth/src/main/java/org/radarcns/auth/config/YamlServerConfig.java +++ b/radar-auth/src/main/java/org/radarcns/auth/config/YamlServerConfig.java @@ -15,8 +15,14 @@ import java.net.URI; import java.net.URL; import java.security.KeyFactory; -import java.security.interfaces.RSAPublicKey; +import java.security.PublicKey; import java.security.spec.X509EncodedKeySpec; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; /** * Created by dverbeec on 14/06/2017. @@ -24,14 +30,23 @@ public class YamlServerConfig implements ServerConfig { public static final String LOCATION_ENV = "RADAR_IS_CONFIG_LOCATION"; public static final String CONFIG_FILE_NAME = "radar-is.yml"; - private URI publicKeyEndpoint; + private List publicKeyEndpoints = new LinkedList<>(); private String resourceName; - private RSAPublicKey publicKey; + private List publicKeys = new LinkedList<>(); private static YamlServerConfig config; private final Logger log = LoggerFactory.getLogger(YamlServerConfig.class); + // a map with as key the string to search for in a PEM encoded public key, and as value the + // KeyFactory type to request + private final Map keyFactoryTypes = new HashMap<>(); + + /** + * Default constructor. Initializes the keyFactoryTypes map. + */ public YamlServerConfig() { + keyFactoryTypes.put("-----BEGIN PUBLIC KEY-----", "RSA"); + keyFactoryTypes.put("-----BEGIN EC PUBLIC KEY-----", "EC"); log.info("YamlServerConfig initializing..."); } @@ -85,13 +100,13 @@ public static YamlServerConfig reloadConfig() { return readFromFileOrClasspath(); } - public URI getPublicKeyEndpoint() { - return publicKeyEndpoint; + public List getPublicKeyEndpoints() { + return publicKeyEndpoints; } - public void setPublicKeyEndpoint(URI publicKeyEndpoint) { - log.info("Token public key endpoint set to " + publicKeyEndpoint.toString()); - this.publicKeyEndpoint = publicKeyEndpoint; + public void setPublicKeyEndpoints(List publicKeyEndpoints) { + log.info("Token public key endpoints set to " + publicKeyEndpoints.toString()); + this.publicKeyEndpoints = publicKeyEndpoints; } @Override @@ -100,8 +115,8 @@ public String getResourceName() { } @Override - public RSAPublicKey getPublicKey() { - return publicKey; + public List getPublicKeys() { + return publicKeys; } public void setResourceName(String resourceName) { @@ -109,44 +124,52 @@ public void setResourceName(String resourceName) { } /** - * Set the public key. This method converts the public key from a PEM formatted string to a - * {@link RSAPublicKey} format. - * @param publicKey The PEM formatted public key + * Set the public keys. This method will detect the public key type (EC or RSA) and parse + * accordingly. + * @param publicKeys The public keys to parse */ - public void setPublicKey(String publicKey) { - log.debug("Parsing public key: " + publicKey); - try (PemReader pemReader = new PemReader(new StringReader(publicKey))) { - byte[] keyBytes = pemReader.readPemObject().getContent(); - pemReader.close(); - X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); - KeyFactory kf = KeyFactory.getInstance("RSA"); - this.publicKey = (RSAPublicKey) kf.generatePublic(spec); - } catch (Exception ex) { - throw new ConfigurationException(ex); - } + public void setPublicKeys(List publicKeys) { + this.publicKeys = publicKeys.stream().map(this::parseKey).collect(Collectors.toList()); } @Override - public boolean equals(Object other) { - if (this == other) { + public boolean equals(Object o) { + if (this == o) { return true; } - if (!(other instanceof YamlServerConfig)) { + if (!(o instanceof YamlServerConfig)) { return false; } - - YamlServerConfig that = (YamlServerConfig) other; - - if (!publicKeyEndpoint.equals(that.publicKeyEndpoint)) { - return false; - } - return resourceName.equals(that.resourceName); + YamlServerConfig that = (YamlServerConfig) o; + return Objects.equals(publicKeyEndpoints, that.publicKeyEndpoints) + && Objects.equals(resourceName, that.resourceName) + && Objects.equals(publicKeys, that.publicKeys); } @Override public int hashCode() { - int result = publicKeyEndpoint.hashCode(); - result = 31 * result + resourceName.hashCode(); - return result; + return Objects.hash(publicKeyEndpoints, resourceName, publicKeys); + } + + private PublicKey parseKey(String publicKey) { + String factoryType = keyFactoryTypes.keySet().stream() + // find the string that is contained in publicKey + .filter(publicKey::contains) + .findFirst() + // get the actual factory type + .map(keyFactoryTypes::get) + // if not found throw a ConfigurationException + .orElseThrow(() -> new ConfigurationException("Unsupported public key: " + + publicKey)); + log.debug("Parsing {} public key: {}", factoryType, publicKey); + try (PemReader pemReader = new PemReader(new StringReader(publicKey))) { + byte[] keyBytes = pemReader.readPemObject().getContent(); + pemReader.close(); + X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); + KeyFactory kf = KeyFactory.getInstance(factoryType); + return kf.generatePublic(spec); + } catch (Exception ex) { + throw new ConfigurationException(ex); + } } } diff --git a/radar-auth/src/test/java/org/radarcns/auth/authentication/TokenValidatorTest.java b/radar-auth/src/test/java/org/radarcns/auth/authentication/TokenValidatorTest.java index 418d349eb..0d7a44044 100644 --- a/radar-auth/src/test/java/org/radarcns/auth/authentication/TokenValidatorTest.java +++ b/radar-auth/src/test/java/org/radarcns/auth/authentication/TokenValidatorTest.java @@ -52,7 +52,7 @@ public void setUp() throws Exception { @Test public void testValidToken() { - validator.validateAccessToken(TokenTestUtils.VALID_TOKEN); + validator.validateAccessToken(TokenTestUtils.VALID_RSA_TOKEN); } @Test(expected = TokenValidationException.class) @@ -77,6 +77,6 @@ public void testPublicKeyFromConfigFile() { environmentVariables.set(YamlServerConfig.LOCATION_ENV, configFile.getAbsolutePath()); // reinitialize TokenValidator to pick up new config validator = new TokenValidator(); - validator.validateAccessToken(TokenTestUtils.VALID_TOKEN); + validator.validateAccessToken(TokenTestUtils.VALID_RSA_TOKEN); } } diff --git a/radar-auth/src/test/java/org/radarcns/auth/config/YamlServerConfigTest.java b/radar-auth/src/test/java/org/radarcns/auth/config/YamlServerConfigTest.java index 1302109cf..6523734ce 100644 --- a/radar-auth/src/test/java/org/radarcns/auth/config/YamlServerConfigTest.java +++ b/radar-auth/src/test/java/org/radarcns/auth/config/YamlServerConfigTest.java @@ -1,16 +1,19 @@ package org.radarcns.auth.config; -import org.apache.commons.codec.binary.Base64; import org.junit.Rule; import org.junit.Test; import org.junit.contrib.java.lang.system.EnvironmentVariables; -import org.radarcns.auth.util.TokenTestUtils; import java.io.File; -import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.stream.Collectors; +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; + /** * Created by dverbeec on 19/06/2017. @@ -21,33 +24,30 @@ public class YamlServerConfigTest { public final EnvironmentVariables environmentVariables = new EnvironmentVariables(); @Test - public void testLoadYamlFileFromClasspath() throws IOException { + public void testLoadYamlFileFromClasspath() throws URISyntaxException { ServerConfig config = YamlServerConfig.readFromFileOrClasspath(); - assertEquals("http://localhost:8089/oauth/token_key", config.getPublicKeyEndpoint().toString()); - assertEquals("unit_test", config.getResourceName()); + checkConfig(config); } @Test - public void testLoadYamlFileFromEnv() throws IOException { + public void testLoadYamlFileFromEnv() throws URISyntaxException { ClassLoader loader = getClass().getClassLoader(); File configFile = new File(loader.getResource(YamlServerConfig.CONFIG_FILE_NAME).getFile()); environmentVariables.set(YamlServerConfig.LOCATION_ENV, configFile.getAbsolutePath()); ServerConfig config = YamlServerConfig.readFromFileOrClasspath(); - assertEquals("http://localhost:8089/oauth/token_key", config.getPublicKeyEndpoint().toString()); - assertNull(config.getPublicKey()); - assertEquals("unit_test", config.getResourceName()); + checkConfig(config); } - @Test - public void testLoadYamlFileWithPublicKey() throws Exception { - ClassLoader loader = getClass().getClassLoader(); - File configFile = new File(loader.getResource("radar-is-2.yml").getFile()); - environmentVariables.set(YamlServerConfig.LOCATION_ENV, configFile.getAbsolutePath()); - ServerConfig config = YamlServerConfig.readFromFileOrClasspath(); - TokenTestUtils.setUp(); - assertEquals(TokenTestUtils.PUBLIC_KEY_STRING, new String(new Base64().encode(config - .getPublicKey().getEncoded()))); - assertNull(config.getPublicKeyEndpoint()); + private void checkConfig(ServerConfig config) throws URISyntaxException { + List uris = config.getPublicKeyEndpoints(); + assertThat(uris, hasItems(new URI("http://localhost:8089/oauth/token_key"), + new URI("http://localhost:8089/oauth/token_key"))); + assertEquals(2, uris.size()); assertEquals("unit_test", config.getResourceName()); + assertEquals(2, config.getPublicKeys().size()); + List algs = config.getPublicKeys().stream() + .map(key -> key.getAlgorithm()) + .collect(Collectors.toList()); + assertThat(algs, hasItems("RSA", "EC")); } } diff --git a/radar-auth/src/test/java/org/radarcns/auth/util/TokenTestUtils.java b/radar-auth/src/test/java/org/radarcns/auth/util/TokenTestUtils.java index dcb66c435..a7642c821 100644 --- a/radar-auth/src/test/java/org/radarcns/auth/util/TokenTestUtils.java +++ b/radar-auth/src/test/java/org/radarcns/auth/util/TokenTestUtils.java @@ -21,7 +21,7 @@ public class TokenTestUtils { public static final String PUBLIC_KEY = "/oauth/token_key"; public static String PUBLIC_KEY_BODY; - public static String VALID_TOKEN; + public static String VALID_RSA_TOKEN; public static String INCORRECT_AUDIENCE_TOKEN; public static String EXPIRED_TOKEN; public static String INCORRECT_ALGORITHM_TOKEN; @@ -189,7 +189,7 @@ private static void initProjectAdminToken(Algorithm algorithm, Instant exp, Inst } private static void initValidToken(Algorithm algorithm, Instant exp, Instant iat) { - VALID_TOKEN = JWT.create() + VALID_RSA_TOKEN = JWT.create() .withIssuer(ISS) .withIssuedAt(Date.from(iat)) .withExpiresAt(Date.from(exp)) @@ -204,7 +204,7 @@ 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); + SUPER_USER_TOKEN = JWT.decode(VALID_RSA_TOKEN); } private static void initTokenWithScopes(Algorithm algorithm, Instant exp, Instant iat) { diff --git a/radar-auth/src/test/resources/radar-is-2.yml b/radar-auth/src/test/resources/radar-is-2.yml index 3b9427f72..52feaa416 100644 --- a/radar-auth/src/test/resources/radar-is-2.yml +++ b/radar-auth/src/test/resources/radar-is-2.yml @@ -1,5 +1,10 @@ resourceName: unit_test -publicKey: |- - -----BEGIN PUBLIC KEY----- - MIICHDANBgkqhkiG9w0BAQEFAAOCAgkAMIICBAKCAfsAqM4o+hVAdF2QATQBmpehSMyhdqKvwh9mrfnxDNtctZYlpiQXMbq4uqRgp98aBy6bMKKr3k0rSXTzr27Y+tdLUWXqbl4y8kKm8rGZo9gTbPyhqPm4f4OIxMRJcuhQ7f8qBY87w9buzClQeUs3h5f+DUVRUfB9FnDtim+ma3mFqYh38TMnrBapCtG+7iVKRFgGv6JWiNTql+oVBPNuUX3koc5/zO6IhrD49vBbsjaRWTJV2xMNll82gPvVLtgQNA2t7iGnUPhfKDj1NInZeg79NzFnWAa9Jtc1r2Q7D68MiJhYZN2QAlZS1GfbELnRAeUmSxT5i3BHu23iz9zluhIhYe1vhA1QWk2HsriGL9w+iFqzYlk5P3GCAE+nfNmM/6GIp1ehzW+/4+xgik5rOakCWw4vewmSBWOrV/XZvT2ZT3AA6zIByWdERyMOVJmd9rqPH1FIDtQk8h2jFTqIvBda727DHXeUB9J4hHQTzQmvOxPMipwDslxWOjnG4nbq6Exme0o/ELMOxt+4APH6KW+LqCNl5jGdbKxySLQyNgfUjhXJ06U1b8JHPheTnWcKO+cMmhyheUkZmLMLK2mlAsR+JJeBDY1/jd7+q6hgymeJzoDoXJj4LARiYZ+StRr/E0+P8DrprWYZPi496VIzwgV8otV9fVz29V501rcCAwEAAQ== - -----END PUBLIC KEY----- +publicKeys: + - |- + -----BEGIN PUBLIC KEY----- + MIICHDANBgkqhkiG9w0BAQEFAAOCAgkAMIICBAKCAfsAqM4o+hVAdF2QATQBmpehSMyhdqKvwh9mrfnxDNtctZYlpiQXMbq4uqRgp98aBy6bMKKr3k0rSXTzr27Y+tdLUWXqbl4y8kKm8rGZo9gTbPyhqPm4f4OIxMRJcuhQ7f8qBY87w9buzClQeUs3h5f+DUVRUfB9FnDtim+ma3mFqYh38TMnrBapCtG+7iVKRFgGv6JWiNTql+oVBPNuUX3koc5/zO6IhrD49vBbsjaRWTJV2xMNll82gPvVLtgQNA2t7iGnUPhfKDj1NInZeg79NzFnWAa9Jtc1r2Q7D68MiJhYZN2QAlZS1GfbELnRAeUmSxT5i3BHu23iz9zluhIhYe1vhA1QWk2HsriGL9w+iFqzYlk5P3GCAE+nfNmM/6GIp1ehzW+/4+xgik5rOakCWw4vewmSBWOrV/XZvT2ZT3AA6zIByWdERyMOVJmd9rqPH1FIDtQk8h2jFTqIvBda727DHXeUB9J4hHQTzQmvOxPMipwDslxWOjnG4nbq6Exme0o/ELMOxt+4APH6KW+LqCNl5jGdbKxySLQyNgfUjhXJ06U1b8JHPheTnWcKO+cMmhyheUkZmLMLK2mlAsR+JJeBDY1/jd7+q6hgymeJzoDoXJj4LARiYZ+StRr/E0+P8DrprWYZPi496VIzwgV8otV9fVz29V501rcCAwEAAQ== + -----END PUBLIC KEY----- + - |- + -----BEGIN EC PUBLIC KEY----- + MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvmBia5inhASHAVrFBB5JAh0ne/aKb6z/sCIuWzKp/azFcD/OPJ2H6RPLn3t7XA4oAa2FR3GB4ZhU7SCh20FUhA== + -----END EC PUBLIC KEY----- diff --git a/radar-auth/src/test/resources/radar-is.yml b/radar-auth/src/test/resources/radar-is.yml index 4947233d1..990dbd032 100644 --- a/radar-auth/src/test/resources/radar-is.yml +++ b/radar-auth/src/test/resources/radar-is.yml @@ -1,2 +1,13 @@ resourceName: unit_test -publicKeyEndpoint: http://localhost:8089/oauth/token_key +publicKeyEndpoints: + - http://localhost:8089/oauth/token_key + - http://localhost:8089/oauth/token_key # in our tests we only have one wiremock port open, so we have two times the same uri +publicKeys: + - |- + -----BEGIN PUBLIC KEY----- + MIICHDANBgkqhkiG9w0BAQEFAAOCAgkAMIICBAKCAfsAqM4o+hVAdF2QATQBmpehSMyhdqKvwh9mrfnxDNtctZYlpiQXMbq4uqRgp98aBy6bMKKr3k0rSXTzr27Y+tdLUWXqbl4y8kKm8rGZo9gTbPyhqPm4f4OIxMRJcuhQ7f8qBY87w9buzClQeUs3h5f+DUVRUfB9FnDtim+ma3mFqYh38TMnrBapCtG+7iVKRFgGv6JWiNTql+oVBPNuUX3koc5/zO6IhrD49vBbsjaRWTJV2xMNll82gPvVLtgQNA2t7iGnUPhfKDj1NInZeg79NzFnWAa9Jtc1r2Q7D68MiJhYZN2QAlZS1GfbELnRAeUmSxT5i3BHu23iz9zluhIhYe1vhA1QWk2HsriGL9w+iFqzYlk5P3GCAE+nfNmM/6GIp1ehzW+/4+xgik5rOakCWw4vewmSBWOrV/XZvT2ZT3AA6zIByWdERyMOVJmd9rqPH1FIDtQk8h2jFTqIvBda727DHXeUB9J4hHQTzQmvOxPMipwDslxWOjnG4nbq6Exme0o/ELMOxt+4APH6KW+LqCNl5jGdbKxySLQyNgfUjhXJ06U1b8JHPheTnWcKO+cMmhyheUkZmLMLK2mlAsR+JJeBDY1/jd7+q6hgymeJzoDoXJj4LARiYZ+StRr/E0+P8DrprWYZPi496VIzwgV8otV9fVz29V501rcCAwEAAQ== + -----END PUBLIC KEY----- + - |- + -----BEGIN EC PUBLIC KEY----- + MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvmBia5inhASHAVrFBB5JAh0ne/aKb6z/sCIuWzKp/azFcD/OPJ2H6RPLn3t7XA4oAa2FR3GB4ZhU7SCh20FUhA== + -----END EC PUBLIC KEY----- From 24c31fd5e2fad3543587693d9213021fba266582 Mon Sep 17 00:00:00 2001 From: Denny Verbeeck Date: Tue, 12 Jun 2018 11:48:52 +0200 Subject: [PATCH 03/15] Allow EC signing keys and multiple checking keys in ManagementPortal Also adds configuration keys for the keystore password and the key aliases to use for signing and checking. --- .../auth/authentication/TokenValidator.java | 6 +- .../config/LocalKeystoreConfig.java | 32 +++--- .../config/ManagementPortalProperties.java | 33 +++++- .../config/OAuth2ServerConfiguration.java | 50 +++++++-- .../config/SecurityConfiguration.java | 13 ++- .../security/JwtAuthenticationFilter.java | 7 +- .../management/security/jwt/EcdsaSigner.java | 43 ++++++++ .../security/jwt/EcdsaVerifier.java | 43 ++++++++ .../security/jwt/MultiVerifier.java | 42 ++++++++ .../jwt/RadarJwtAccessTokenConverter.java | 97 ++++++++++++++++++ .../security/jwt/RadarKeyStoreKeyFactory.java | 61 +++++++++++ .../security/jwt/SignatureException.java | 20 ++++ src/main/resources/config/application.yml | 6 ++ .../cucumber/stepdefs/UserStepDefs.java | 2 +- .../JwtAuthenticationFilterIntTest.java | 87 ++++++++++++++++ .../web/rest/AuditResourceIntTest.java | 25 ++--- .../web/rest/OAuthClientsResourceIntTest.java | 2 +- .../management/web/rest/OAuthHelper.java | 71 +++++++++---- .../web/rest/ProjectResourceIntTest.java | 2 +- .../web/rest/SourceDataResourceIntTest.java | 2 +- .../web/rest/SourceResourceIntTest.java | 2 +- .../web/rest/SourceTypeResourceIntTest.java | 2 +- .../web/rest/SubjectResourceIntTest.java | 2 +- .../web/rest/UserResourceIntTest.java | 2 +- src/test/resources/config/application.yml | 8 ++ src/test/resources/config/keystore.jks | Bin 3873 -> 4581 bytes 26 files changed, 588 insertions(+), 72 deletions(-) create mode 100644 src/main/java/org/radarcns/management/security/jwt/EcdsaSigner.java create mode 100644 src/main/java/org/radarcns/management/security/jwt/EcdsaVerifier.java create mode 100644 src/main/java/org/radarcns/management/security/jwt/MultiVerifier.java create mode 100644 src/main/java/org/radarcns/management/security/jwt/RadarJwtAccessTokenConverter.java create mode 100644 src/main/java/org/radarcns/management/security/jwt/RadarKeyStoreKeyFactory.java create mode 100644 src/main/java/org/radarcns/management/security/jwt/SignatureException.java create mode 100644 src/test/java/org/radarcns/management/security/JwtAuthenticationFilterIntTest.java 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 041300b5cc58bdae71f747540bc4e2cd71894146..aa6d01b64bdae969089636065f0cbf78476f6369 100644 GIT binary patch delta 2988 zcmaKsXH3(L8pYdEDn*7+)+?K`5vc4TyC9W~Y!E243Kl3Jpfdckd_k0rGDI07P%NwL zDMMt*5-CI3GE{~IMcLQ9d6WC$CilZho+mjcIeC7`Qy{Q{1WqlgRfi<;Jh5{$=7#DUr=93~xI6Wjg1IshZ)gm`EBREjqET zvkCRQ&HK{<45D|CQ%0%(nRZdp;O#$3&!>ZcArPqJ)7uFsCK!YC-^RmQP&!C@J3N?I z4W)uoMyV*PC|`8qX1=I~Qb#HNyQX)*iGU*t0Xy?g0s{UwN(Vkerw@X*yL%9_N@AgL zV5c6a00zP@5w@2=B%fA^$T=&Jj>$d#I>VQq#eG2x=7oj1ut^?dU+^H&>-a!(akY0N4;%}EZTk#BE;TW^-9k!_{FhYlNMLDd{FM| zkfn9NZKH6??=hB_7V*!a1pW`#z+2dVzyfwD-XIYC1}Ylv`9Ex<;THDfPWtk+H|b>( zGrM6!=45T&wp#ZsIavsw>bNn&^t^`wm1VBwoQSr+!m*R>6JGc1gsEL(dUBm$VEDMQ zCGrhX!{N&ne5FQ6)Mi@k_5Rtb3BD5D5>ei(+t0qIY5oMzubxbn#29#)yaPbrzQ)B+ z1yfeJ02Q%sG58I?d9E^nB?MD{UbO(JKg189P@C@r-j2D8e(u69#hvtmYrl`)YpC&h zbW6|?vWA8AK(QI|@RSLbcMf*;@^Ut7^_N-now32q8jpfUNKv(f^wS$(&u#G3mGE@HhkSPu8cxG>3nU8ITed$X5+v;$%f%;lhuU7L>25e5_HU zLV3Ulr=ck4?Y*jyj`}AfWqj#dNX6yf1=Wa^%CZMXttQ;6nPM|s=aLUI!a2@(+33*_ zPUEiaIVD|Pii7WaCt^H!J3=9G$8L<-soX8X!sAQyT<&7k%jZQ@gJlK+8lB7=0-{`k z*edUNP6xd|(P~#Jt8<60lMJ#JZB*8Et4#ozg^cn>;L?7!F5KP1G7)MZ2wW;xP8Cyg}R-h?Y zoGGS#!N4O8=A9XtzIKz8Tj_4;H_B5~0seI|p3@k@ZrGW^*^RN-uhh^4Z|7IJpI9Pl zm#?vWLW|uO!lU1HiX3eJRws;}aE4eQTdM0kiimxyv>8`&4LC4GWgBM%5hs9s6x+Ta@hFj%75HW~kqxRfLYHud`=uyz(fNrN&2Ff1a4g z3^o^7a;8}cXO6lg0Z~PvwjXTw-AZE}|L$q|=C)be&@5VWlJHFDLOU}Mha?hvx$UHN zR@l*Fk~=94sdB$!(h=MH$@#E4t~)s5*taJf!szICUsO`!p7Bk66C#aQR>gVmU3}2L zelwZ+iLfng!>!|~{$_6ehI`5~#XjP0VjoB~CBW}zOyA>rX-lS+AWhINlBG&Jn zsk01{!LC)dgVgdA?!0W^W2kw$I;l<-pKq&HC6Ie7S91-qmx7~-Ok&`wFTMQdn#h|s z_fp?h1X`ZR9_HL%&`o&JSJS^JN;Cg&>+uUuq? zCouNbpjRuNw^dY?^&#?w+SVOf{)e@hHd&04pI(nMn-YfmjKCWJm#dN}vxModPPXfd zH(zFow0z=)u4jai0e^-fTXO0oBCNnyDmmp)RzPzn*GM>`sxg+KJ|iEwp{B)u<^*>>+G4iJLfIGFatC`P9$#nW4hP>NZ(H+X72vi7{K z<#<+cb$8t{9Qr+<>GUY>0N{^tfLoDBPR1|0ET)xq?Z0l#DTvQ7bj*z(f5X%|muWK0 ztOMMC#S3`@j}DG-b{**Lzy@5n8|Wz_H(!)`R-*a=nVfu}NLrM~2pyEWF&>T< zM;;9OQXe|eR6-ebNVA2jy_KtzLQ8n8in5~YSwQ`b8OFiMASutL1gB@}D>yo7!2(Zr zrc_?2n~qi4eJis4cb)i;=l9Nt@YU;X1>n;+yLH)>ii8wrzOt z{+xdw%{3?N>CaCyE?nIBoodJHZ7J9fq-pKaYn%D7>0L(uHgZ6jAndu^c9hf;wIhNz zeSEpzvWb}M32bM`i_YkRuPl|v(#7p188ao#H&Hg7!w^XBMQS9cuc!l#RodrLucg&( ziicKsjTpv3Twv9We;StRys_x$KaiuJ53lu7SUciTeilb~E&g<}G~ayPon#6yVG6JZ z7p}uzB43{SWzCXgSpyETj2Jp6<|51+4JJ?7I5Vf>pc$9X+8gfCf_iO_ykPmrxLBG| zC3Df5<;I>jOwP5S0)h&gyMSbjsB|0T znTIc!clp~RyeNr@ygwKxQh}kJYUQ$xh{Oxw@Sru3cQUJKstee^Zn_L;T+N&r3pu-4 zyvk|rOk*1tw~^K76C{*PeR?3%o^AuHlGN{_f1F>q(_Jp~^m^&^B#*zJK`@1AE3cGdU5lI;{AuwY9=Uax delta 2453 zcmV;G32OG`BcUE7{_Xzl000020000100001019(uY-V$5XKrO=00048&#Z-dljs5* ze;O|in7y9P$8KW9_-uDR_sXXp^ZsJhO3Ig_ggz?}enn?H0V_(A$ze-IpR+e8vNHtH zdV-vo1jZ>D&gz8G7ftB@!*;I)PDp?ToXMkGX4NJm&$MiqaGaUk#S*jaWDKX-oloG6 zvQ!Bo`>eP5^tv0E$)^DfSvESaM^^xNe|2(gkH)NiQIu=XOu6y>1Pu1w-b<$}N0K(P zD?^SWwKFPG5G9%1G}=*xZ(%sa>XC z3y^eA*0=@VTh{&s2FkLfN9r4TfVSe&5mpW(ia}z3z!VF%ZfBD7d z&K7lkw24#dN!_@U{IpNXJmIe@We(iJth%%_Ewn){0xt>qrmPzp#%Vm$R@HlprgB6o znOC>*(ouc?w8c4UgpE`Jl{?t{TT; zNfPE$Ui+8lNgx1oD(tjPqUGYze>16{v!-4B2$7u`uu>6jJ4}%k$#ed0(Gs?K8Y_=; ztrbTWH#&NnvHN%M0_AWD6#;@ET?JlI1nQZFBnF;lR{_+^WBQO2>H)eLLEa-s&v7|| z-D7fVg_r~eT;!^f-y zdewMFUKf<7IcCzHkghHZjS#vOZaYnez;7%sV38C*-RaQitEu?fLdtc}m!H3Kk)WOD zyt;Y(VUzAjztAUe+$FYXpX~k^>+B&?O>$g`XjO@gvv3r1!NBM+dIb9OP=&QH_)nn{ zsmI3XAy@9`m?S)U9L2M_f1-4f2-ed}P0z-rlAq!d7Y0nqVY7I@g0@RFGsUYv#QlKb zDl#)J#%vF+g*h5HJ&E{kB)^v=YojU6~rSMg#Cg)%2Fm{*qV?03&z}e@%?N3|dk1IV+R7 z^U1I$U!B-^KQy{KId_x9)=+zT3M;BmFK|WZ4G#=1Eh5MnuvGr( z`spPHHq?;qOVeQ^e{6LrQV0|b$6hj(fEzJKICrV5nst^o1G*fvEg6r&9E64greOw8 zyY~f^+vHomX{01qWdbU6599qwSHJJZf)itVQGR$w;<0VNU}U zeRl`p^wpE)bPxioXPb@u?AMR44~f2|`sRrE_2RRFtt-^4a%69kj`0~>BnXmfylN7b zOZJ&wpol=7f1hI!jJN^*FdhdvP@Cl{g7o9lk|a z*P%A`e&*>V}qNAjv}X zd7ZfV@Jv$tb##&sg3Vy<-Gv`jo41#My%-Cwg5rX!wPBlQjV>W?n9x}p3BdA8_JI1t z1Pon6cFoHt{z_R8jVdzzvDjSNyLjMCgSm5zf6X*v%8CEsHM_kGQ~F(^B4HSA2Afr* ziX=4&OebTFbAQ6_uWSRNe4J+QoBTy-!&$=L`r1P5>g8F3N%f>I0%5ozn-zr43;lzE z?G#yo@>?M%EMGGf2)KQbU)G^<09RzRqsjAl+Kw^+py+!3?;5Bu4(UcFv0~#hyvsyT^Z=QeM66{TFpA&cz6D!8lA^u$I9#&CktaemhRW_WktYxx z^;470i;M9ERMebK7;T8oP4IR`Ikc&V;9N)k8wSrwae3ss3@`x+;SSSH{TWA0q8&XX zbqdPrgV+Xa5Y()yJ^i%(63sEFpi^Mr`9Q7^p Date: Tue, 12 Jun 2018 12:18:35 +0200 Subject: [PATCH 04/15] Add default behavior, if no checkingkey aliases are defined, use signingkey alias --- .../config/OAuth2ServerConfiguration.java | 33 ++++++++++--------- .../config/SecurityConfiguration.java | 12 ++++++- src/main/resources/config/application.yml | 3 -- 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/radarcns/management/config/OAuth2ServerConfiguration.java b/src/main/java/org/radarcns/management/config/OAuth2ServerConfiguration.java index 211019c0e..c723e1888 100644 --- a/src/main/java/org/radarcns/management/config/OAuth2ServerConfiguration.java +++ b/src/main/java/org/radarcns/management/config/OAuth2ServerConfiguration.java @@ -212,21 +212,24 @@ public JwtAccessTokenConverter accessTokenConverter() { 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)); + // if a list of checking keys is defined, use that for checking + if (managementPortalProperties.getOauth().getCheckingKeyAliases() != null + && !managementPortalProperties.getOauth().getCheckingKeyAliases().isEmpty()) { + // 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 033d7c7b5..fe8a059d4 100644 --- a/src/main/java/org/radarcns/management/config/SecurityConfiguration.java +++ b/src/main/java/org/radarcns/management/config/SecurityConfiguration.java @@ -28,6 +28,8 @@ import javax.annotation.PostConstruct; import javax.servlet.Filter; +import java.util.Collections; +import java.util.List; @Configuration @EnableWebSecurity @@ -138,8 +140,16 @@ public FilterRegistrationBean jwtAuthenticationFilterRegistration() { } public Filter jwtAuthenticationFilter() { + List publicKeyAliases; + if (managementPortalProperties.getOauth().getCheckingKeyAliases() != null && + !managementPortalProperties.getOauth().getCheckingKeyAliases().isEmpty()) { + publicKeyAliases = managementPortalProperties.getOauth().getCheckingKeyAliases(); + } else { + publicKeyAliases = Collections.singletonList(managementPortalProperties.getOauth() + .getSigningKeyAlias()); + } return new JwtAuthenticationFilter(new TokenValidator( new LocalKeystoreConfig(managementPortalProperties.getOauth().getKeyStorePassword(), - managementPortalProperties.getOauth().getCheckingKeyAliases()))); + publicKeyAliases))); } } diff --git a/src/main/resources/config/application.yml b/src/main/resources/config/application.yml index 11ba5fe49..389202dcd 100644 --- a/src/main/resources/config/application.yml +++ b/src/main/resources/config/application.yml @@ -94,9 +94,6 @@ managementportal: oauth: keyStorePassword: radarbase signingKeyAlias: radarbase-managementportal-ec - checkingKeyAliases: - - radarbase-managementportal-ec - - radarbase-managementportal-rsa # =================================================================== # JHipster specific properties From 36755ade78b2ad2b876afaf9ea748b59af1abff8 Mon Sep 17 00:00:00 2001 From: Denny Verbeeck Date: Tue, 12 Jun 2018 12:23:34 +0200 Subject: [PATCH 05/15] Update documentation --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0b936159e..af174fed0 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # ManagementPortal [![Build Status](https://travis-ci.org/RADAR-base/ManagementPortal.svg?branch=master)](https://travis-ci.org/RADAR-base/ManagementPortal) -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/87bb961266d3443988b52ee7aa32f100)](https://www.codacy.com/app/RADAR-CNS/ManagementPortal?utm_source=github.com&utm_medium=referral&utm_content=RADAR-CNS/ManagementPortal&utm_campaign=Badge_Grade) -[![Codacy Badge](https://api.codacy.com/project/badge/Coverage/87bb961266d3443988b52ee7aa32f100)](https://www.codacy.com/app/RADAR-CNS/ManagementPortal?utm_source=github.com&utm_medium=referral&utm_content=RADAR-CNS/ManagementPortal&utm_campaign=Badge_Coverage) +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/d6945ebd1eba4a3fbb55882cda33655e)](https://www.codacy.com/app/RADAR-CNS/ManagementPortal?utm_source=github.com&utm_medium=referral&utm_content=RADAR-CNS/ManagementPortal&utm_campaign=Badge_Grade) +[![Codacy Badge](https://api.codacy.com/project/badge/Coverage/d6945ebd1eba4a3fbb55882cda33655e)](https://www.codacy.com/app/RADAR-CNS/ManagementPortal?utm_source=github.com&utm_medium=referral&utm_content=RADAR-CNS/ManagementPortal&utm_campaign=Badge_Coverage) ManagementPortal is an application which is used to manage pilot studies for [RADAR-CNS](http://www.radar-cns.org/). @@ -35,7 +35,7 @@ docker-compose files. 1. Make sure [Docker][] and [Docker-Compose][] are installed on your system. 2. Generate a key pair for signing JWT tokens as follows: ```shell - keytool -genkey -alias selfsigned -keyalg RSA -keystore src/main/docker/etc/config/keystore.jks -keysize 4048 -storepass radarbase + keytool -genkeypair -alias radarbase-managementportal-ec -keyalg EC -keysize 256 -sigalg SHA256withECDSA -storetype JKS -keystore src/main/docker/etc/config/keystore.jks -storepass radarbase ``` 3. Now, we can start the stack with `docker-compose -f src/main/docker/management-portal.yml up -d`. @@ -50,7 +50,7 @@ you must install and configure the following dependencies on your machine to run Depending on your system, you can install Yarn either from source or as a pre-packaged bundle. 3. Generate a key pair for signing JWT tokens as follows: ```shell - keytool -genkey -alias selfsigned -keyalg RSA -keystore src/main/resources/config/keystore.jks -keysize 4048 -storepass radarbase + keytool -genkeypair -alias radarbase-managementportal-ec -keyalg EC -keysize 256 -sigalg SHA256withECDSA -storetype JKS -keystore keystore.jks -storepass radarbase ``` **Make sure the key password and store password are the same!** This is a requirement for Spring Security. @@ -98,6 +98,9 @@ for other options on overriding the default configuration. | `MANAGEMENTPORTAL_FRONTEND_ACCESS_TOKEN_VALIDITY_SECONDS` | `14400` | Frontend access token validity period in seconds | | `MANAGEMENTPORTAL_FRONTEND_REFRESH_TOKEN_VALIDITY_SECONDS` | `259200` | Frontend refresh token validity period in seconds | | `MANAGEMENTPORTAL_OAUTH_CLIENTS_FILE` | `/mp-includes/config/oauth_client_details.csv` | Location of the OAuth clients file | +| `MANAGEMENTPORTAL_OAUTH_KEY_STORE_PASSWORD` | `radarbase` | Password for the JKS keystore | +| `MANAGEMENTPORTAL_OAUTH_SIGNING_KEY_ALIAS` | `radarbase-managementportal-ec` | Alias in the keystore of the keypair to use for signing | +| `MANAGEMENTPORTAL_OAUTH_CHECKING_KEY_ALIASES_0` | None | Alias in the keystore of the public key to use for checking. Define multiple aliases by increasing number suffix (i.e. setting `MANAGEMENTPORTAL_OAUTH_CHECKING_KEY_ALIASES_1`, `MANAGEMENTPORTAL_OAUTH_CHECKING_KEY_ALIASES_2` etc.). If you do not set a list of checking key aliases, the public key of the signing keypair will be used for checking signatures. | | `MANAGEMENTPORTAL_CATALOGUE_SERVER_ENABLE_AUTO_IMPORT` | `false` | Wether to enable or disable auto import of sources from the catalogue server | | `MANAGEMENTPORTAL_CATALOGUE_SERVER_SERVER_URL` | None | URL to the catalogue server | | `JHIPSTER_SLEEP` | `10` | Time in seconds that the application should wait at bootup. Used to allow the database to become ready | From 9a14d3d8816c8ef10664ecaceaf05d4d959e2f74 Mon Sep 17 00:00:00 2001 From: Denny Verbeeck Date: Tue, 12 Jun 2018 13:01:03 +0200 Subject: [PATCH 06/15] Style fixes and test fixes --- .../config/OAuth2ServerConfiguration.java | 23 ++++++++++--------- .../config/SecurityConfiguration.java | 9 ++++++-- .../jwt/RadarJwtAccessTokenConverter.java | 8 ++++--- .../management/web/rest/OAuthHelper.java | 17 +++++++++----- src/test/resources/config/application.yml | 5 +--- 5 files changed, 36 insertions(+), 26 deletions(-) diff --git a/src/main/java/org/radarcns/management/config/OAuth2ServerConfiguration.java b/src/main/java/org/radarcns/management/config/OAuth2ServerConfiguration.java index c723e1888..36b139b5c 100644 --- a/src/main/java/org/radarcns/management/config/OAuth2ServerConfiguration.java +++ b/src/main/java/org/radarcns/management/config/OAuth2ServerConfiguration.java @@ -217,17 +217,18 @@ public JwtAccessTokenConverter accessTokenConverter() { && !managementPortalProperties.getOauth().getCheckingKeyAliases().isEmpty()) { // 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()); + 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)); } diff --git a/src/main/java/org/radarcns/management/config/SecurityConfiguration.java b/src/main/java/org/radarcns/management/config/SecurityConfiguration.java index fe8a059d4..9224ba7aa 100644 --- a/src/main/java/org/radarcns/management/config/SecurityConfiguration.java +++ b/src/main/java/org/radarcns/management/config/SecurityConfiguration.java @@ -139,10 +139,15 @@ public FilterRegistrationBean jwtAuthenticationFilterRegistration() { return registration; } + /** + * Create a {@link JwtAuthenticationFilter}. + * + * @return the JwtAuthenticationFilter + */ public Filter jwtAuthenticationFilter() { List publicKeyAliases; - if (managementPortalProperties.getOauth().getCheckingKeyAliases() != null && - !managementPortalProperties.getOauth().getCheckingKeyAliases().isEmpty()) { + if (managementPortalProperties.getOauth().getCheckingKeyAliases() != null + && !managementPortalProperties.getOauth().getCheckingKeyAliases().isEmpty()) { publicKeyAliases = managementPortalProperties.getOauth().getCheckingKeyAliases(); } else { publicKeyAliases = Collections.singletonList(managementPortalProperties.getOauth() diff --git a/src/main/java/org/radarcns/management/security/jwt/RadarJwtAccessTokenConverter.java b/src/main/java/org/radarcns/management/security/jwt/RadarJwtAccessTokenConverter.java index dfd0cfe8c..a551ccc24 100644 --- a/src/main/java/org/radarcns/management/security/jwt/RadarJwtAccessTokenConverter.java +++ b/src/main/java/org/radarcns/management/security/jwt/RadarJwtAccessTokenConverter.java @@ -25,8 +25,8 @@ /** * 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. + *

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 { @@ -92,6 +92,8 @@ protected String encode(OAuth2AccessToken accessToken, OAuth2Authentication auth @Override public boolean isPublic() { - return true; + // the signer is private in our superclass, but we can check the algorithm with getKey() + return getKey().getOrDefault("alg", "").equals("SHA256withECDSA") + || getKey().getOrDefault("alg", "").equals("SHA256withRSA"); } } 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 aff60c749..fa470d462 100644 --- a/src/test/java/org/radarcns/management/web/rest/OAuthHelper.java +++ b/src/test/java/org/radarcns/management/web/rest/OAuthHelper.java @@ -29,8 +29,8 @@ public class OAuthHelper { 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 String TEST_SIGNKEY_ALIAS = "radarbase-managementportal-ec"; + public static String TEST_CHECKKEY_ALIAS = "radarbase-managementportal-rsa"; public static final String[] SCOPES = allScopes(); public static final String[] AUTHORITIES = {"ROLE_SYS_ADMIN"}; @@ -89,18 +89,23 @@ public static void setUp() throws Exception { Certificate cert = ks.getCertificate(TEST_SIGNKEY_ALIAS); ECPublicKey publicKey = (ECPublicKey) cert.getPublicKey(); + VALID_EC_TOKEN = createValidToken(Algorithm.ECDSA256(publicKey, privateKey)); + SUPER_USER_TOKEN = JWT.decode(VALID_EC_TOKEN); + // 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(); - - 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)); + keyStream.close(); } + /** + * Helper method to initialize an authentication filter for use in test classes. + * + * @return an initialized JwtAuthenticationFilter + */ public static JwtAuthenticationFilter createAuthenticationFilter() { return new JwtAuthenticationFilter(new TokenValidator( new LocalKeystoreConfig(TEST_KEYSTORE_PASSWORD, Arrays.asList(TEST_SIGNKEY_ALIAS, diff --git a/src/test/resources/config/application.yml b/src/test/resources/config/application.yml index 1625bfa48..b1a32b08c 100644 --- a/src/test/resources/config/application.yml +++ b/src/test/resources/config/application.yml @@ -99,7 +99,4 @@ application: managementportal: oauth: keyStorePassword: radarbase - signingKeyAlias: ec - checkingKeyAliases: - - ec - - selfsigned + signingKeyAlias: radarbase-managementportal-ec From 31e59bc37cae39956f82fdf5e16292d373bea5c7 Mon Sep 17 00:00:00 2001 From: Denny Verbeeck Date: Tue, 12 Jun 2018 15:40:29 +0200 Subject: [PATCH 07/15] Codacy style fixes and additional debug log output --- .../auth/authentication/TokenValidator.java | 3 ++- .../security/jwt/MultiVerifier.java | 6 ++++- .../management/web/rest/OAuthHelper.java | 22 +++++++++---------- 3 files changed, 18 insertions(+), 13 deletions(-) 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 da4e83658..2a8317833 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 @@ -132,7 +132,8 @@ public RadarToken validateAccessToken(String token) throws TokenValidationExcept refresh(); return validateAccessToken(token); } catch (JWTVerificationException ex) { - // this verifier does not accept the token, move on to the next one + log.debug("Verifier {} with implementation {} did not accept token {}", + verifier.toString(), verifier.getClass().toString(), token); } } throw new TokenValidationException("No registered validator could authenticate this token"); diff --git a/src/main/java/org/radarcns/management/security/jwt/MultiVerifier.java b/src/main/java/org/radarcns/management/security/jwt/MultiVerifier.java index 87b3f1dfe..9a4f7c3a6 100644 --- a/src/main/java/org/radarcns/management/security/jwt/MultiVerifier.java +++ b/src/main/java/org/radarcns/management/security/jwt/MultiVerifier.java @@ -1,5 +1,7 @@ package org.radarcns.management.security.jwt; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.security.jwt.crypto.sign.SignatureVerifier; import java.util.LinkedList; @@ -11,6 +13,7 @@ */ public class MultiVerifier implements SignatureVerifier { + private static final Logger log = LoggerFactory.getLogger(MultiVerifier.class); private final List verifiers = new LinkedList<>(); /** @@ -28,7 +31,8 @@ public void verify(byte[] content, byte[] signature) { verifier.verify(content, signature); return; } catch (RuntimeException ex) { - // try the next verifier + log.debug("Verifier {} with implementation {} could not verify the signature", + verifier.toString(), verifier.getClass().toString()); } } throw new SignatureException("Signature could not be verified by any of the registered " 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 fa470d462..bee4f4042 100644 --- a/src/test/java/org/radarcns/management/web/rest/OAuthHelper.java +++ b/src/test/java/org/radarcns/management/web/rest/OAuthHelper.java @@ -25,13 +25,13 @@ * Created by dverbeec on 29/06/2017. */ public class OAuthHelper { - 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 = "radarbase-managementportal-ec"; - public static String TEST_CHECKKEY_ALIAS = "radarbase-managementportal-rsa"; + public static String validEcToken; + public static DecodedJWT superUserToken; + public static String validRsaToken; + public static final String TEST_KEYSTORE_PASSWORD = "radarbase"; + public static final String TEST_SIGNKEY_ALIAS = "radarbase-managementportal-ec"; + public static final String TEST_CHECKKEY_ALIAS = "radarbase-managementportal-rsa"; public static final String[] SCOPES = allScopes(); public static final String[] AUTHORITIES = {"ROLE_SYS_ADMIN"}; public static final String[] ROLES = {}; @@ -56,7 +56,7 @@ public class OAuthHelper { */ public static RequestPostProcessor bearerToken() { return mockRequest -> { - mockRequest.addHeader("Authorization", "Bearer " + VALID_EC_TOKEN); + mockRequest.addHeader("Authorization", "Bearer " + validEcToken); return mockRequest; }; } @@ -68,7 +68,7 @@ public static RequestPostProcessor bearerToken() { */ public static RequestPostProcessor rsaBearerToken() { return mockRequest -> { - mockRequest.addHeader("Authorization", "Bearer " + VALID_RSA_TOKEN); + mockRequest.addHeader("Authorization", "Bearer " + validRsaToken); return mockRequest; }; } @@ -89,15 +89,15 @@ public static void setUp() throws Exception { Certificate cert = ks.getCertificate(TEST_SIGNKEY_ALIAS); ECPublicKey publicKey = (ECPublicKey) cert.getPublicKey(); - VALID_EC_TOKEN = createValidToken(Algorithm.ECDSA256(publicKey, privateKey)); - SUPER_USER_TOKEN = JWT.decode(VALID_EC_TOKEN); + validEcToken = createValidToken(Algorithm.ECDSA256(publicKey, privateKey)); + superUserToken = JWT.decode(validEcToken); // 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(); - VALID_RSA_TOKEN = createValidToken(Algorithm.RSA256(rsaPublicKey, rsaPrivateKey)); + validRsaToken = createValidToken(Algorithm.RSA256(rsaPublicKey, rsaPrivateKey)); keyStream.close(); } From dc64b0f3ab508b7add35cec71f90655155f8d282 Mon Sep 17 00:00:00 2001 From: Denny Verbeeck Date: Wed, 13 Jun 2018 15:27:48 +0200 Subject: [PATCH 08/15] Refactor public key parsing and algorithm generation to seperate class --- .../auth/authentication/TokenValidator.java | 90 +++++++------------ .../radarcns/auth/config/ServerConfig.java | 5 +- .../auth/config/YamlServerConfig.java | 47 +--------- .../AbstractTokenValidationAlgorithm.java | 35 ++++++++ .../ECTokenValidationAlgorithm.java | 28 ++++++ .../RSATokenValidationAlgorithm.java | 27 ++++++ .../validation/TokenValidationAlgorithm.java | 26 ++++++ .../auth/config/YamlServerConfigTest.java | 11 +-- .../config/LocalKeystoreConfig.java | 17 +++- 9 files changed, 170 insertions(+), 116 deletions(-) create mode 100644 radar-auth/src/main/java/org/radarcns/auth/token/validation/AbstractTokenValidationAlgorithm.java create mode 100644 radar-auth/src/main/java/org/radarcns/auth/token/validation/ECTokenValidationAlgorithm.java create mode 100644 radar-auth/src/main/java/org/radarcns/auth/token/validation/RSATokenValidationAlgorithm.java create mode 100644 radar-auth/src/main/java/org/radarcns/auth/token/validation/TokenValidationAlgorithm.java 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 2a8317833..a034740f9 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 @@ -8,24 +8,20 @@ import com.auth0.jwt.interfaces.DecodedJWT; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import org.bouncycastle.util.io.pem.PemReader; import org.radarcns.auth.config.ServerConfig; import org.radarcns.auth.config.YamlServerConfig; import org.radarcns.auth.exception.TokenValidationException; import org.radarcns.auth.token.JwtRadarToken; import org.radarcns.auth.token.RadarToken; +import org.radarcns.auth.token.validation.ECTokenValidationAlgorithm; +import org.radarcns.auth.token.validation.RSATokenValidationAlgorithm; +import org.radarcns.auth.token.validation.TokenValidationAlgorithm; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.InputStream; -import java.io.StringReader; import java.net.URI; import java.net.URLConnection; -import java.security.KeyFactory; -import java.security.PublicKey; -import java.security.interfaces.ECPublicKey; -import java.security.interfaces.RSAPublicKey; -import java.security.spec.X509EncodedKeySpec; import java.time.Duration; import java.time.Instant; import java.util.Arrays; @@ -50,6 +46,8 @@ public class TokenValidator { private final ServerConfig config; private List verifiers = new LinkedList<>(); + private final List algorithmList = Arrays.asList( + new ECTokenValidationAlgorithm(), new RSATokenValidationAlgorithm()); // If a client presents a token with an invalid signature, it might be the keypair was changed. // In that case we need to fetch it again, but we don't want a malicious client to be able to @@ -177,23 +175,24 @@ private List loadVerifiers() throws TokenValidationException { lastFetch = Instant.now(); } - List publicKeys = new LinkedList<>(); + List algorithms = new LinkedList<>(); if (config.getPublicKeyEndpoints() != null) { - publicKeys.addAll(config.getPublicKeyEndpoints().stream() - .map(this::publicKeyFromServer).collect(Collectors.toList())); + algorithms.addAll(config.getPublicKeyEndpoints().stream() + .map(this::algorithmFromServerPublicKey).collect(Collectors.toList())); } if (config.getPublicKeys() != null) { - publicKeys.addAll(config.getPublicKeys()); + algorithms.addAll(config.getPublicKeys().stream() + .map(this::algorithmFromString).collect(Collectors.toList())); } - // Create a verifier for each public key we have in our config - return publicKeys.stream().map(key -> JWT.require(publicKeyToAlgorithm(key)) + // Create a verifier for each signature verification algorithm we created + return algorithms.stream().map(alg -> JWT.require(alg) .withAudience(config.getResourceName()) .build()) .collect(Collectors.toList()); } - private PublicKey publicKeyFromServer(URI serverUri) throws TokenValidationException { + private Algorithm algorithmFromServerPublicKey(URI serverUri) throws TokenValidationException { log.info("Getting the JWT public key at " + serverUri); try { URLConnection connection = serverUri.toURL().openConnection(); @@ -201,58 +200,29 @@ private PublicKey publicKeyFromServer(URI serverUri) throws TokenValidationExcep try (InputStream inputStream = connection.getInputStream()) { ObjectMapper mapper = new ObjectMapper(); JsonNode publicKeyInfo = mapper.readTree(inputStream); - // We expect RSA or ECDSA algorithm, and deny to trust the public key otherwise + // We deny to trust the public key if the reported algorithm is unknown to us // https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/ - if (publicKeyInfo.get("alg").asText().equals("SHA256withRSA")) { - return rsaPublicKeyFromString(publicKeyInfo.get("value").asText()); - } - if (publicKeyInfo.get("alg").asText().equals("SHA256withECDSA")) { - return ecPublicKeyFromString(publicKeyInfo.get("value").asText()); - } - throw new TokenValidationException("The identity server reported the following " - + "signing algorithm: " + publicKeyInfo.get("alg") - + ". Expected SHA256withRSA or SHA256withECDSA."); + String alg = publicKeyInfo.get("alg").asText(); + String pk = publicKeyInfo.get("value").asText(); + return algorithmList.stream() + .filter(algorithm -> algorithm.getJwtAlgorithm().equals(alg)) + .filter(algorithm -> pk.startsWith(algorithm.getKeyHeader())) + .findFirst() + .orElseThrow(() -> new TokenValidationException("The identity server " + + "reported an unsupported signing algorithm: " + alg)) + .getAlgorithm(pk); } } catch (Exception ex) { throw new TokenValidationException(ex); } } - private RSAPublicKey rsaPublicKeyFromString(String keyString) throws TokenValidationException { - log.debug("Parsing RSA public key: " + keyString); - try (PemReader pemReader = new PemReader(new StringReader(keyString))) { - byte[] keyBytes = pemReader.readPemObject().getContent(); - pemReader.close(); - X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); - KeyFactory kf = KeyFactory.getInstance("RSA"); - return (RSAPublicKey) kf.generatePublic(spec); - } catch (Exception ex) { - throw new TokenValidationException(ex); - } - } - - private ECPublicKey ecPublicKeyFromString(String keyString) throws TokenValidationException { - log.debug("Parsing EC public key: " + keyString); - try (PemReader pemReader = new PemReader(new StringReader(keyString))) { - byte[] keyBytes = pemReader.readPemObject().getContent(); - pemReader.close(); - X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); - KeyFactory kf = KeyFactory.getInstance("ECDSA"); - return (ECPublicKey) kf.generatePublic(spec); - } catch (Exception ex) { - throw new TokenValidationException(ex); - } - } - - private Algorithm publicKeyToAlgorithm(PublicKey key) throws IllegalArgumentException { - switch (key.getAlgorithm()) { - case "EC": - return Algorithm.ECDSA256((ECPublicKey) key, null); - case "RSA": - return Algorithm.RSA256((RSAPublicKey) key, null); - default: - throw new IllegalArgumentException("Unsupported public key algorithm: {}. " - + "Expected either EC or RSA."); - } + private Algorithm algorithmFromString(String publicKey) { + return algorithmList.stream() + .filter(algorithm -> publicKey.startsWith(algorithm.getKeyHeader())) + .findFirst() + .orElseThrow(() -> new TokenValidationException("Unsupported public key: " + + publicKey)) + .getAlgorithm(publicKey); } } diff --git a/radar-auth/src/main/java/org/radarcns/auth/config/ServerConfig.java b/radar-auth/src/main/java/org/radarcns/auth/config/ServerConfig.java index 9f0035427..0fbac6d2b 100644 --- a/radar-auth/src/main/java/org/radarcns/auth/config/ServerConfig.java +++ b/radar-auth/src/main/java/org/radarcns/auth/config/ServerConfig.java @@ -1,7 +1,6 @@ package org.radarcns.auth.config; import java.net.URI; -import java.security.PublicKey; import java.util.List; public interface ServerConfig { @@ -20,9 +19,9 @@ public interface ServerConfig { String getResourceName(); /** - * Get the public keys set in the config file. + * Get the public keys set in the config file. They should be in PEM format. * @return The public keys, or null if not defined */ - List getPublicKeys(); + List getPublicKeys(); } diff --git a/radar-auth/src/main/java/org/radarcns/auth/config/YamlServerConfig.java b/radar-auth/src/main/java/org/radarcns/auth/config/YamlServerConfig.java index 90ae8afa5..c70c68301 100644 --- a/radar-auth/src/main/java/org/radarcns/auth/config/YamlServerConfig.java +++ b/radar-auth/src/main/java/org/radarcns/auth/config/YamlServerConfig.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import org.bouncycastle.util.io.pem.PemReader; import org.radarcns.auth.exception.ConfigurationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -10,19 +9,12 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.io.StringReader; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; -import java.security.KeyFactory; -import java.security.PublicKey; -import java.security.spec.X509EncodedKeySpec; -import java.util.HashMap; import java.util.LinkedList; import java.util.List; -import java.util.Map; import java.util.Objects; -import java.util.stream.Collectors; /** * Created by dverbeec on 14/06/2017. @@ -32,23 +24,12 @@ public class YamlServerConfig implements ServerConfig { public static final String CONFIG_FILE_NAME = "radar-is.yml"; private List publicKeyEndpoints = new LinkedList<>(); private String resourceName; - private List publicKeys = new LinkedList<>(); + private List publicKeys = new LinkedList<>(); private static YamlServerConfig config; private final Logger log = LoggerFactory.getLogger(YamlServerConfig.class); - // a map with as key the string to search for in a PEM encoded public key, and as value the - // KeyFactory type to request - private final Map keyFactoryTypes = new HashMap<>(); - /** - * Default constructor. Initializes the keyFactoryTypes map. - */ - public YamlServerConfig() { - keyFactoryTypes.put("-----BEGIN PUBLIC KEY-----", "RSA"); - keyFactoryTypes.put("-----BEGIN EC PUBLIC KEY-----", "EC"); - log.info("YamlServerConfig initializing..."); - } /** * Read the configuration from file. This method will first check if the environment variable @@ -115,7 +96,7 @@ public String getResourceName() { } @Override - public List getPublicKeys() { + public List getPublicKeys() { return publicKeys; } @@ -129,7 +110,7 @@ public void setResourceName(String resourceName) { * @param publicKeys The public keys to parse */ public void setPublicKeys(List publicKeys) { - this.publicKeys = publicKeys.stream().map(this::parseKey).collect(Collectors.toList()); + this.publicKeys = publicKeys; } @Override @@ -150,26 +131,4 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(publicKeyEndpoints, resourceName, publicKeys); } - - private PublicKey parseKey(String publicKey) { - String factoryType = keyFactoryTypes.keySet().stream() - // find the string that is contained in publicKey - .filter(publicKey::contains) - .findFirst() - // get the actual factory type - .map(keyFactoryTypes::get) - // if not found throw a ConfigurationException - .orElseThrow(() -> new ConfigurationException("Unsupported public key: " - + publicKey)); - log.debug("Parsing {} public key: {}", factoryType, publicKey); - try (PemReader pemReader = new PemReader(new StringReader(publicKey))) { - byte[] keyBytes = pemReader.readPemObject().getContent(); - pemReader.close(); - X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); - KeyFactory kf = KeyFactory.getInstance(factoryType); - return kf.generatePublic(spec); - } catch (Exception ex) { - throw new ConfigurationException(ex); - } - } } diff --git a/radar-auth/src/main/java/org/radarcns/auth/token/validation/AbstractTokenValidationAlgorithm.java b/radar-auth/src/main/java/org/radarcns/auth/token/validation/AbstractTokenValidationAlgorithm.java new file mode 100644 index 000000000..b9354c863 --- /dev/null +++ b/radar-auth/src/main/java/org/radarcns/auth/token/validation/AbstractTokenValidationAlgorithm.java @@ -0,0 +1,35 @@ +package org.radarcns.auth.token.validation; + +import org.bouncycastle.util.io.pem.PemReader; +import org.radarcns.auth.exception.ConfigurationException; + +import java.io.StringReader; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.spec.X509EncodedKeySpec; + +public abstract class AbstractTokenValidationAlgorithm implements TokenValidationAlgorithm { + + /** + * The key factory type for keys that this algorithm can parse. + * @return the key factory type + */ + protected abstract String getKeyFactoryType(); + + /** + * Parse a public key in PEM format. + * @param publicKey the public key to parse + * @return a PublicKey object representing the supplied public key + */ + protected PublicKey parseKey(String publicKey) { + try (PemReader pemReader = new PemReader(new StringReader(publicKey))) { + byte[] keyBytes = pemReader.readPemObject().getContent(); + pemReader.close(); + X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); + KeyFactory kf = KeyFactory.getInstance(getKeyFactoryType()); + return kf.generatePublic(spec); + } catch (Exception ex) { + throw new ConfigurationException(ex); + } + } +} diff --git a/radar-auth/src/main/java/org/radarcns/auth/token/validation/ECTokenValidationAlgorithm.java b/radar-auth/src/main/java/org/radarcns/auth/token/validation/ECTokenValidationAlgorithm.java new file mode 100644 index 000000000..be00a6b83 --- /dev/null +++ b/radar-auth/src/main/java/org/radarcns/auth/token/validation/ECTokenValidationAlgorithm.java @@ -0,0 +1,28 @@ +package org.radarcns.auth.token.validation; + +import com.auth0.jwt.algorithms.Algorithm; + +import java.security.interfaces.ECPublicKey; + +public class ECTokenValidationAlgorithm extends AbstractTokenValidationAlgorithm { + + @Override + public String getJwtAlgorithm() { + return "SHA256withECDSA"; + } + + @Override + public String getKeyHeader() { + return "-----BEGIN EC PUBLIC KEY-----"; + } + + @Override + public Algorithm getAlgorithm(String publicKey) { + return Algorithm.ECDSA256((ECPublicKey) parseKey(publicKey), null); + } + + @Override + protected String getKeyFactoryType() { + return "EC"; + } +} diff --git a/radar-auth/src/main/java/org/radarcns/auth/token/validation/RSATokenValidationAlgorithm.java b/radar-auth/src/main/java/org/radarcns/auth/token/validation/RSATokenValidationAlgorithm.java new file mode 100644 index 000000000..3445fac1d --- /dev/null +++ b/radar-auth/src/main/java/org/radarcns/auth/token/validation/RSATokenValidationAlgorithm.java @@ -0,0 +1,27 @@ +package org.radarcns.auth.token.validation; + +import com.auth0.jwt.algorithms.Algorithm; + +import java.security.interfaces.RSAPublicKey; + +public class RSATokenValidationAlgorithm extends AbstractTokenValidationAlgorithm { + @Override + protected String getKeyFactoryType() { + return "RSA"; + } + + @Override + public String getJwtAlgorithm() { + return "SHA256withRSA"; + } + + @Override + public String getKeyHeader() { + return "-----BEGIN PUBLIC KEY-----"; + } + + @Override + public Algorithm getAlgorithm(String publicKey) { + return Algorithm.RSA256((RSAPublicKey) parseKey(publicKey), null); + } +} diff --git a/radar-auth/src/main/java/org/radarcns/auth/token/validation/TokenValidationAlgorithm.java b/radar-auth/src/main/java/org/radarcns/auth/token/validation/TokenValidationAlgorithm.java new file mode 100644 index 000000000..25790860d --- /dev/null +++ b/radar-auth/src/main/java/org/radarcns/auth/token/validation/TokenValidationAlgorithm.java @@ -0,0 +1,26 @@ +package org.radarcns.auth.token.validation; + +import com.auth0.jwt.algorithms.Algorithm; + +public interface TokenValidationAlgorithm { + /** + * Get the algorithm description as it will be reported by the server public key endpoint + * (e.g. "SHA256withRSA" or "SHA256withEC"). + * @return the algorithm description + */ + String getJwtAlgorithm(); + + /** + * Get the header for a PEM encoded key that this algorithm can parse. + * + * @return the header for a PEM encoded key that this algorithm can parse + */ + String getKeyHeader(); + + /** + * Build a verification algorithm based on the supplied public key. + * @param publicKey the public key in PEM format + * @return the verification algorithm + */ + Algorithm getAlgorithm(String publicKey); +} diff --git a/radar-auth/src/test/java/org/radarcns/auth/config/YamlServerConfigTest.java b/radar-auth/src/test/java/org/radarcns/auth/config/YamlServerConfigTest.java index 6523734ce..67104916e 100644 --- a/radar-auth/src/test/java/org/radarcns/auth/config/YamlServerConfigTest.java +++ b/radar-auth/src/test/java/org/radarcns/auth/config/YamlServerConfigTest.java @@ -3,14 +3,16 @@ import org.junit.Rule; import org.junit.Test; import org.junit.contrib.java.lang.system.EnvironmentVariables; +import org.radarcns.auth.token.validation.ECTokenValidationAlgorithm; +import org.radarcns.auth.token.validation.RSATokenValidationAlgorithm; import java.io.File; import java.net.URI; import java.net.URISyntaxException; import java.util.List; -import java.util.stream.Collectors; import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.CoreMatchers.startsWith; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; @@ -45,9 +47,8 @@ private void checkConfig(ServerConfig config) throws URISyntaxException { assertEquals(2, uris.size()); assertEquals("unit_test", config.getResourceName()); assertEquals(2, config.getPublicKeys().size()); - List algs = config.getPublicKeys().stream() - .map(key -> key.getAlgorithm()) - .collect(Collectors.toList()); - assertThat(algs, hasItems("RSA", "EC")); + List algs = config.getPublicKeys(); + assertThat(algs, hasItems(startsWith(new ECTokenValidationAlgorithm().getKeyHeader()), + startsWith(new RSATokenValidationAlgorithm().getKeyHeader()))); } } diff --git a/src/main/java/org/radarcns/management/config/LocalKeystoreConfig.java b/src/main/java/org/radarcns/management/config/LocalKeystoreConfig.java index 75accbf98..c3ae52543 100644 --- a/src/main/java/org/radarcns/management/config/LocalKeystoreConfig.java +++ b/src/main/java/org/radarcns/management/config/LocalKeystoreConfig.java @@ -1,11 +1,12 @@ package org.radarcns.management.config; import org.radarcns.auth.config.ServerConfig; +import org.radarcns.management.security.jwt.RadarJwtAccessTokenConverter; import org.radarcns.management.security.jwt.RadarKeyStoreKeyFactory; import org.springframework.core.io.ClassPathResource; import java.net.URI; -import java.security.PublicKey; +import java.security.KeyPair; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -17,7 +18,7 @@ public class LocalKeystoreConfig implements ServerConfig { public static final String RES_MANAGEMENT_PORTAL = "res_ManagementPortal"; - private final List publicKeys; + private final List publicKeys; /** * Constructor will look for the keystore in the classpath at /config/keystore.jks and load @@ -26,8 +27,16 @@ public class LocalKeystoreConfig implements ServerConfig { public LocalKeystoreConfig(String keyStorePassword, List checkingKeyAliases) { RadarKeyStoreKeyFactory keyFactory = new RadarKeyStoreKeyFactory( new ClassPathResource("/config/keystore.jks"), keyStorePassword.toCharArray()); + // Load the key and convert to PEM format, internally spring uses the + // JwtAccessTokenConverter to do that for the token_key endpoint. We can use it here as + // well. + RadarJwtAccessTokenConverter converter = new RadarJwtAccessTokenConverter(); publicKeys = checkingKeyAliases.stream() - .map(alias -> keyFactory.getKeyPair(alias).getPublic()) + .map(alias -> { + KeyPair keyPair = keyFactory.getKeyPair(alias); + converter.setKeyPair(keyPair); + return converter.getKey().get("value"); + }) .collect(Collectors.toList()); } @@ -42,7 +51,7 @@ public String getResourceName() { } @Override - public List getPublicKeys() { + public List getPublicKeys() { return publicKeys; } } From 80d30ea8e97b0009108e0cf35dda86e4c9131463 Mon Sep 17 00:00:00 2001 From: nivethika Date: Mon, 9 Jul 2018 18:39:23 +0200 Subject: [PATCH 09/15] POST request to update attributes for sources. --- .../management/service/SourceService.java | 56 +++++++++++++ .../management/web/rest/SourceResource.java | 2 +- .../management/web/rest/SubjectResource.java | 84 +++++++++++++++++-- 3 files changed, 136 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/radarcns/management/service/SourceService.java b/src/main/java/org/radarcns/management/service/SourceService.java index b7f986fa9..cce797902 100644 --- a/src/main/java/org/radarcns/management/service/SourceService.java +++ b/src/main/java/org/radarcns/management/service/SourceService.java @@ -1,14 +1,24 @@ package org.radarcns.management.service; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collection; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; +import java.util.stream.Stream; + import org.radarcns.management.domain.Source; import org.radarcns.management.repository.SourceRepository; import org.radarcns.management.service.dto.MinimalSourceDetailsDTO; import org.radarcns.management.service.dto.SourceDTO; import org.radarcns.management.service.mapper.SourceMapper; +import org.radarcns.management.web.rest.errors.CustomConflictException; +import org.radarcns.management.web.rest.errors.ErrorConstants; +import org.radarcns.management.web.rest.util.HeaderUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -131,4 +141,50 @@ public List findAllMinimalSourceDetailsByProjectAndAssi .map(sourceMapper::sourceToMinimalSourceDetailsDTO) .collect(Collectors.toList()); } + + /** + * This method does a safe update of source assigned to a subject. + * @param sourceToUpdate source fetched from database + * @param sourceDto value to update + * @return Updated {@link MinimalSourceDetailsDTO} of source + * @throws CustomConflictException when source is requested to replace with an existing + * source-name. + */ + public MinimalSourceDetailsDTO safeUpdate(Source sourceToUpdate, + MinimalSourceDetailsDTO sourceDto) throws URISyntaxException { + // if a source-name update is expected, check whether any source available under the + // newly requested source-name + if (sourceDto.getSourceName() != null + && !sourceToUpdate.getSourceName().equals(sourceDto.getSourceName()) + && sourceRepository.findOneBySourceName(sourceDto.getSourceName()).isPresent()) { + Map errorParams = new HashMap<>(); + errorParams.put("message", "Source already exists with provided sourceName "); + errorParams.put("sourceName", sourceDto.getSourceName()); + throw new CustomConflictException(ErrorConstants.ERR_SOURCE_NAME_EXISTS, errorParams, + new URI(HeaderUtil.buildPath("api", "sources", sourceDto.getSourceName()))); + } + + // update source name + sourceToUpdate.setSourceName(sourceDto.getSourceName()); + + // update source attributes + Map mergedValues = Stream.of(sourceToUpdate.getAttributes(), + sourceDto.getAttributes()) + .map(Map::entrySet) + .flatMap(Collection::stream) + .collect( + Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + // such that if a value already exist + // for a given key, the value from the request is taken + (v1, v2) -> v2 + )); + + sourceToUpdate.setAttributes(mergedValues); + // update expected source-name + sourceToUpdate.setExpectedSourceName(sourceToUpdate.getExpectedSourceName()); + // rest of the attributes should not be updated from this request. + return sourceMapper.sourceToMinimalSourceDetailsDTO(sourceRepository.save(sourceToUpdate)); + } } diff --git a/src/main/java/org/radarcns/management/web/rest/SourceResource.java b/src/main/java/org/radarcns/management/web/rest/SourceResource.java index a97a78fba..fdc51abfc 100644 --- a/src/main/java/org/radarcns/management/web/rest/SourceResource.java +++ b/src/main/java/org/radarcns/management/web/rest/SourceResource.java @@ -131,7 +131,7 @@ public ResponseEntity> getAllSources(@ApiParam Pageable pageable log.debug("REST request to get all Sources"); Page page = sourceService.findAll(pageable); HttpHeaders headers = PaginationUtil - .generatePaginationHttpHeaders(page, "/api/source-types"); + .generatePaginationHttpHeaders(page, "/api/sources"); return new ResponseEntity<>(page.getContent(), headers, HttpStatus.OK); } diff --git a/src/main/java/org/radarcns/management/web/rest/SubjectResource.java b/src/main/java/org/radarcns/management/web/rest/SubjectResource.java index c2343ce16..931b17e7d 100644 --- a/src/main/java/org/radarcns/management/web/rest/SubjectResource.java +++ b/src/main/java/org/radarcns/management/web/rest/SubjectResource.java @@ -2,6 +2,7 @@ import static org.radarcns.auth.authorization.AuthoritiesConstants.INACTIVE_PARTICIPANT; import static org.radarcns.auth.authorization.AuthoritiesConstants.PARTICIPANT; +import static org.radarcns.auth.authorization.Permission.SOURCE_UPDATE; import static org.radarcns.auth.authorization.Permission.SUBJECT_CREATE; import static org.radarcns.auth.authorization.Permission.SUBJECT_DELETE; import static org.radarcns.auth.authorization.Permission.SUBJECT_READ; @@ -11,36 +12,44 @@ import static org.radarcns.auth.authorization.RadarAuthorization.checkPermissionOnSubject; import static org.radarcns.management.security.SecurityUtils.getJWT; -import com.codahale.metrics.annotation.Timed; -import io.github.jhipster.web.util.ResponseUtil; -import io.swagger.annotations.ApiParam; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; import java.net.URI; import java.net.URISyntaxException; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.stream.Collectors; import java.util.stream.Stream; import javax.servlet.http.HttpServletRequest; + +import com.codahale.metrics.annotation.Timed; +import io.github.jhipster.web.util.ResponseUtil; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; import org.radarcns.auth.config.Constants; import org.radarcns.auth.exception.NotAuthorizedException; import org.radarcns.management.domain.Role; +import org.radarcns.management.domain.Source; import org.radarcns.management.domain.SourceType; import org.radarcns.management.domain.Subject; import org.radarcns.management.repository.ProjectRepository; import org.radarcns.management.repository.SubjectRepository; import org.radarcns.management.security.SecurityUtils; +import org.radarcns.management.service.SourceService; import org.radarcns.management.service.SourceTypeService; import org.radarcns.management.service.SubjectService; import org.radarcns.management.service.dto.MinimalSourceDetailsDTO; import org.radarcns.management.service.dto.SourceTypeDTO; import org.radarcns.management.service.dto.SubjectDTO; import org.radarcns.management.service.mapper.SubjectMapper; +import org.radarcns.management.web.rest.errors.CustomConflictException; +import org.radarcns.management.web.rest.errors.CustomNotFoundException; import org.radarcns.management.web.rest.errors.CustomParameterizedException; +import org.radarcns.management.web.rest.errors.ErrorConstants; import org.radarcns.management.web.rest.util.HeaderUtil; import org.radarcns.management.web.rest.util.PaginationUtil; import org.slf4j.Logger; @@ -95,6 +104,11 @@ public class SubjectResource { @Autowired private AuditEventRepository eventRepository; + @Autowired + private SourceService sourceService; + + + /** * POST /subjects : Create a new subject. * @@ -447,4 +461,64 @@ public ResponseEntity> getSubjectSources( return ResponseEntity.ok().body(subjectService.getSources(subject.get())); } + + + /** + * POST /subjects/:login/sources/:sourceName Update source attributes and source-name. + * + *

The request body is a {@link MinimalSourceDetailsDTO}. The body should contain the data + * retrieved from management-portal and the data that need to be updated. This request allows + * update of attributes and source-name if necessary. The source-name will be updated only + * if the existing source-name doesn't match with the value requested and if no source + * available with the requested source-name. Attributes will be merged and if a new value is + * provided for an existing key, the new value will be updated. + *

+ * + * @param sourceDto The {@link MinimalSourceDetailsDTO} specification + * @return The {@link MinimalSourceDetailsDTO} completed with all identifying fields. + * @throws CustomConflictException if a source already available with the existing name. + * @throws CustomNotFoundException if the subject or the source not found using given ids. + */ + @PostMapping("/subjects/{login:" + Constants.ENTITY_ID_REGEX + "}/sources/{sourceName:" + + Constants.ENTITY_ID_REGEX + "}") + @ApiResponses({ + @ApiResponse(code = 200, message = "An existing source was updated"), + @ApiResponse(code = 400, message = "You must supply existing sourceId)"), + @ApiResponse(code = 404, message = "Either the subject or the source was not found.") + }) + @Timed + public ResponseEntity updateSubjectSource(@PathVariable String login, + @PathVariable String sourceName, @RequestBody MinimalSourceDetailsDTO sourceDto) + throws CustomNotFoundException, CustomConflictException, NotAuthorizedException, + URISyntaxException { + // check the subject id + Optional subject = subjectRepository.findOneWithEagerBySubjectLogin(login); + if (!subject.isPresent()) { + Map errorParams = new HashMap<>(); + errorParams.put("message", "Subject ID not found"); + errorParams.put("subjectLogin", login); + throw new CustomNotFoundException(ErrorConstants.ERR_SUBJECT_NOT_FOUND, errorParams); + } + // check the permission to update source + SubjectDTO subjectDto = subjectMapper.subjectToSubjectDTO(subject.get()); + checkPermissionOnSubject(getJWT(servletRequest), SOURCE_UPDATE, + subjectDto.getProject().getProjectName(), subjectDto.getLogin()); + + // find source under subject + List sources = subject.get().getSources().stream() + .filter(s -> s.getSourceName().equals(sourceName)) + .collect(Collectors.toList()); + + // exception if source is not found under subject + if (sources.isEmpty()) { + Map errorParams = new HashMap<>(); + errorParams.put("message", "Source not found under assigned sources of subject"); + errorParams.put("subjectLogin", login); + errorParams.put("sourceName", sourceName); + throw new CustomNotFoundException(ErrorConstants.ERR_SUBJECT_NOT_FOUND, errorParams); + } + + // there should be only one source under a source-name. + return ResponseEntity.ok().body(sourceService.safeUpdate(sources.get(0), sourceDto)); + } } From 0164a17b3c77b332a02847c03f2cd75cf0dd1da3 Mon Sep 17 00:00:00 2001 From: Denny Verbeeck Date: Tue, 10 Jul 2018 09:19:13 +0200 Subject: [PATCH 10/15] Update Codacy badges --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5ca828051..fc6bcb424 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # ManagementPortal [![Build Status](https://travis-ci.org/RADAR-base/ManagementPortal.svg?branch=master)](https://travis-ci.org/RADAR-base/ManagementPortal) -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/d6945ebd1eba4a3fbb55882cda33655e)](https://www.codacy.com/app/RADAR-CNS/ManagementPortal?utm_source=github.com&utm_medium=referral&utm_content=RADAR-CNS/ManagementPortal&utm_campaign=Badge_Grade) -[![Codacy Badge](https://api.codacy.com/project/badge/Coverage/d6945ebd1eba4a3fbb55882cda33655e)](https://www.codacy.com/app/RADAR-CNS/ManagementPortal?utm_source=github.com&utm_medium=referral&utm_content=RADAR-CNS/ManagementPortal&utm_campaign=Badge_Coverage) +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/d6945ebd1eba4a3fbb55882cda33655e)](https://www.codacy.com/app/RADAR-base/ManagementPortal?utm_source=github.com&utm_medium=referral&utm_content=RADAR-base/ManagementPortal&utm_campaign=Badge_Grade) +[![Codacy Badge](https://api.codacy.com/project/badge/Coverage/d6945ebd1eba4a3fbb55882cda33655e)](https://www.codacy.com/app/RADAR-base/ManagementPortal?utm_source=github.com&utm_medium=referral&utm_content=RADAR-base/ManagementPortal&utm_campaign=Badge_Coverage) ManagementPortal is an application which is used to manage pilot studies for [RADAR-CNS](http://www.radar-cns.org/). From 85ae7a1566c1c120983e747d4d501c405e04fc42 Mon Sep 17 00:00:00 2001 From: nivethika Date: Tue, 10 Jul 2018 11:18:55 +0200 Subject: [PATCH 11/15] POST request to update attributes for sources. --- .../management/service/SourceService.java | 36 ++++--------------- .../management/web/rest/SubjectResource.java | 16 ++++----- 2 files changed, 13 insertions(+), 39 deletions(-) diff --git a/src/main/java/org/radarcns/management/service/SourceService.java b/src/main/java/org/radarcns/management/service/SourceService.java index cce797902..cb7d8eb16 100644 --- a/src/main/java/org/radarcns/management/service/SourceService.java +++ b/src/main/java/org/radarcns/management/service/SourceService.java @@ -1,9 +1,6 @@ package org.radarcns.management.service; -import java.net.URI; -import java.net.URISyntaxException; import java.util.Collection; -import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -16,9 +13,6 @@ import org.radarcns.management.service.dto.MinimalSourceDetailsDTO; import org.radarcns.management.service.dto.SourceDTO; import org.radarcns.management.service.mapper.SourceMapper; -import org.radarcns.management.web.rest.errors.CustomConflictException; -import org.radarcns.management.web.rest.errors.ErrorConstants; -import org.radarcns.management.web.rest.util.HeaderUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -143,33 +137,17 @@ public List findAllMinimalSourceDetailsByProjectAndAssi } /** - * This method does a safe update of source assigned to a subject. + * This method does a safe update of source assigned to a subject. It will allow updates of + * attributes only. * @param sourceToUpdate source fetched from database - * @param sourceDto value to update + * @param attributes value to update * @return Updated {@link MinimalSourceDetailsDTO} of source - * @throws CustomConflictException when source is requested to replace with an existing - * source-name. */ public MinimalSourceDetailsDTO safeUpdate(Source sourceToUpdate, - MinimalSourceDetailsDTO sourceDto) throws URISyntaxException { - // if a source-name update is expected, check whether any source available under the - // newly requested source-name - if (sourceDto.getSourceName() != null - && !sourceToUpdate.getSourceName().equals(sourceDto.getSourceName()) - && sourceRepository.findOneBySourceName(sourceDto.getSourceName()).isPresent()) { - Map errorParams = new HashMap<>(); - errorParams.put("message", "Source already exists with provided sourceName "); - errorParams.put("sourceName", sourceDto.getSourceName()); - throw new CustomConflictException(ErrorConstants.ERR_SOURCE_NAME_EXISTS, errorParams, - new URI(HeaderUtil.buildPath("api", "sources", sourceDto.getSourceName()))); - } - - // update source name - sourceToUpdate.setSourceName(sourceDto.getSourceName()); + Map attributes) { // update source attributes - Map mergedValues = Stream.of(sourceToUpdate.getAttributes(), - sourceDto.getAttributes()) + Map mergedValues = Stream.of(sourceToUpdate.getAttributes(), attributes) .map(Map::entrySet) .flatMap(Collection::stream) .collect( @@ -182,9 +160,7 @@ public MinimalSourceDetailsDTO safeUpdate(Source sourceToUpdate, )); sourceToUpdate.setAttributes(mergedValues); - // update expected source-name - sourceToUpdate.setExpectedSourceName(sourceToUpdate.getExpectedSourceName()); - // rest of the attributes should not be updated from this request. + // rest of the properties should not be updated from this request. return sourceMapper.sourceToMinimalSourceDetailsDTO(sourceRepository.save(sourceToUpdate)); } } diff --git a/src/main/java/org/radarcns/management/web/rest/SubjectResource.java b/src/main/java/org/radarcns/management/web/rest/SubjectResource.java index 931b17e7d..0b47fd1d8 100644 --- a/src/main/java/org/radarcns/management/web/rest/SubjectResource.java +++ b/src/main/java/org/radarcns/management/web/rest/SubjectResource.java @@ -466,15 +466,13 @@ public ResponseEntity> getSubjectSources( /** * POST /subjects/:login/sources/:sourceName Update source attributes and source-name. * - *

The request body is a {@link MinimalSourceDetailsDTO}. The body should contain the data - * retrieved from management-portal and the data that need to be updated. This request allows - * update of attributes and source-name if necessary. The source-name will be updated only - * if the existing source-name doesn't match with the value requested and if no source - * available with the requested source-name. Attributes will be merged and if a new value is - * provided for an existing key, the new value will be updated. + *

The request body is a {@link Map} of strings. This request allows + * update of attributes only. Attributes will be merged and if a new value is + * provided for an existing key, the new value will be updated. The request will be validated + * for SOURCE.UPDATE permission and *

* - * @param sourceDto The {@link MinimalSourceDetailsDTO} specification + * @param attributes The {@link Map} specification * @return The {@link MinimalSourceDetailsDTO} completed with all identifying fields. * @throws CustomConflictException if a source already available with the existing name. * @throws CustomNotFoundException if the subject or the source not found using given ids. @@ -488,7 +486,7 @@ public ResponseEntity> getSubjectSources( }) @Timed public ResponseEntity updateSubjectSource(@PathVariable String login, - @PathVariable String sourceName, @RequestBody MinimalSourceDetailsDTO sourceDto) + @PathVariable String sourceName, @RequestBody Map attributes) throws CustomNotFoundException, CustomConflictException, NotAuthorizedException, URISyntaxException { // check the subject id @@ -519,6 +517,6 @@ public ResponseEntity updateSubjectSource(@PathVariable } // there should be only one source under a source-name. - return ResponseEntity.ok().body(sourceService.safeUpdate(sources.get(0), sourceDto)); + return ResponseEntity.ok().body(sourceService.safeUpdate(sources.get(0), attributes)); } } From a0296b8f36d9a4db3658ef002f3aaa8022b6eb4a Mon Sep 17 00:00:00 2001 From: nivethika Date: Tue, 10 Jul 2018 12:23:41 +0200 Subject: [PATCH 12/15] add test --- .../web/rest/SubjectResourceIntTest.java | 58 +++++++++++++++++++ .../management/web/rest/TestUtil.java | 27 +++++++-- 2 files changed, 79 insertions(+), 6 deletions(-) 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..d17bf797a 100644 --- a/src/test/java/org/radarcns/management/web/rest/SubjectResourceIntTest.java +++ b/src/test/java/org/radarcns/management/web/rest/SubjectResourceIntTest.java @@ -12,7 +12,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import javax.persistence.EntityManager; import javax.servlet.ServletException; @@ -26,6 +28,7 @@ import org.radarcns.management.repository.ProjectRepository; import org.radarcns.management.repository.SubjectRepository; import org.radarcns.management.security.JwtAuthenticationFilter; +import org.radarcns.management.service.SourceService; import org.radarcns.management.service.SourceTypeService; import org.radarcns.management.service.SubjectService; import org.radarcns.management.service.dto.MinimalSourceDetailsDTO; @@ -44,6 +47,7 @@ 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.MvcResult; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.transaction.annotation.Transactional; @@ -102,6 +106,9 @@ public class SubjectResourceIntTest { @Autowired private HttpServletRequest servletRequest; + @Autowired + private SourceService sourceService; + private MockMvc restSubjectMockMvc; @Before @@ -114,6 +121,7 @@ public void setUp() throws ServletException { ReflectionTestUtils.setField(subjectResource, "projectRepository" , projectRepository); ReflectionTestUtils.setField(subjectResource, "sourceTypeService", sourceTypeService); ReflectionTestUtils.setField(subjectResource, "servletRequest", servletRequest); + ReflectionTestUtils.setField(subjectResource, "sourceService", sourceService); JwtAuthenticationFilter filter = new JwtAuthenticationFilter(); filter.init(new MockFilterConfig()); @@ -448,4 +456,54 @@ private MinimalSourceDetailsDTO createSourceWithoutDeviceId() { assertThat(sourceRegistrationDto.getSourceId()).isNull(); return sourceRegistrationDto; } + + @Test + @Transactional + public void updateSourceAttributes() throws Exception { + final int databaseSizeBeforeCreate = subjectRepository.findAll().size(); + + // Create the Subject + SubjectDTO subjectDto = createEntityDTO(em); + restSubjectMockMvc.perform(post("/api/subjects") + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(subjectDto))) + .andExpect(status().isCreated()); + + // Validate the Subject in the database + List subjectList = subjectRepository.findAll(); + assertThat(subjectList).hasSize(databaseSizeBeforeCreate + 1); + Subject testSubject = subjectList.get(subjectList.size() - 1); + + String subjectLogin = testSubject.getUser().getLogin(); + assertNotNull(subjectLogin); + + // Create a source description + MinimalSourceDetailsDTO sourceRegistrationDto = createSourceWithoutDeviceId(); + + MvcResult result = restSubjectMockMvc.perform(post("/api/subjects/{login}/sources", + subjectLogin) + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(sourceRegistrationDto))) + .andExpect(status().isOk()) + .andReturn(); + + MinimalSourceDetailsDTO value = (MinimalSourceDetailsDTO) + TestUtil.convertJsonStringToObject(result + .getResponse().getContentAsString() , MinimalSourceDetailsDTO.class); + + assertNotNull(value.getSourceName()); + + Map attributes = new HashMap<>(); + attributes.put("TEST_KEY" , "Value"); + attributes.put("ANDROID_VERSION" , "something"); + attributes.put("Other" , "test"); + + restSubjectMockMvc.perform(post( + "/api/subjects/{login}/sources/{sourceName}", subjectLogin, value.getSourceName()) + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(attributes))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.attributes").isNotEmpty()); + + } } diff --git a/src/test/java/org/radarcns/management/web/rest/TestUtil.java b/src/test/java/org/radarcns/management/web/rest/TestUtil.java index 6f14157a8..9335494ae 100644 --- a/src/test/java/org/radarcns/management/web/rest/TestUtil.java +++ b/src/test/java/org/radarcns/management/web/rest/TestUtil.java @@ -23,6 +23,27 @@ public class TestUtil { MediaType.APPLICATION_JSON.getType(), MediaType.APPLICATION_JSON.getSubtype(), Charset.forName("utf8")); + + private static final JavaTimeModule module = new JavaTimeModule(); + + private static final ObjectMapper mapper = new ObjectMapper() + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .registerModule(module); + + + /** + * Convert a JSON String to an object. + * + * @param json JSON String to convert. + * @param objectClass Object class to form. + * + * @return the converted object instance. + */ + public static Object convertJsonStringToObject(String json, Class objectClass) + throws IOException { + return mapper.readValue(json, objectClass); + } + /** * Convert an object to JSON byte array. * @@ -31,12 +52,6 @@ public class TestUtil { * @return the JSON byte array */ public static byte[] convertObjectToJsonBytes(Object object) throws IOException { - ObjectMapper mapper = new ObjectMapper(); - mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); - - JavaTimeModule module = new JavaTimeModule(); - mapper.registerModule(module); - return mapper.writeValueAsBytes(object); } From 2c44ff34a8c61b847012b979734053bd701f4fed Mon Sep 17 00:00:00 2001 From: nivethika Date: Tue, 10 Jul 2018 12:26:26 +0200 Subject: [PATCH 13/15] add one scenario to cover dynamic registration source registration --- .../web/rest/SubjectResourceIntTest.java | 39 +------------------ 1 file changed, 1 insertion(+), 38 deletions(-) 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 d17bf797a..f271f4495 100644 --- a/src/test/java/org/radarcns/management/web/rest/SubjectResourceIntTest.java +++ b/src/test/java/org/radarcns/management/web/rest/SubjectResourceIntTest.java @@ -382,43 +382,6 @@ public void dynamicSourceRegistrationWithId() throws Exception { .andExpect(status().is4xxClientError()); } - @Test - @Transactional - public void dynamicSourceRegistrationWithoutId() throws Exception { - final int databaseSizeBeforeCreate = subjectRepository.findAll().size(); - - // Create the Subject - SubjectDTO subjectDto = createEntityDTO(em); - restSubjectMockMvc.perform(post("/api/subjects") - .contentType(TestUtil.APPLICATION_JSON_UTF8) - .content(TestUtil.convertObjectToJsonBytes(subjectDto))) - .andExpect(status().isCreated()); - - // Validate the Subject in the database - List subjectList = subjectRepository.findAll(); - assertThat(subjectList).hasSize(databaseSizeBeforeCreate + 1); - Subject testSubject = subjectList.get(subjectList.size() - 1); - - String subjectLogin = testSubject.getUser().getLogin(); - assertNotNull(subjectLogin); - - // Create a source description - MinimalSourceDetailsDTO sourceRegistrationDto = createSourceWithoutDeviceId(); - - restSubjectMockMvc.perform(post("/api/subjects/{login}/sources", subjectLogin) - .contentType(TestUtil.APPLICATION_JSON_UTF8) - .content(TestUtil.convertObjectToJsonBytes(sourceRegistrationDto))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.sourceId").isNotEmpty()); - - // A source can not be assigned twice to a subject, so this call must fail - assertThat(sourceRegistrationDto.getSourceId()).isNull(); - restSubjectMockMvc.perform(post("/api/subjects/{login}/sources", subjectLogin) - .contentType(TestUtil.APPLICATION_JSON_UTF8) - .content(TestUtil.convertObjectToJsonBytes(sourceRegistrationDto))) - .andExpect(status().is4xxClientError()); - } - private MinimalSourceDetailsDTO createSourceWithDeviceId() { // Create a source description MinimalSourceDetailsDTO sourceRegistrationDto = new MinimalSourceDetailsDTO(); @@ -459,7 +422,7 @@ private MinimalSourceDetailsDTO createSourceWithoutDeviceId() { @Test @Transactional - public void updateSourceAttributes() throws Exception { + public void testDynamicRegistrationAndUpdateSourceAttributes() throws Exception { final int databaseSizeBeforeCreate = subjectRepository.findAll().size(); // Create the Subject From 94c5064b0d302f17e151150a1ce33b05894cd7a4 Mon Sep 17 00:00:00 2001 From: nivethika Date: Tue, 10 Jul 2018 12:30:14 +0200 Subject: [PATCH 14/15] remove exception that is not thrown --- .../org/radarcns/management/web/rest/SubjectResource.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/org/radarcns/management/web/rest/SubjectResource.java b/src/main/java/org/radarcns/management/web/rest/SubjectResource.java index 0b47fd1d8..fff51d6b1 100644 --- a/src/main/java/org/radarcns/management/web/rest/SubjectResource.java +++ b/src/main/java/org/radarcns/management/web/rest/SubjectResource.java @@ -46,7 +46,6 @@ import org.radarcns.management.service.dto.SourceTypeDTO; import org.radarcns.management.service.dto.SubjectDTO; import org.radarcns.management.service.mapper.SubjectMapper; -import org.radarcns.management.web.rest.errors.CustomConflictException; import org.radarcns.management.web.rest.errors.CustomNotFoundException; import org.radarcns.management.web.rest.errors.CustomParameterizedException; import org.radarcns.management.web.rest.errors.ErrorConstants; @@ -474,7 +473,6 @@ public ResponseEntity> getSubjectSources( * * @param attributes The {@link Map} specification * @return The {@link MinimalSourceDetailsDTO} completed with all identifying fields. - * @throws CustomConflictException if a source already available with the existing name. * @throws CustomNotFoundException if the subject or the source not found using given ids. */ @PostMapping("/subjects/{login:" + Constants.ENTITY_ID_REGEX + "}/sources/{sourceName:" @@ -487,7 +485,7 @@ public ResponseEntity> getSubjectSources( @Timed public ResponseEntity updateSubjectSource(@PathVariable String login, @PathVariable String sourceName, @RequestBody Map attributes) - throws CustomNotFoundException, CustomConflictException, NotAuthorizedException, + throws CustomNotFoundException, NotAuthorizedException, URISyntaxException { // check the subject id Optional subject = subjectRepository.findOneWithEagerBySubjectLogin(login); From f5ce9d61ef1085f53dc695790322855ac6c41725 Mon Sep 17 00:00:00 2001 From: nivethika Date: Tue, 10 Jul 2018 13:23:02 +0200 Subject: [PATCH 15/15] simplify merge --- .../management/service/SourceService.java | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/radarcns/management/service/SourceService.java b/src/main/java/org/radarcns/management/service/SourceService.java index cb7d8eb16..e4bb38a55 100644 --- a/src/main/java/org/radarcns/management/service/SourceService.java +++ b/src/main/java/org/radarcns/management/service/SourceService.java @@ -1,12 +1,11 @@ package org.radarcns.management.service; -import java.util.Collection; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; -import java.util.stream.Stream; import org.radarcns.management.domain.Source; import org.radarcns.management.repository.SourceRepository; @@ -147,19 +146,11 @@ public MinimalSourceDetailsDTO safeUpdate(Source sourceToUpdate, Map attributes) { // update source attributes - Map mergedValues = Stream.of(sourceToUpdate.getAttributes(), attributes) - .map(Map::entrySet) - .flatMap(Collection::stream) - .collect( - Collectors.toMap( - Map.Entry::getKey, - Map.Entry::getValue, - // such that if a value already exist - // for a given key, the value from the request is taken - (v1, v2) -> v2 - )); - - sourceToUpdate.setAttributes(mergedValues); + Map updatedAttributes = new HashMap<>(); + updatedAttributes.putAll(sourceToUpdate.getAttributes()); + updatedAttributes.putAll(attributes); + + sourceToUpdate.setAttributes(updatedAttributes); // rest of the properties should not be updated from this request. return sourceMapper.sourceToMinimalSourceDetailsDTO(sourceRepository.save(sourceToUpdate)); }