From b06e8df89258e135fe838e392289a0d77fab591c Mon Sep 17 00:00:00 2001 From: Denny Verbeeck Date: Thu, 7 Jun 2018 16:19:43 +0200 Subject: [PATCH] Allow for multiple public keys and multiple public key endpoints in radar-auth Also allow for EC public keys next to RSA public keys. This will allow clients of the radar-auth library to use any combination of public keys and server public key endpoints, increasing the flexibility. This will also be necessary when MP switches to EC public keys, since there will be a transition period where there are clients with RSA signatures and clients with EC signatures. --- radar-auth/README.md | 31 ++-- .../auth/authentication/TokenValidator.java | 152 +++++++++++------- .../radarcns/auth/config/ServerConfig.java | 11 +- .../auth/config/YamlServerConfig.java | 97 ++++++----- .../authentication/TokenValidatorTest.java | 4 +- .../auth/config/YamlServerConfigTest.java | 42 ++--- .../radarcns/auth/util/TokenTestUtils.java | 6 +- radar-auth/src/test/resources/radar-is-2.yml | 13 +- radar-auth/src/test/resources/radar-is.yml | 13 +- 9 files changed, 225 insertions(+), 144 deletions(-) diff --git a/radar-auth/README.md b/radar-auth/README.md index c1f104130..6a61cbbcb 100644 --- a/radar-auth/README.md +++ b/radar-auth/README.md @@ -15,30 +15,37 @@ compile group: 'org.radarcns', name: 'radar-auth', version: '0.3.6' The library expects the identity server configuration in a file called `radar-is.yml`. Either set the environment variable `RADAR_IS_CONFIG_LOCATION` to the full path of the file, or put the file -somewhere on the classpath. The file should define `resourceName` and either of `publicKeyEndpoint` -or `publicKey`. If both are specified, `publicKey` has the priority. +somewhere on the classpath. The file should define `resourceName` and at least one of +`publicKeyEndpoints` or `publicKeys`. You can specify both, than public keys fetched from the +endpoints as well as public keys defined in the file will be used for validation. -| Variable name | Description | -|---------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `resourceName` | The name of this resource. It has to appear in the `audience` claim of a JWT token in order for the token to be accepted. | -| `publicKeyEndpoint` | Server endpoint that provides the public key of the keypair used to sign the JWTs. The expected response from this endpoint is a JSON structure containing two fields: `alg` and `value`, where `alg` should be equal to `SHA256withRSA`, and `value` should be equal to the public key in PEM format. | -| `publicKey` | PEM formatted public key for JWT validation. You can use YAML [literal style] to conveniently specify a multiline value for a variable. Also handy for testing scenario's where you don't necessarily have access to the public key endpoint. | +| Variable name | Description | +|---------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `resourceName` | The name of this resource. It has to appear in the `audience` claim of a JWT token in order for the token to be accepted. | +| `publicKeyEndpoints` | List of server endpoints that provide the public key of the keypair used to sign the JWTs. The expected response from this endpoint is a JSON structure containing two fields: `alg` and `value`, where `alg` should be equal to `SHA256withRSA` or ``SHA256withECDSA`, and `value` should be equal to the public key in PEM format. | +| `publicKeys` | List of PEM formatted public keys for JWT validation. You can use YAML [literal style] to conveniently specify a multiline value for a variable. Also handy for testing scenario's where you don't necessarily have access to the public key endpoint. Entries can be RSA or EC public keys. | For example: ```yaml resourceName: resource_name -publicKeyEndpoint: http://localhost:8080/oauth/token_key +publicKeyEndpoints: + - http://localhost:8080/oauth/token_key ``` or ```yaml resourceName: res_ManagementPortal -publicKey: |- - -----BEGIN PUBLIC KEY----- - MIICHDANBgkqhkiG9w0BAQEFAAOCAgkAMIICBAKCAfsAqM4o+hVAdF2QATQBmpehSMyhdqKvwh9mrfnxDNtctZYlpiQXMbq4uqRgp98aBy6bMKKr3k0rSXTzr27Y+tdLUWXqbl4y8kKm8rGZo9gTbPyhqPm4f4OIxMRJcuhQ7f8qBY87w9buzClQeUs3h5f+DUVRUfB9FnDtim+ma3mFqYh38TMnrBapCtG+7iVKRFgGv6JWiNTql+oVBPNuUX3koc5/zO6IhrD49vBbsjaRWTJV2xMNll82gPvVLtgQNA2t7iGnUPhfKDj1NInZeg79NzFnWAa9Jtc1r2Q7D68MiJhYZN2QAlZS1GfbELnRAeUmSxT5i3BHu23iz9zluhIhYe1vhA1QWk2HsriGL9w+iFqzYlk5P3GCAE+nfNmM/6GIp1ehzW+/4+xgik5rOakCWw4vewmSBWOrV/XZvT2ZT3AA6zIByWdERyMOVJmd9rqPH1FIDtQk8h2jFTqIvBda727DHXeUB9J4hHQTzQmvOxPMipwDslxWOjnG4nbq6Exme0o/ELMOxt+4APH6KW+LqCNl5jGdbKxySLQyNgfUjhXJ06U1b8JHPheTnWcKO+cMmhyheUkZmLMLK2mlAsR+JJeBDY1/jd7+q6hgymeJzoDoXJj4LARiYZ+StRr/E0+P8DrprWYZPi496VIzwgV8otV9fVz29V501rcCAwEAAQ== - -----END PUBLIC KEY----- +publicKeys: + - |- + -----BEGIN PUBLIC KEY----- + MIICHDANBgkqhkiG9w0BAQEFAAOCAgkAMIICBAKCAfsAqM4o+hVAdF2QATQBmpehSMyhdqKvwh9mrfnxDNtctZYlpiQXMbq4uqRgp98aBy6bMKKr3k0rSXTzr27Y+tdLUWXqbl4y8kKm8rGZo9gTbPyhqPm4f4OIxMRJcuhQ7f8qBY87w9buzClQeUs3h5f+DUVRUfB9FnDtim+ma3mFqYh38TMnrBapCtG+7iVKRFgGv6JWiNTql+oVBPNuUX3koc5/zO6IhrD49vBbsjaRWTJV2xMNll82gPvVLtgQNA2t7iGnUPhfKDj1NInZeg79NzFnWAa9Jtc1r2Q7D68MiJhYZN2QAlZS1GfbELnRAeUmSxT5i3BHu23iz9zluhIhYe1vhA1QWk2HsriGL9w+iFqzYlk5P3GCAE+nfNmM/6GIp1ehzW+/4+xgik5rOakCWw4vewmSBWOrV/XZvT2ZT3AA6zIByWdERyMOVJmd9rqPH1FIDtQk8h2jFTqIvBda727DHXeUB9J4hHQTzQmvOxPMipwDslxWOjnG4nbq6Exme0o/ELMOxt+4APH6KW+LqCNl5jGdbKxySLQyNgfUjhXJ06U1b8JHPheTnWcKO+cMmhyheUkZmLMLK2mlAsR+JJeBDY1/jd7+q6hgymeJzoDoXJj4LARiYZ+StRr/E0+P8DrprWYZPi496VIzwgV8otV9fVz29V501rcCAwEAAQ== + -----END PUBLIC KEY----- + - |- + -----BEGIN EC PUBLIC KEY----- + MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvmBia5inhASHAVrFBB5JAh0ne/aKb6z/sCIuWzKp/azFcD/OPJ2H6RPLn3t7XA4oAa2FR3GB4ZhU7SCh20FUhA== + -----END EC PUBLIC KEY----- ``` Usage diff --git a/radar-auth/src/main/java/org/radarcns/auth/authentication/TokenValidator.java b/radar-auth/src/main/java/org/radarcns/auth/authentication/TokenValidator.java index 4d9b682f2..62be0efdd 100644 --- a/radar-auth/src/main/java/org/radarcns/auth/authentication/TokenValidator.java +++ b/radar-auth/src/main/java/org/radarcns/auth/authentication/TokenValidator.java @@ -8,26 +8,31 @@ import com.auth0.jwt.interfaces.DecodedJWT; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import org.bouncycastle.util.io.pem.PemReader; +import org.radarcns.auth.config.ServerConfig; +import org.radarcns.auth.config.YamlServerConfig; +import org.radarcns.auth.exception.TokenValidationException; +import org.radarcns.auth.token.JwtRadarToken; +import org.radarcns.auth.token.RadarToken; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.InputStream; import java.io.StringReader; +import java.net.URI; import java.net.URLConnection; import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAPublicKey; import java.security.spec.X509EncodedKeySpec; import java.time.Duration; import java.time.Instant; import java.util.Arrays; +import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.stream.Collectors; -import org.bouncycastle.util.io.pem.PemReader; -import org.radarcns.auth.config.ServerConfig; -import org.radarcns.auth.config.YamlServerConfig; -import org.radarcns.auth.exception.TokenValidationException; -import org.radarcns.auth.token.JwtRadarToken; -import org.radarcns.auth.token.RadarToken; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Validates JWT token signed by the Management Portal. It is synchronized and may be used from @@ -44,7 +49,7 @@ public class TokenValidator { JwtRadarToken.GRANT_TYPE_CLAIM, JwtRadarToken.SCOPE_CLAIM); private final ServerConfig config; - private JWTVerifier verifier; + private List verifiers = new LinkedList<>(); // If a client presents a token with an invalid signature, it might be the keypair was changed. // In that case we need to fetch it again, but we don't want a malicious client to be able to @@ -110,39 +115,41 @@ public TokenValidator(ServerConfig config, long fetchTimeout) { * @throws TokenValidationException If the token can not be validated. */ public RadarToken validateAccessToken(String token) throws TokenValidationException { - try { - DecodedJWT jwt = getVerifier().verify(token); - Set claims = jwt.getClaims().keySet(); - Set missing = REQUIRED_CLAIMS.stream() - .filter(c -> !claims.contains(c)) - .collect(Collectors.toSet()); - if (!missing.isEmpty()) { - throw new TokenValidationException("The following required claims were missing " - + "from the token: " + String.join(", ", missing)); + for (JWTVerifier verifier : getVerifiers()) { + try { + DecodedJWT jwt = verifier.verify(token); + Set claims = jwt.getClaims().keySet(); + Set missing = REQUIRED_CLAIMS.stream() + .filter(c -> !claims.contains(c)).collect(Collectors.toSet()); + if (!missing.isEmpty()) { + throw new TokenValidationException("The following required claims were " + + "missing from the token: " + String.join(", ", missing)); + } + return new JwtRadarToken(jwt); + } catch (SignatureVerificationException sve) { + log.warn("Client presented a token with an incorrect signature, fetching public key" + + " again. Token: {}", token); + refresh(); + return validateAccessToken(token); + } catch (JWTVerificationException ex) { + throw new TokenValidationException(ex); } - return new JwtRadarToken(jwt); - } catch (SignatureVerificationException sve) { - log.warn("Client presented a token with an incorrect signature, fetching public key" - + " again. Token: {}", token); - refresh(); - return validateAccessToken(token); - } catch (JWTVerificationException ex) { - throw new TokenValidationException(ex); } + throw new TokenValidationException("No registered validator could authenticate this token"); } - private JWTVerifier getVerifier() { + private List getVerifiers() { synchronized (this) { - if (verifier != null) { - return verifier; + if (!verifiers.isEmpty()) { + return verifiers; } } - JWTVerifier localVerifier = loadVerifier(); + List localVerifiers = loadVerifiers(); synchronized (this) { - verifier = localVerifier; - return verifier; + verifiers = localVerifiers; + return verifiers; } } @@ -151,13 +158,13 @@ private JWTVerifier getVerifier() { * @throws TokenValidationException if the public key could not be refreshed. */ public void refresh() throws TokenValidationException { - JWTVerifier localVerifier = loadVerifier(); + List localVerifiers = loadVerifiers(); synchronized (this) { - this.verifier = localVerifier; + this.verifiers = localVerifiers; } } - private JWTVerifier loadVerifier() throws TokenValidationException { + private List loadVerifiers() throws TokenValidationException { synchronized (this) { // whether successful or not, do not request the key more than once per minute if (Instant.now().isBefore(lastFetch.plus(fetchTimeout))) { @@ -169,47 +176,49 @@ private JWTVerifier loadVerifier() throws TokenValidationException { lastFetch = Instant.now(); } - RSAPublicKey publicKey; - if (config.getPublicKey() == null) { - publicKey = publicKeyFromServer(); - } else { - publicKey = config.getPublicKey(); + List publicKeys = new LinkedList<>(); + if (config.getPublicKeyEndpoints() != null) { + publicKeys.addAll(config.getPublicKeyEndpoints().stream() + .map(this::publicKeyFromServer).collect(Collectors.toList())); } - Algorithm alg = Algorithm.RSA256(publicKey, null); - // we successfully fetched the public key, reset the timer - return JWT.require(alg) + if (config.getPublicKeys() != null) { + publicKeys.addAll(config.getPublicKeys()); + } + + // Create a verifier for each public key we have in our config + return publicKeys.stream().map(key -> JWT.require(publicKeyToAlgorithm(key)) .withAudience(config.getResourceName()) - .build(); + .build()) + .collect(Collectors.toList()); } - private RSAPublicKey publicKeyFromServer() throws TokenValidationException { - log.info("Getting the JWT public key at " + config.getPublicKeyEndpoint()); - + private PublicKey publicKeyFromServer(URI serverUri) throws TokenValidationException { + log.info("Getting the JWT public key at " + serverUri); try { - URLConnection connection = config.getPublicKeyEndpoint().toURL().openConnection(); + URLConnection connection = serverUri.toURL().openConnection(); connection.setRequestProperty("Accept", "application/json"); try (InputStream inputStream = connection.getInputStream()) { ObjectMapper mapper = new ObjectMapper(); JsonNode publicKeyInfo = mapper.readTree(inputStream); - - // We expect RSA algorithm, and deny to trust the public key otherwise, see also + // We expect RSA or ECDSA algorithm, and deny to trust the public key otherwise // https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/ - if (!publicKeyInfo.get("alg").asText().equals("SHA256withRSA")) { - throw new TokenValidationException("The identity server reported the following " - + "signing algorithm: " + publicKeyInfo.get("alg") - + ". Expected SHA256withRSA."); + if (publicKeyInfo.get("alg").asText().equals("SHA256withRSA")) { + return rsaPublicKeyFromString(publicKeyInfo.get("value").asText()); } - - String keyString = publicKeyInfo.get("value").asText(); - return publicKeyFromString(keyString); + if (publicKeyInfo.get("alg").asText().equals("SHA256withECDSA")) { + return ecPublicKeyFromString(publicKeyInfo.get("value").asText()); + } + throw new TokenValidationException("The identity server reported the following " + + "signing algorithm: " + publicKeyInfo.get("alg") + + ". Expected SHA256withRSA or SHA256withECDSA."); } } catch (Exception ex) { throw new TokenValidationException(ex); } } - private RSAPublicKey publicKeyFromString(String keyString) throws TokenValidationException { - log.debug("Parsing public key: " + keyString); + private RSAPublicKey rsaPublicKeyFromString(String keyString) throws TokenValidationException { + log.debug("Parsing RSA public key: " + keyString); try (PemReader pemReader = new PemReader(new StringReader(keyString))) { byte[] keyBytes = pemReader.readPemObject().getContent(); pemReader.close(); @@ -220,4 +229,29 @@ private RSAPublicKey publicKeyFromString(String keyString) throws TokenValidatio throw new TokenValidationException(ex); } } + + private ECPublicKey ecPublicKeyFromString(String keyString) throws TokenValidationException { + log.debug("Parsing EC public key: " + keyString); + try (PemReader pemReader = new PemReader(new StringReader(keyString))) { + byte[] keyBytes = pemReader.readPemObject().getContent(); + pemReader.close(); + X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); + KeyFactory kf = KeyFactory.getInstance("ECDSA"); + return (ECPublicKey) kf.generatePublic(spec); + } catch (Exception ex) { + throw new TokenValidationException(ex); + } + } + + private Algorithm publicKeyToAlgorithm(PublicKey key) throws IllegalArgumentException { + switch (key.getAlgorithm()) { + case "EC": + return Algorithm.ECDSA256((ECPublicKey) key, null); + case "RSA": + return Algorithm.RSA256((RSAPublicKey) key, null); + default: + throw new IllegalArgumentException("Unsupported public key algorithm: {}. " + + "Expected either EC or RSA."); + } + } } diff --git a/radar-auth/src/main/java/org/radarcns/auth/config/ServerConfig.java b/radar-auth/src/main/java/org/radarcns/auth/config/ServerConfig.java index 476c9f0b0..9f0035427 100644 --- a/radar-auth/src/main/java/org/radarcns/auth/config/ServerConfig.java +++ b/radar-auth/src/main/java/org/radarcns/auth/config/ServerConfig.java @@ -1,7 +1,8 @@ package org.radarcns.auth.config; import java.net.URI; -import java.security.interfaces.RSAPublicKey; +import java.security.PublicKey; +import java.util.List; public interface ServerConfig { @@ -9,7 +10,7 @@ public interface ServerConfig { * Get the public key endpoint as a URI. * @return The public key endpoint URI, or null if not defined */ - URI getPublicKeyEndpoint(); + List getPublicKeyEndpoints(); /** * The name of this resource. It should be in the list of allowed resources for the OAuth @@ -19,9 +20,9 @@ public interface ServerConfig { String getResourceName(); /** - * Get the public key set in the config file. - * @return The public key, or null if not defined + * Get the public keys set in the config file. + * @return The public keys, or null if not defined */ - RSAPublicKey getPublicKey(); + List getPublicKeys(); } diff --git a/radar-auth/src/main/java/org/radarcns/auth/config/YamlServerConfig.java b/radar-auth/src/main/java/org/radarcns/auth/config/YamlServerConfig.java index 86854b7bf..90ae8afa5 100644 --- a/radar-auth/src/main/java/org/radarcns/auth/config/YamlServerConfig.java +++ b/radar-auth/src/main/java/org/radarcns/auth/config/YamlServerConfig.java @@ -15,8 +15,14 @@ import java.net.URI; import java.net.URL; import java.security.KeyFactory; -import java.security.interfaces.RSAPublicKey; +import java.security.PublicKey; import java.security.spec.X509EncodedKeySpec; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; /** * Created by dverbeec on 14/06/2017. @@ -24,14 +30,23 @@ public class YamlServerConfig implements ServerConfig { public static final String LOCATION_ENV = "RADAR_IS_CONFIG_LOCATION"; public static final String CONFIG_FILE_NAME = "radar-is.yml"; - private URI publicKeyEndpoint; + private List publicKeyEndpoints = new LinkedList<>(); private String resourceName; - private RSAPublicKey publicKey; + private List publicKeys = new LinkedList<>(); private static YamlServerConfig config; private final Logger log = LoggerFactory.getLogger(YamlServerConfig.class); + // a map with as key the string to search for in a PEM encoded public key, and as value the + // KeyFactory type to request + private final Map keyFactoryTypes = new HashMap<>(); + + /** + * Default constructor. Initializes the keyFactoryTypes map. + */ public YamlServerConfig() { + keyFactoryTypes.put("-----BEGIN PUBLIC KEY-----", "RSA"); + keyFactoryTypes.put("-----BEGIN EC PUBLIC KEY-----", "EC"); log.info("YamlServerConfig initializing..."); } @@ -85,13 +100,13 @@ public static YamlServerConfig reloadConfig() { return readFromFileOrClasspath(); } - public URI getPublicKeyEndpoint() { - return publicKeyEndpoint; + public List getPublicKeyEndpoints() { + return publicKeyEndpoints; } - public void setPublicKeyEndpoint(URI publicKeyEndpoint) { - log.info("Token public key endpoint set to " + publicKeyEndpoint.toString()); - this.publicKeyEndpoint = publicKeyEndpoint; + public void setPublicKeyEndpoints(List publicKeyEndpoints) { + log.info("Token public key endpoints set to " + publicKeyEndpoints.toString()); + this.publicKeyEndpoints = publicKeyEndpoints; } @Override @@ -100,8 +115,8 @@ public String getResourceName() { } @Override - public RSAPublicKey getPublicKey() { - return publicKey; + public List getPublicKeys() { + return publicKeys; } public void setResourceName(String resourceName) { @@ -109,44 +124,52 @@ public void setResourceName(String resourceName) { } /** - * Set the public key. This method converts the public key from a PEM formatted string to a - * {@link RSAPublicKey} format. - * @param publicKey The PEM formatted public key + * Set the public keys. This method will detect the public key type (EC or RSA) and parse + * accordingly. + * @param publicKeys The public keys to parse */ - public void setPublicKey(String publicKey) { - log.debug("Parsing public key: " + publicKey); - try (PemReader pemReader = new PemReader(new StringReader(publicKey))) { - byte[] keyBytes = pemReader.readPemObject().getContent(); - pemReader.close(); - X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); - KeyFactory kf = KeyFactory.getInstance("RSA"); - this.publicKey = (RSAPublicKey) kf.generatePublic(spec); - } catch (Exception ex) { - throw new ConfigurationException(ex); - } + public void setPublicKeys(List publicKeys) { + this.publicKeys = publicKeys.stream().map(this::parseKey).collect(Collectors.toList()); } @Override - public boolean equals(Object other) { - if (this == other) { + public boolean equals(Object o) { + if (this == o) { return true; } - if (!(other instanceof YamlServerConfig)) { + if (!(o instanceof YamlServerConfig)) { return false; } - - YamlServerConfig that = (YamlServerConfig) other; - - if (!publicKeyEndpoint.equals(that.publicKeyEndpoint)) { - return false; - } - return resourceName.equals(that.resourceName); + YamlServerConfig that = (YamlServerConfig) o; + return Objects.equals(publicKeyEndpoints, that.publicKeyEndpoints) + && Objects.equals(resourceName, that.resourceName) + && Objects.equals(publicKeys, that.publicKeys); } @Override public int hashCode() { - int result = publicKeyEndpoint.hashCode(); - result = 31 * result + resourceName.hashCode(); - return result; + return Objects.hash(publicKeyEndpoints, resourceName, publicKeys); + } + + private PublicKey parseKey(String publicKey) { + String factoryType = keyFactoryTypes.keySet().stream() + // find the string that is contained in publicKey + .filter(publicKey::contains) + .findFirst() + // get the actual factory type + .map(keyFactoryTypes::get) + // if not found throw a ConfigurationException + .orElseThrow(() -> new ConfigurationException("Unsupported public key: " + + publicKey)); + log.debug("Parsing {} public key: {}", factoryType, publicKey); + try (PemReader pemReader = new PemReader(new StringReader(publicKey))) { + byte[] keyBytes = pemReader.readPemObject().getContent(); + pemReader.close(); + X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); + KeyFactory kf = KeyFactory.getInstance(factoryType); + return kf.generatePublic(spec); + } catch (Exception ex) { + throw new ConfigurationException(ex); + } } } diff --git a/radar-auth/src/test/java/org/radarcns/auth/authentication/TokenValidatorTest.java b/radar-auth/src/test/java/org/radarcns/auth/authentication/TokenValidatorTest.java index 418d349eb..0d7a44044 100644 --- a/radar-auth/src/test/java/org/radarcns/auth/authentication/TokenValidatorTest.java +++ b/radar-auth/src/test/java/org/radarcns/auth/authentication/TokenValidatorTest.java @@ -52,7 +52,7 @@ public void setUp() throws Exception { @Test public void testValidToken() { - validator.validateAccessToken(TokenTestUtils.VALID_TOKEN); + validator.validateAccessToken(TokenTestUtils.VALID_RSA_TOKEN); } @Test(expected = TokenValidationException.class) @@ -77,6 +77,6 @@ public void testPublicKeyFromConfigFile() { environmentVariables.set(YamlServerConfig.LOCATION_ENV, configFile.getAbsolutePath()); // reinitialize TokenValidator to pick up new config validator = new TokenValidator(); - validator.validateAccessToken(TokenTestUtils.VALID_TOKEN); + validator.validateAccessToken(TokenTestUtils.VALID_RSA_TOKEN); } } diff --git a/radar-auth/src/test/java/org/radarcns/auth/config/YamlServerConfigTest.java b/radar-auth/src/test/java/org/radarcns/auth/config/YamlServerConfigTest.java index 1302109cf..6523734ce 100644 --- a/radar-auth/src/test/java/org/radarcns/auth/config/YamlServerConfigTest.java +++ b/radar-auth/src/test/java/org/radarcns/auth/config/YamlServerConfigTest.java @@ -1,16 +1,19 @@ package org.radarcns.auth.config; -import org.apache.commons.codec.binary.Base64; import org.junit.Rule; import org.junit.Test; import org.junit.contrib.java.lang.system.EnvironmentVariables; -import org.radarcns.auth.util.TokenTestUtils; import java.io.File; -import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.stream.Collectors; +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; + /** * Created by dverbeec on 19/06/2017. @@ -21,33 +24,30 @@ public class YamlServerConfigTest { public final EnvironmentVariables environmentVariables = new EnvironmentVariables(); @Test - public void testLoadYamlFileFromClasspath() throws IOException { + public void testLoadYamlFileFromClasspath() throws URISyntaxException { ServerConfig config = YamlServerConfig.readFromFileOrClasspath(); - assertEquals("http://localhost:8089/oauth/token_key", config.getPublicKeyEndpoint().toString()); - assertEquals("unit_test", config.getResourceName()); + checkConfig(config); } @Test - public void testLoadYamlFileFromEnv() throws IOException { + public void testLoadYamlFileFromEnv() throws URISyntaxException { ClassLoader loader = getClass().getClassLoader(); File configFile = new File(loader.getResource(YamlServerConfig.CONFIG_FILE_NAME).getFile()); environmentVariables.set(YamlServerConfig.LOCATION_ENV, configFile.getAbsolutePath()); ServerConfig config = YamlServerConfig.readFromFileOrClasspath(); - assertEquals("http://localhost:8089/oauth/token_key", config.getPublicKeyEndpoint().toString()); - assertNull(config.getPublicKey()); - assertEquals("unit_test", config.getResourceName()); + checkConfig(config); } - @Test - public void testLoadYamlFileWithPublicKey() throws Exception { - ClassLoader loader = getClass().getClassLoader(); - File configFile = new File(loader.getResource("radar-is-2.yml").getFile()); - environmentVariables.set(YamlServerConfig.LOCATION_ENV, configFile.getAbsolutePath()); - ServerConfig config = YamlServerConfig.readFromFileOrClasspath(); - TokenTestUtils.setUp(); - assertEquals(TokenTestUtils.PUBLIC_KEY_STRING, new String(new Base64().encode(config - .getPublicKey().getEncoded()))); - assertNull(config.getPublicKeyEndpoint()); + private void checkConfig(ServerConfig config) throws URISyntaxException { + List uris = config.getPublicKeyEndpoints(); + assertThat(uris, hasItems(new URI("http://localhost:8089/oauth/token_key"), + new URI("http://localhost:8089/oauth/token_key"))); + assertEquals(2, uris.size()); assertEquals("unit_test", config.getResourceName()); + assertEquals(2, config.getPublicKeys().size()); + List algs = config.getPublicKeys().stream() + .map(key -> key.getAlgorithm()) + .collect(Collectors.toList()); + assertThat(algs, hasItems("RSA", "EC")); } } diff --git a/radar-auth/src/test/java/org/radarcns/auth/util/TokenTestUtils.java b/radar-auth/src/test/java/org/radarcns/auth/util/TokenTestUtils.java index dcb66c435..a7642c821 100644 --- a/radar-auth/src/test/java/org/radarcns/auth/util/TokenTestUtils.java +++ b/radar-auth/src/test/java/org/radarcns/auth/util/TokenTestUtils.java @@ -21,7 +21,7 @@ public class TokenTestUtils { public static final String PUBLIC_KEY = "/oauth/token_key"; public static String PUBLIC_KEY_BODY; - public static String VALID_TOKEN; + public static String VALID_RSA_TOKEN; public static String INCORRECT_AUDIENCE_TOKEN; public static String EXPIRED_TOKEN; public static String INCORRECT_ALGORITHM_TOKEN; @@ -189,7 +189,7 @@ private static void initProjectAdminToken(Algorithm algorithm, Instant exp, Inst } private static void initValidToken(Algorithm algorithm, Instant exp, Instant iat) { - VALID_TOKEN = JWT.create() + VALID_RSA_TOKEN = JWT.create() .withIssuer(ISS) .withIssuedAt(Date.from(iat)) .withExpiresAt(Date.from(exp)) @@ -204,7 +204,7 @@ private static void initValidToken(Algorithm algorithm, Instant exp, Instant iat .withClaim("jti", JTI) .withClaim("grant_type", "password") .sign(algorithm); - SUPER_USER_TOKEN = JWT.decode(VALID_TOKEN); + SUPER_USER_TOKEN = JWT.decode(VALID_RSA_TOKEN); } private static void initTokenWithScopes(Algorithm algorithm, Instant exp, Instant iat) { diff --git a/radar-auth/src/test/resources/radar-is-2.yml b/radar-auth/src/test/resources/radar-is-2.yml index 3b9427f72..52feaa416 100644 --- a/radar-auth/src/test/resources/radar-is-2.yml +++ b/radar-auth/src/test/resources/radar-is-2.yml @@ -1,5 +1,10 @@ resourceName: unit_test -publicKey: |- - -----BEGIN PUBLIC KEY----- - MIICHDANBgkqhkiG9w0BAQEFAAOCAgkAMIICBAKCAfsAqM4o+hVAdF2QATQBmpehSMyhdqKvwh9mrfnxDNtctZYlpiQXMbq4uqRgp98aBy6bMKKr3k0rSXTzr27Y+tdLUWXqbl4y8kKm8rGZo9gTbPyhqPm4f4OIxMRJcuhQ7f8qBY87w9buzClQeUs3h5f+DUVRUfB9FnDtim+ma3mFqYh38TMnrBapCtG+7iVKRFgGv6JWiNTql+oVBPNuUX3koc5/zO6IhrD49vBbsjaRWTJV2xMNll82gPvVLtgQNA2t7iGnUPhfKDj1NInZeg79NzFnWAa9Jtc1r2Q7D68MiJhYZN2QAlZS1GfbELnRAeUmSxT5i3BHu23iz9zluhIhYe1vhA1QWk2HsriGL9w+iFqzYlk5P3GCAE+nfNmM/6GIp1ehzW+/4+xgik5rOakCWw4vewmSBWOrV/XZvT2ZT3AA6zIByWdERyMOVJmd9rqPH1FIDtQk8h2jFTqIvBda727DHXeUB9J4hHQTzQmvOxPMipwDslxWOjnG4nbq6Exme0o/ELMOxt+4APH6KW+LqCNl5jGdbKxySLQyNgfUjhXJ06U1b8JHPheTnWcKO+cMmhyheUkZmLMLK2mlAsR+JJeBDY1/jd7+q6hgymeJzoDoXJj4LARiYZ+StRr/E0+P8DrprWYZPi496VIzwgV8otV9fVz29V501rcCAwEAAQ== - -----END PUBLIC KEY----- +publicKeys: + - |- + -----BEGIN PUBLIC KEY----- + MIICHDANBgkqhkiG9w0BAQEFAAOCAgkAMIICBAKCAfsAqM4o+hVAdF2QATQBmpehSMyhdqKvwh9mrfnxDNtctZYlpiQXMbq4uqRgp98aBy6bMKKr3k0rSXTzr27Y+tdLUWXqbl4y8kKm8rGZo9gTbPyhqPm4f4OIxMRJcuhQ7f8qBY87w9buzClQeUs3h5f+DUVRUfB9FnDtim+ma3mFqYh38TMnrBapCtG+7iVKRFgGv6JWiNTql+oVBPNuUX3koc5/zO6IhrD49vBbsjaRWTJV2xMNll82gPvVLtgQNA2t7iGnUPhfKDj1NInZeg79NzFnWAa9Jtc1r2Q7D68MiJhYZN2QAlZS1GfbELnRAeUmSxT5i3BHu23iz9zluhIhYe1vhA1QWk2HsriGL9w+iFqzYlk5P3GCAE+nfNmM/6GIp1ehzW+/4+xgik5rOakCWw4vewmSBWOrV/XZvT2ZT3AA6zIByWdERyMOVJmd9rqPH1FIDtQk8h2jFTqIvBda727DHXeUB9J4hHQTzQmvOxPMipwDslxWOjnG4nbq6Exme0o/ELMOxt+4APH6KW+LqCNl5jGdbKxySLQyNgfUjhXJ06U1b8JHPheTnWcKO+cMmhyheUkZmLMLK2mlAsR+JJeBDY1/jd7+q6hgymeJzoDoXJj4LARiYZ+StRr/E0+P8DrprWYZPi496VIzwgV8otV9fVz29V501rcCAwEAAQ== + -----END PUBLIC KEY----- + - |- + -----BEGIN EC PUBLIC KEY----- + MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvmBia5inhASHAVrFBB5JAh0ne/aKb6z/sCIuWzKp/azFcD/OPJ2H6RPLn3t7XA4oAa2FR3GB4ZhU7SCh20FUhA== + -----END EC PUBLIC KEY----- diff --git a/radar-auth/src/test/resources/radar-is.yml b/radar-auth/src/test/resources/radar-is.yml index 4947233d1..990dbd032 100644 --- a/radar-auth/src/test/resources/radar-is.yml +++ b/radar-auth/src/test/resources/radar-is.yml @@ -1,2 +1,13 @@ resourceName: unit_test -publicKeyEndpoint: http://localhost:8089/oauth/token_key +publicKeyEndpoints: + - http://localhost:8089/oauth/token_key + - http://localhost:8089/oauth/token_key # in our tests we only have one wiremock port open, so we have two times the same uri +publicKeys: + - |- + -----BEGIN PUBLIC KEY----- + MIICHDANBgkqhkiG9w0BAQEFAAOCAgkAMIICBAKCAfsAqM4o+hVAdF2QATQBmpehSMyhdqKvwh9mrfnxDNtctZYlpiQXMbq4uqRgp98aBy6bMKKr3k0rSXTzr27Y+tdLUWXqbl4y8kKm8rGZo9gTbPyhqPm4f4OIxMRJcuhQ7f8qBY87w9buzClQeUs3h5f+DUVRUfB9FnDtim+ma3mFqYh38TMnrBapCtG+7iVKRFgGv6JWiNTql+oVBPNuUX3koc5/zO6IhrD49vBbsjaRWTJV2xMNll82gPvVLtgQNA2t7iGnUPhfKDj1NInZeg79NzFnWAa9Jtc1r2Q7D68MiJhYZN2QAlZS1GfbELnRAeUmSxT5i3BHu23iz9zluhIhYe1vhA1QWk2HsriGL9w+iFqzYlk5P3GCAE+nfNmM/6GIp1ehzW+/4+xgik5rOakCWw4vewmSBWOrV/XZvT2ZT3AA6zIByWdERyMOVJmd9rqPH1FIDtQk8h2jFTqIvBda727DHXeUB9J4hHQTzQmvOxPMipwDslxWOjnG4nbq6Exme0o/ELMOxt+4APH6KW+LqCNl5jGdbKxySLQyNgfUjhXJ06U1b8JHPheTnWcKO+cMmhyheUkZmLMLK2mlAsR+JJeBDY1/jd7+q6hgymeJzoDoXJj4LARiYZ+StRr/E0+P8DrprWYZPi496VIzwgV8otV9fVz29V501rcCAwEAAQ== + -----END PUBLIC KEY----- + - |- + -----BEGIN EC PUBLIC KEY----- + MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvmBia5inhASHAVrFBB5JAh0ne/aKb6z/sCIuWzKp/azFcD/OPJ2H6RPLn3t7XA4oAa2FR3GB4ZhU7SCh20FUhA== + -----END EC PUBLIC KEY-----