diff --git a/README.md b/README.md index 5e8ae19af..a994f0188 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-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/). @@ -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 | diff --git a/radar-auth/README.md b/radar-auth/README.md index db9773f01..99f05ef82 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.8' 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..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,26 +8,27 @@ import com.auth0.jwt.interfaces.DecodedJWT; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +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.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 +45,9 @@ public class TokenValidator { JwtRadarToken.GRANT_TYPE_CLAIM, JwtRadarToken.SCOPE_CLAIM); private final ServerConfig config; - private JWTVerifier verifier; + 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 @@ -110,39 +113,42 @@ 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 " + + "keys again. Token: {}", token); + refresh(); + return validateAccessToken(token); + } catch (JWTVerificationException ex) { + log.debug("Verifier {} with implementation {} did not accept token {}", + verifier.toString(), verifier.getClass().toString(), token); } - 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 +157,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,55 +175,54 @@ private JWTVerifier loadVerifier() throws TokenValidationException { lastFetch = Instant.now(); } - RSAPublicKey publicKey; - if (config.getPublicKey() == null) { - publicKey = publicKeyFromServer(); - } else { - publicKey = config.getPublicKey(); + List algorithms = new LinkedList<>(); + if (config.getPublicKeyEndpoints() != null) { + algorithms.addAll(config.getPublicKeyEndpoints().stream() + .map(this::algorithmFromServerPublicKey).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) { + algorithms.addAll(config.getPublicKeys().stream() + .map(this::algorithmFromString).collect(Collectors.toList())); + } + + // Create a verifier for each signature verification algorithm we created + return algorithms.stream().map(alg -> JWT.require(alg) .withAudience(config.getResourceName()) - .build(); + .build()) + .collect(Collectors.toList()); } - private RSAPublicKey publicKeyFromServer() throws TokenValidationException { - log.info("Getting the JWT public key at " + config.getPublicKeyEndpoint()); - + private Algorithm algorithmFromServerPublicKey(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 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")) { - throw new TokenValidationException("The identity server reported the following " - + "signing algorithm: " + publicKeyInfo.get("alg") - + ". Expected SHA256withRSA."); - } - - String keyString = publicKeyInfo.get("value").asText(); - return publicKeyFromString(keyString); + 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 publicKeyFromString(String keyString) throws TokenValidationException { - log.debug("Parsing 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 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 476c9f0b0..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,7 @@ package org.radarcns.auth.config; import java.net.URI; -import java.security.interfaces.RSAPublicKey; +import java.util.List; public interface ServerConfig { @@ -9,7 +9,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 +19,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. They should be in PEM format. + * @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..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,13 +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.interfaces.RSAPublicKey; -import java.security.spec.X509EncodedKeySpec; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; /** * Created by dverbeec on 14/06/2017. @@ -24,16 +22,14 @@ 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); - public YamlServerConfig() { - log.info("YamlServerConfig initializing..."); - } + /** * Read the configuration from file. This method will first check if the environment variable @@ -85,13 +81,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 +96,8 @@ public String getResourceName() { } @Override - public RSAPublicKey getPublicKey() { - return publicKey; + public List getPublicKeys() { + return publicKeys; } public void setResourceName(String resourceName) { @@ -109,44 +105,30 @@ 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; } @Override - public boolean equals(Object other) { - if (this == other) { + public boolean equals(Object o) { + if (this == o) { return true; } - if (!(other instanceof YamlServerConfig)) { - return false; - } - - YamlServerConfig that = (YamlServerConfig) other; - - if (!publicKeyEndpoint.equals(that.publicKeyEndpoint)) { + if (!(o instanceof YamlServerConfig)) { 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); } } 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/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..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 @@ -1,16 +1,21 @@ 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 org.radarcns.auth.token.validation.ECTokenValidationAlgorithm; +import org.radarcns.auth.token.validation.RSATokenValidationAlgorithm; import java.io.File; -import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.CoreMatchers.startsWith; +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 +26,29 @@ 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(); + assertThat(algs, hasItems(startsWith(new ECTokenValidationAlgorithm().getKeyHeader()), + startsWith(new RSATokenValidationAlgorithm().getKeyHeader()))); } } 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----- diff --git a/src/main/java/org/radarcns/management/config/LocalKeystoreConfig.java b/src/main/java/org/radarcns/management/config/LocalKeystoreConfig.java index bf0c4a4d2..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,15 @@ 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.RadarJwtAccessTokenConverter; +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.KeyPair; +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 +18,31 @@ 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()); + // 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 -> { + KeyPair keyPair = keyFactory.getKeyPair(alias); + converter.setKeyPair(keyPair); + return converter.getKey().get("value"); + }) + .collect(Collectors.toList()); } @Override - public URI getPublicKeyEndpoint() { - return null; + public List getPublicKeyEndpoints() { + return Collections.emptyList(); } @Override @@ -38,7 +51,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 07a27fdba..b3f4fdd35 100644 --- a/src/main/java/org/radarcns/management/config/OAuth2ServerConfiguration.java +++ b/src/main/java/org/radarcns/management/config/OAuth2ServerConfiguration.java @@ -1,15 +1,14 @@ package org.radarcns.management.config; -import static org.springframework.orm.jpa.vendor.Database.POSTGRESQL; - 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.PostgresApprovalStore; +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.boot.autoconfigure.orm.jpa.JpaProperties; @@ -27,6 +26,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; @@ -46,10 +47,19 @@ 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; + +import static org.springframework.orm.jpa.vendor.Database.POSTGRESQL; + @Configuration public class OAuth2ServerConfiguration { @@ -174,6 +184,9 @@ protected static class AuthorizationServerConfiguration extends @Autowired private JdbcClientDetailsService jdbcClientDetailsService; + @Autowired + private ManagementPortalProperties managementPortalProperties; + @Bean protected AuthorizationCodeServices authorizationCodeServices() { return new JdbcAuthorizationCodeServices(dataSource); @@ -201,13 +214,36 @@ 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); + // 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 76b94d118..9224ba7aa 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,11 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.data.repository.query.SecurityEvaluationContextExtension; +import javax.annotation.PostConstruct; +import javax.servlet.Filter; +import java.util.Collections; +import java.util.List; + @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) @@ -41,6 +45,9 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Autowired private ApplicationEventPublisher applicationEventPublisher; + @Autowired + private ManagementPortalProperties managementPortalProperties; + @PostConstruct public void init() { try { @@ -132,7 +139,22 @@ public FilterRegistrationBean jwtAuthenticationFilterRegistration() { return registration; } + /** + * Create a {@link JwtAuthenticationFilter}. + * + * @return the JwtAuthenticationFilter + */ public Filter jwtAuthenticationFilter() { - return new 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(), + publicKeyAliases))); } } diff --git a/src/main/java/org/radarcns/management/security/JwtAuthenticationFilter.java b/src/main/java/org/radarcns/management/security/JwtAuthenticationFilter.java index 06f69ed41..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 { @@ -42,6 +45,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() + "\"}"); } } 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..9a4f7c3a6 --- /dev/null +++ b/src/main/java/org/radarcns/management/security/jwt/MultiVerifier.java @@ -0,0 +1,46 @@ +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; +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 static final Logger log = LoggerFactory.getLogger(MultiVerifier.class); + 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) { + 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 " + + "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..a551ccc24 --- /dev/null +++ b/src/main/java/org/radarcns/management/security/jwt/RadarJwtAccessTokenConverter.java @@ -0,0 +1,99 @@ +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() { + // 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/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/java/org/radarcns/management/service/SourceService.java b/src/main/java/org/radarcns/management/service/SourceService.java index 6a8fa35e4..fbda777dd 100644 --- a/src/main/java/org/radarcns/management/service/SourceService.java +++ b/src/main/java/org/radarcns/management/service/SourceService.java @@ -1,5 +1,11 @@ package org.radarcns.management.service; +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 org.radarcns.management.domain.Source; import org.radarcns.management.repository.SourceRepository; import org.radarcns.management.service.dto.MinimalSourceDetailsDTO; @@ -15,13 +21,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -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; - /** * Service Implementation for managing Source. */ @@ -147,4 +146,24 @@ public List findAllMinimalSourceDetailsByProjectAndAssi .map(sourceMapper::sourceToMinimalSourceDetailsDTO) .collect(Collectors.toList()); } + + /** + * 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 attributes value to update + * @return Updated {@link MinimalSourceDetailsDTO} of source + */ + public MinimalSourceDetailsDTO safeUpdate(Source sourceToUpdate, + Map attributes) { + + // update source attributes + 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)); + } } 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 09a2853f1..24b491aca 100644 --- a/src/main/java/org/radarcns/management/web/rest/SourceResource.java +++ b/src/main/java/org/radarcns/management/web/rest/SourceResource.java @@ -132,7 +132,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 d2f7435e7..6ca4f0f3d 100644 --- a/src/main/java/org/radarcns/management/web/rest/SubjectResource.java +++ b/src/main/java/org/radarcns/management/web/rest/SubjectResource.java @@ -9,6 +9,7 @@ import org.radarcns.auth.exception.NotAuthorizedException; import org.radarcns.auth.token.RadarToken; 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; @@ -16,6 +17,7 @@ import org.radarcns.management.security.SecurityUtils; import org.radarcns.management.service.ResourceUriService; import org.radarcns.management.service.RevisionService; +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; @@ -23,6 +25,7 @@ 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.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; @@ -53,12 +56,15 @@ 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 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; @@ -103,6 +109,9 @@ public class SubjectResource { @Autowired private RevisionService revisionService; + @Autowired + private SourceService sourceService; + /** * POST /subjects : Create a new subject. * @@ -512,4 +521,61 @@ 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 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 attributes The {@link Map} specification + * @return The {@link MinimalSourceDetailsDTO} completed with all identifying fields. + * @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 Map attributes) + throws CustomNotFoundException, 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), attributes)); + } } diff --git a/src/main/resources/config/application.yml b/src/main/resources/config/application.yml index 7df061113..fe11faa27 100644 --- a/src/main/resources/config/application.yml +++ b/src/main/resources/config/application.yml @@ -98,6 +98,9 @@ info: managementportal: mail: # specific JHipster mail property, for standard properties see MailProperties from: ManagementPortal@localhost + oauth: + keyStorePassword: radarbase + signingKeyAlias: radarbase-managementportal-ec # =================================================================== # 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..bee4f4042 100644 --- a/src/test/java/org/radarcns/management/web/rest/OAuthHelper.java +++ b/src/test/java/org/radarcns/management/web/rest/OAuthHelper.java @@ -3,24 +3,35 @@ 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 DecodedJWT SUPER_USER_TOKEN; + 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 = {}; @@ -45,7 +56,19 @@ public class OAuthHelper { */ public static RequestPostProcessor bearerToken() { return mockRequest -> { - mockRequest.addHeader("Authorization", "Bearer " + VALID_TOKEN); + mockRequest.addHeader("Authorization", "Bearer " + validEcToken); + 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 " + validRsaToken); return mockRequest; }; } @@ -58,27 +81,41 @@ 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(); + validEcToken = createValidToken(Algorithm.ECDSA256(publicKey, privateKey)); + superUserToken = JWT.decode(validEcToken); - initValidToken(algorithm, exp, iat); + // 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(); + validRsaToken = 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, + 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 +130,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 dbce983fd..f4c960694 100644 --- a/src/test/java/org/radarcns/management/web/rest/ProjectResourceIntTest.java +++ b/src/test/java/org/radarcns/management/web/rest/ProjectResourceIntTest.java @@ -119,7 +119,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 b624f9983..5007d69aa 100644 --- a/src/test/java/org/radarcns/management/web/rest/SubjectResourceIntTest.java +++ b/src/test/java/org/radarcns/management/web/rest/SubjectResourceIntTest.java @@ -15,8 +15,10 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.util.Collections; -import java.util.List; import java.util.UUID; +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; @@ -50,6 +52,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; @@ -123,8 +126,9 @@ 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(); + JwtAuthenticationFilter filter = OAuthHelper.createAuthenticationFilter(); filter.init(new MockFilterConfig()); this.restSubjectMockMvc = MockMvcBuilders.standaloneSetup(subjectResource) @@ -565,4 +569,54 @@ private MinimalSourceDetailsDTO getSource() { assertThat(sourceRegistrationDto.getSourceId()).isNull(); return sourceRegistrationDto; } + + @Test + @Transactional + public void testDynamicRegistrationAndUpdateSourceAttributes() 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 = createSourceWithoutSourceTypeId(); + + 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 b171a9068..24e518d4f 100644 --- a/src/test/java/org/radarcns/management/web/rest/TestUtil.java +++ b/src/test/java/org/radarcns/management/web/rest/TestUtil.java @@ -1,19 +1,20 @@ package org.radarcns.management.web.rest; -import static org.assertj.core.api.Assertions.assertThat; - -import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import java.io.IOException; -import java.nio.charset.Charset; -import java.time.ZonedDateTime; -import java.time.format.DateTimeParseException; import org.hamcrest.Description; import org.hamcrest.TypeSafeDiagnosingMatcher; import org.springframework.http.MediaType; import org.springframework.test.context.transaction.TestTransaction; +import java.io.IOException; +import java.nio.charset.Charset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeParseException; + +import static org.assertj.core.api.Assertions.assertThat; + /** * Utility class for testing REST controllers. */ @@ -24,30 +25,35 @@ public class TestUtil { MediaType.APPLICATION_JSON.getType(), MediaType.APPLICATION_JSON.getSubtype(), Charset.forName("utf8")); - private static final ObjectMapper mapper = new ObjectMapper() - .setSerializationInclusion(Include.NON_NULL) - .registerModule(new JavaTimeModule()); + + private static final JavaTimeModule module = new JavaTimeModule(); + + private static final ObjectMapper mapper = new ObjectMapper() + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .registerModule(module); + /** - * Convert an object to JSON byte array. + * Convert a JSON String to an object. * - * @param object the object to convert - * @return the JSON byte array + * @param json JSON String to convert. + * @param objectClass Object class to form. + * + * @return the converted object instance. */ - public static byte[] convertObjectToJsonBytes(Object object) throws IOException { - return mapper.writeValueAsBytes(object); + public static Object convertJsonStringToObject(String json, Class objectClass) + throws IOException { + return mapper.readValue(json, objectClass); } /** * Convert an object to JSON byte array. * - * @param json string to convert - * @param objectClass Class to create an instance the object to convert + * @param object the object to convert * @return the JSON byte array */ - public static Object convertJsonStringToObject(String json, Class objectClass) throws - IOException { - return mapper.readValue(json, objectClass); + public static byte[] convertObjectToJsonBytes(Object object) throws IOException { + return mapper.writeValueAsBytes(object); } /** 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 1a3a58901..153f0fa68 100644 --- a/src/test/java/org/radarcns/management/web/rest/UserResourceIntTest.java +++ b/src/test/java/org/radarcns/management/web/rest/UserResourceIntTest.java @@ -111,7 +111,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 3c7389c3c..f3556d344 100644 --- a/src/test/resources/config/application.yml +++ b/src/test/resources/config/application.yml @@ -106,3 +106,8 @@ jhipster: # =================================================================== application: + +managementportal: + oauth: + keyStorePassword: radarbase + signingKeyAlias: radarbase-managementportal-ec diff --git a/src/test/resources/config/keystore.jks b/src/test/resources/config/keystore.jks index 041300b5c..aa6d01b64 100644 Binary files a/src/test/resources/config/keystore.jks and b/src/test/resources/config/keystore.jks differ