This is an automated email from the ASF dual-hosted git repository.
btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
The following commit(s) were added to refs/heads/master by this push:
new 8a293a4 JAMES-1655 Allow to configure several public keys (#700)
8a293a4 is described below
commit 8a293a4fddb92999fd5c7ff42f032e35943933b5
Author: Benoit TELLIER <[email protected]>
AuthorDate: Fri Oct 22 10:16:14 2021 +0700
JAMES-1655 Allow to configure several public keys (#700)
This enables request to be supplied by several sources without needing
to share their private keys together.
Also, JWT public keys should not be parsed on a per-request basis.
---
.../docs/modules/ROOT/pages/configure/jmap.adoc | 3 +-
.../org/apache/james/MemoryJamesServerMain.java | 5 ++--
.../java/org/apache/james/cli/JwtOptionTest.java | 4 +--
.../org/apache/james/jmap/draft/JMAPModule.java | 9 ++++--
.../apache/james/modules/TestJMAPServerModule.java | 3 +-
.../james/modules/server/WebAdminServerModule.java | 2 +-
.../james/jmap/draft/JMAPDraftConfiguration.java | 17 ++++++-----
.../james/jmap/draft/crypto/SecurityKeyLoader.java | 2 +-
.../jmap/draft/JMAPDraftConfigurationTest.java | 6 ++--
.../draft/crypto/JamesSignatureHandlerFixture.java | 6 ++--
.../jmap/draft/crypto/SecurityKeyLoaderTest.java | 22 +++++++--------
.../org/apache/james/jwt/JwtConfiguration.java | 16 +++++------
.../org/apache/james/jwt/JwtTokenVerifier.java | 30 ++++++++++++++------
.../org/apache/james/jwt/PublicKeyProvider.java | 15 ++++++++--
.../java/org/apache/james/jwt/PublicKeyReader.java | 6 ++--
.../org/apache/james/jwt/JwtConfigurationTest.java | 16 +++++------
.../org/apache/james/jwt/JwtTokenVerifierTest.java | 33 ++++++++++++++++++----
.../apache/james/jwt/PublicKeyProviderTest.java | 14 ++++-----
.../org/apache/james/jwt/PublicKeyReaderTest.java | 10 ++-----
.../integration/JwtFilterIntegrationTest.java | 6 ++--
src/site/xdoc/server/config-jmap.xml | 3 +-
21 files changed, 137 insertions(+), 91 deletions(-)
diff --git
a/server/apps/distributed-app/docs/modules/ROOT/pages/configure/jmap.adoc
b/server/apps/distributed-app/docs/modules/ROOT/pages/configure/jmap.adoc
index b094439..60032d9 100644
--- a/server/apps/distributed-app/docs/modules/ROOT/pages/configure/jmap.adoc
+++ b/server/apps/distributed-app/docs/modules/ROOT/pages/configure/jmap.adoc
@@ -27,7 +27,8 @@ This should not be the same keystore than the ones used by
TLS based protocols.
| Password used to read the keystore
| jwt.publickeypem.url
-| Optional. JWT tokens allow request to bypass authentication
+| Optional. Coma separated list of RSA public keys URLs to validate JWT tokens
allowing requests to bypass authentication.
+Defaults to an empty list.
| url.prefix
| Optional. Configuration urlPrefix for JMAP routes.
diff --git
a/server/apps/memory-app/src/main/java/org/apache/james/MemoryJamesServerMain.java
b/server/apps/memory-app/src/main/java/org/apache/james/MemoryJamesServerMain.java
index 2c29c2c..24c43be 100644
---
a/server/apps/memory-app/src/main/java/org/apache/james/MemoryJamesServerMain.java
+++
b/server/apps/memory-app/src/main/java/org/apache/james/MemoryJamesServerMain.java
@@ -19,8 +19,6 @@
package org.apache.james;
-import java.util.Optional;
-
import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
import org.apache.james.jwt.JwtConfiguration;
import org.apache.james.modules.BlobExportMechanismModule;
@@ -64,6 +62,7 @@ import org.apache.james.webadmin.WebAdminConfiguration;
import org.apache.james.webadmin.authentication.AuthenticationFilter;
import org.apache.james.webadmin.authentication.NoAuthenticationFilter;
+import com.google.common.collect.ImmutableList;
import com.google.inject.Module;
import com.google.inject.util.Modules;
@@ -82,7 +81,7 @@ public class MemoryJamesServerMain implements JamesServerMain
{
new SwaggerRoutesModule());
- public static final JwtConfiguration NO_JWT_CONFIGURATION = new
JwtConfiguration(Optional.empty());
+ public static final JwtConfiguration NO_JWT_CONFIGURATION = new
JwtConfiguration(ImmutableList.of());
public static final Module WEBADMIN_NO_AUTH_MODULE =
Modules.combine(binder ->
binder.bind(JwtConfiguration.class).toInstance(NO_JWT_CONFIGURATION),
binder ->
binder.bind(AuthenticationFilter.class).to(NoAuthenticationFilter.class),
diff --git
a/server/apps/webadmin-cli/src/test/java/org/apache/james/cli/JwtOptionTest.java
b/server/apps/webadmin-cli/src/test/java/org/apache/james/cli/JwtOptionTest.java
index 1993e4f..8be5dd6 100644
---
a/server/apps/webadmin-cli/src/test/java/org/apache/james/cli/JwtOptionTest.java
+++
b/server/apps/webadmin-cli/src/test/java/org/apache/james/cli/JwtOptionTest.java
@@ -23,7 +23,6 @@ import static org.assertj.core.api.Assertions.assertThat;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
-import java.util.Optional;
import org.apache.james.GuiceJamesServer;
import org.apache.james.JamesServerBuilder;
@@ -39,6 +38,7 @@ import
org.apache.james.webadmin.authentication.AuthenticationFilter;
import org.apache.james.webadmin.authentication.JwtFilter;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
+import org.testcontainers.shaded.com.google.common.collect.ImmutableList;
import com.google.inject.name.Names;
@@ -55,7 +55,7 @@ public class JwtOptionTest {
protected static JwtConfiguration jwtConfiguration() {
return new JwtConfiguration(
-
Optional.of(ClassLoaderUtils.getSystemResourceAsString("jwt_publickey")));
+
ImmutableList.of(ClassLoaderUtils.getSystemResourceAsString("jwt_publickey")));
}
private static final String VALID_TOKEN_ADMIN_TRUE =
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbkBvcGVuL" +
diff --git
a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/draft/JMAPModule.java
b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/draft/JMAPModule.java
index 7ea5bdf..e61541b 100644
---
a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/draft/JMAPModule.java
+++
b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/draft/JMAPModule.java
@@ -22,6 +22,7 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.EnumSet;
+import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
@@ -203,7 +204,7 @@ public class JMAPModule extends AbstractModule {
.certificates(configuration.getString("tls.certificates",
null))
.keystoreType(configuration.getString("tls.keystoreType",
null))
.secret(configuration.getString("tls.secret", null))
- .jwtPublicKeyPem(loadPublicKey(fileSystem,
Optional.ofNullable(configuration.getString("jwt.publickeypem.url"))))
+ .jwtPublicKeyPem(loadPublicKey(fileSystem,
ImmutableList.copyOf(configuration.getStringArray("jwt.publickeypem.url"))))
.build();
} catch (FileNotFoundException e) {
LOGGER.warn("Could not find JMAP configuration file. JMAP server
will not be enabled.");
@@ -221,8 +222,10 @@ public class JMAPModule extends AbstractModule {
return JwtTokenVerifier.create(jwtConfiguration);
}
- private Optional<String> loadPublicKey(FileSystem fileSystem,
Optional<String> jwtPublickeyPemUrl) {
- return jwtPublickeyPemUrl.map(Throwing.function(url ->
FileUtils.readFileToString(fileSystem.getFile(url),
StandardCharsets.US_ASCII)));
+ private List<String> loadPublicKey(FileSystem fileSystem, List<String>
jwtPublickeyPemUrl) {
+ return jwtPublickeyPemUrl.stream()
+ .map(Throwing.function(url ->
FileUtils.readFileToString(fileSystem.getFile(url), StandardCharsets.US_ASCII)))
+ .collect(ImmutableList.toImmutableList());
}
@Singleton
diff --git
a/server/container/guice/protocols/jmap/src/test/java/org/apache/james/modules/TestJMAPServerModule.java
b/server/container/guice/protocols/jmap/src/test/java/org/apache/james/modules/TestJMAPServerModule.java
index 701db58..a192e32 100644
---
a/server/container/guice/protocols/jmap/src/test/java/org/apache/james/modules/TestJMAPServerModule.java
+++
b/server/container/guice/protocols/jmap/src/test/java/org/apache/james/modules/TestJMAPServerModule.java
@@ -30,6 +30,7 @@ import org.apache.james.jmap.draft.JMAPDraftConfiguration;
import org.apache.james.jmap.draft.methods.GetMessageListMethod;
import org.apache.james.modules.mailbox.FastRetryBackoffModule;
+import com.google.common.collect.ImmutableList;
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.name.Names;
@@ -103,7 +104,7 @@ public class TestJMAPServerModule extends AbstractModule {
.keystore("keystore")
.keystoreType("JKS")
.secret("james72laBalle")
- .jwtPublicKeyPem(Optional.of(PUBLIC_PEM_KEY));
+ .jwtPublicKeyPem(ImmutableList.of(PUBLIC_PEM_KEY));
}
@Override
diff --git
a/server/container/guice/protocols/webadmin/src/main/java/org/apache/james/modules/server/WebAdminServerModule.java
b/server/container/guice/protocols/webadmin/src/main/java/org/apache/james/modules/server/WebAdminServerModule.java
index 5bf8164..1fb22e0 100644
---
a/server/container/guice/protocols/webadmin/src/main/java/org/apache/james/modules/server/WebAdminServerModule.java
+++
b/server/container/guice/protocols/webadmin/src/main/java/org/apache/james/modules/server/WebAdminServerModule.java
@@ -181,7 +181,7 @@ public class WebAdminServerModule extends AbstractModule {
JwtTokenVerifier.Factory providesJwtTokenVerifier(WebAdminConfiguration
webAdminConfiguration,
@Named("jmap")
Provider<JwtTokenVerifier> jmapTokenVerifier) {
return () -> webAdminConfiguration.getJwtPublicKey()
- .map(keyPath -> new JwtConfiguration(Optional.of(keyPath)))
+ .map(keyPath -> new JwtConfiguration(ImmutableList.of(keyPath)))
.map(JwtTokenVerifier::create)
.orElseGet(jmapTokenVerifier::get);
}
diff --git
a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/JMAPDraftConfiguration.java
b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/JMAPDraftConfiguration.java
index b515a60..3e04a99 100644
---
a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/JMAPDraftConfiguration.java
+++
b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/JMAPDraftConfiguration.java
@@ -18,10 +18,13 @@
****************************************************************/
package org.apache.james.jmap.draft;
+import java.util.Collection;
+import java.util.List;
import java.util.Optional;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
public class JMAPDraftConfiguration {
@@ -36,7 +39,7 @@ public class JMAPDraftConfiguration {
private Optional<String> certificates = Optional.empty();
private Optional<String> secret = Optional.empty();
private Optional<Boolean> enabled = Optional.empty();
- private Optional<String> jwtPublicKeyPem = Optional.empty();
+ private ImmutableList.Builder<String> jwtPublicKeyPem =
ImmutableList.builder();
private Builder() {
@@ -82,9 +85,9 @@ public class JMAPDraftConfiguration {
return this;
}
- public Builder jwtPublicKeyPem(Optional<String> jwtPublicKeyPem) {
+ public Builder jwtPublicKeyPem(Collection<String> jwtPublicKeyPem) {
Preconditions.checkNotNull(jwtPublicKeyPem);
- this.jwtPublicKeyPem = jwtPublicKeyPem;
+ this.jwtPublicKeyPem.addAll(jwtPublicKeyPem);
return this;
}
@@ -93,7 +96,7 @@ public class JMAPDraftConfiguration {
Preconditions.checkState(!enabled.get() ||
cryptoParametersAreSpecified(),
"('keystore' && 'secret') or (privateKey && certificates) is
mandatory");
- return new JMAPDraftConfiguration(enabled.get(), keystore,
privateKey, certificates, keystoreType.orElse("JKS"), secret, jwtPublicKeyPem);
+ return new JMAPDraftConfiguration(enabled.get(), keystore,
privateKey, certificates, keystoreType.orElse("JKS"), secret,
jwtPublicKeyPem.build());
}
private boolean cryptoParametersAreSpecified() {
@@ -108,10 +111,10 @@ public class JMAPDraftConfiguration {
private final Optional<String> certificates;
private final String keystoreType;
private final Optional<String> secret;
- private final Optional<String> jwtPublicKeyPem;
+ private final List<String> jwtPublicKeyPem;
@VisibleForTesting
- JMAPDraftConfiguration(boolean enabled, Optional<String> keystore,
Optional<String> privateKey, Optional<String> certificates, String
keystoreType, Optional<String> secret, Optional<String> jwtPublicKeyPem) {
+ JMAPDraftConfiguration(boolean enabled, Optional<String> keystore,
Optional<String> privateKey, Optional<String> certificates, String
keystoreType, Optional<String> secret, List<String> jwtPublicKeyPem) {
this.enabled = enabled;
this.keystore = keystore;
this.privateKey = privateKey;
@@ -145,7 +148,7 @@ public class JMAPDraftConfiguration {
return secret;
}
- public Optional<String> getJwtPublicKeyPem() {
+ public List<String> getJwtPublicKeyPem() {
return jwtPublicKeyPem;
}
}
diff --git
a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/crypto/SecurityKeyLoader.java
b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/crypto/SecurityKeyLoader.java
index f91565d..e1b4660 100644
---
a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/crypto/SecurityKeyLoader.java
+++
b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/crypto/SecurityKeyLoader.java
@@ -107,7 +107,7 @@ public class SecurityKeyLoader {
} catch (CertificateParseException e) {
String publicKeyAsString =
IOUtils.toString(fileSystem.getResource(jmapDraftConfiguration.getCertificates().get()),
StandardCharsets.US_ASCII);
return new PublicKeyReader()
- .fromPEM(Optional.of(publicKeyAsString))
+ .fromPEM(publicKeyAsString)
.orElseThrow(() -> new IllegalArgumentException("Key must
either be a valid certificate or a public key"));
}
}
diff --git
a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/JMAPDraftConfigurationTest.java
b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/JMAPDraftConfigurationTest.java
index 3b7d740..2126932 100644
---
a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/JMAPDraftConfigurationTest.java
+++
b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/JMAPDraftConfigurationTest.java
@@ -23,10 +23,13 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import java.util.List;
import java.util.Optional;
import org.junit.Test;
+import com.google.common.collect.ImmutableList;
+
public class JMAPDraftConfigurationTest {
public static final boolean ENABLED = true;
@@ -92,14 +95,13 @@ public class JMAPDraftConfigurationTest {
.enable()
.keystore("keystore")
.secret("secret")
- .jwtPublicKeyPem(Optional.empty())
.build())
.doesNotThrowAnyException();
}
@Test
public void buildShouldWorkWhenDisabled() {
- Optional<String> jwtPublicKeyPem = Optional.empty();
+ List<String> jwtPublicKeyPem = ImmutableList.of();
Optional<String> privateKey = Optional.empty();
Optional<String> certificates = Optional.empty();
Optional<String> keystore = Optional.empty();
diff --git
a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/crypto/JamesSignatureHandlerFixture.java
b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/crypto/JamesSignatureHandlerFixture.java
index 8ffe064..f527079 100644
---
a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/crypto/JamesSignatureHandlerFixture.java
+++
b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/crypto/JamesSignatureHandlerFixture.java
@@ -19,11 +19,11 @@
package org.apache.james.jmap.draft.crypto;
-import java.util.Optional;
-
import org.apache.james.filesystem.api.FileSystemFixture;
import org.apache.james.jmap.draft.JMAPDraftConfiguration;
+import com.google.common.collect.ImmutableList;
+
class JamesSignatureHandlerFixture {
static final String JWT_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\n" +
@@ -40,7 +40,7 @@ class JamesSignatureHandlerFixture {
JMAPDraftConfiguration jmapDraftConfiguration =
JMAPDraftConfiguration.builder()
.enable()
- .jwtPublicKeyPem(Optional.of(JWT_PUBLIC_KEY))
+ .jwtPublicKeyPem(ImmutableList.of(JWT_PUBLIC_KEY))
.keystore("keystore")
.secret("james72laBalle")
.build();
diff --git
a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/crypto/SecurityKeyLoaderTest.java
b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/crypto/SecurityKeyLoaderTest.java
index 529e7e2..dd398aa 100644
---
a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/crypto/SecurityKeyLoaderTest.java
+++
b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/crypto/SecurityKeyLoaderTest.java
@@ -24,7 +24,6 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.security.KeyStoreException;
-import java.util.Optional;
import org.apache.james.filesystem.api.FileSystemFixture;
import org.apache.james.jmap.draft.JMAPDraftConfiguration;
@@ -32,15 +31,16 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
+import com.google.common.collect.ImmutableList;
+
import nl.altindag.ssl.exception.GenericKeyStoreException;
class SecurityKeyLoaderTest {
-
@Test
- void loadShouldThrowWhenJMAPIsNotEnabled() throws Exception {
+ void loadShouldThrowWhenJMAPIsNotEnabled() {
JMAPDraftConfiguration jmapConfiguration =
JMAPDraftConfiguration.builder()
.disable()
- .jwtPublicKeyPem(Optional.of(JWT_PUBLIC_KEY))
+ .jwtPublicKeyPem(ImmutableList.of(JWT_PUBLIC_KEY))
.keystore("keystore")
.secret("james72laBalle")
.build();
@@ -55,10 +55,10 @@ class SecurityKeyLoaderTest {
}
@Test
- void loadShouldThrowWhenWrongKeystore() throws Exception {
+ void loadShouldThrowWhenWrongKeystore() {
JMAPDraftConfiguration jmapDraftConfiguration =
JMAPDraftConfiguration.builder()
.enable()
- .jwtPublicKeyPem(Optional.of(JWT_PUBLIC_KEY))
+ .jwtPublicKeyPem(ImmutableList.of(JWT_PUBLIC_KEY))
.keystore("badAliasKeystore")
.secret("password")
.build();
@@ -73,10 +73,10 @@ class SecurityKeyLoaderTest {
}
@Test
- void loadShouldThrowWhenWrongPassword() throws Exception {
+ void loadShouldThrowWhenWrongPassword() {
JMAPDraftConfiguration jmapDraftConfiguration =
JMAPDraftConfiguration.builder()
.enable()
- .jwtPublicKeyPem(Optional.of(JWT_PUBLIC_KEY))
+ .jwtPublicKeyPem(ImmutableList.of(JWT_PUBLIC_KEY))
.keystore("keystore")
.secret("WrongPassword")
.build();
@@ -94,7 +94,7 @@ class SecurityKeyLoaderTest {
void loadShouldReturnAsymmetricKeysWhenCorrectPassword() throws Exception {
JMAPDraftConfiguration jmapDraftConfiguration =
JMAPDraftConfiguration.builder()
.enable()
- .jwtPublicKeyPem(Optional.of(JWT_PUBLIC_KEY))
+ .jwtPublicKeyPem(ImmutableList.of(JWT_PUBLIC_KEY))
.keystore("keystore")
.secret("james72laBalle")
.build();
@@ -111,7 +111,7 @@ class SecurityKeyLoaderTest {
void loadShouldReturnAsymmetricKeysWhenRawPublicKey() throws Exception {
JMAPDraftConfiguration jmapDraftConfiguration =
JMAPDraftConfiguration.builder()
.enable()
- .jwtPublicKeyPem(Optional.of(JWT_PUBLIC_KEY))
+ .jwtPublicKeyPem(ImmutableList.of(JWT_PUBLIC_KEY))
.certificates("key.pub")
.privateKey("private.nopass.key")
.build();
@@ -134,7 +134,7 @@ class SecurityKeyLoaderTest {
JMAPDraftConfiguration jmapDraftConfiguration =
JMAPDraftConfiguration.builder()
.enable()
- .jwtPublicKeyPem(Optional.of(JWT_PUBLIC_KEY))
+ .jwtPublicKeyPem(ImmutableList.of(JWT_PUBLIC_KEY))
.keystore(keyStoreInDifferentVersion)
.secret("james72laBalle")
.build();
diff --git
a/server/protocols/jwt/src/main/java/org/apache/james/jwt/JwtConfiguration.java
b/server/protocols/jwt/src/main/java/org/apache/james/jwt/JwtConfiguration.java
index 922ea00..8fa772c 100644
---
a/server/protocols/jwt/src/main/java/org/apache/james/jwt/JwtConfiguration.java
+++
b/server/protocols/jwt/src/main/java/org/apache/james/jwt/JwtConfiguration.java
@@ -19,26 +19,24 @@
package org.apache.james.jwt;
-import java.util.Optional;
+import java.util.List;
import com.google.common.base.Preconditions;
public class JwtConfiguration {
- private static final boolean DEFAULT_VALUE = true;
- private final Optional<String> jwtPublicKeyPem;
+ private final List<String> jwtPublicKeyPem;
- public JwtConfiguration(Optional<String> jwtPublicKeyPem) {
- Preconditions.checkState(validPublicKey(jwtPublicKeyPem), "The
provided public key is not valid");
+ public JwtConfiguration(List<String> jwtPublicKeyPem) {
+ Preconditions.checkState(validPublicKey(jwtPublicKeyPem), "One of the
provided public key is not valid");
this.jwtPublicKeyPem = jwtPublicKeyPem;
}
- private boolean validPublicKey(Optional<String> jwtPublicKeyPem) {
+ private boolean validPublicKey(List<String> jwtPublicKeyPem) {
PublicKeyReader reader = new PublicKeyReader();
- return jwtPublicKeyPem.map(value ->
reader.fromPEM(Optional.of(value)).isPresent())
- .orElse(DEFAULT_VALUE);
+ return jwtPublicKeyPem.stream().allMatch(value ->
reader.fromPEM(value).isPresent());
}
- public Optional<String> getJwtPublicKeyPem() {
+ public List<String> getJwtPublicKeyPem() {
return jwtPublicKeyPem;
}
}
diff --git
a/server/protocols/jwt/src/main/java/org/apache/james/jwt/JwtTokenVerifier.java
b/server/protocols/jwt/src/main/java/org/apache/james/jwt/JwtTokenVerifier.java
index 6eb71ec..8a8feeb 100644
---
a/server/protocols/jwt/src/main/java/org/apache/james/jwt/JwtTokenVerifier.java
+++
b/server/protocols/jwt/src/main/java/org/apache/james/jwt/JwtTokenVerifier.java
@@ -18,6 +18,8 @@
****************************************************************/
package org.apache.james.jwt;
+import java.security.PublicKey;
+import java.util.List;
import java.util.Optional;
import org.slf4j.Logger;
@@ -43,15 +45,22 @@ public class JwtTokenVerifier {
}
private static final Logger LOGGER =
LoggerFactory.getLogger(JwtTokenVerifier.class);
- private final PublicKeyProvider pubKeyProvider;
+
+ private final List<PublicKey> publicKeys;
public JwtTokenVerifier(PublicKeyProvider pubKeyProvider) {
- this.pubKeyProvider = pubKeyProvider;
+ this.publicKeys = pubKeyProvider.get();
}
public Optional<String> verifyAndExtractLogin(String token) {
+ return publicKeys.stream()
+ .flatMap(key -> verifyAndExtractLogin(token, key).stream())
+ .findFirst();
+ }
+
+ public Optional<String> verifyAndExtractLogin(String token, PublicKey key)
{
try {
- String subject = extractLogin(token);
+ String subject = extractLogin(token, key);
if (Strings.isNullOrEmpty(subject)) {
throw new MalformedJwtException("'subject' field in token is
mandatory");
}
@@ -62,19 +71,24 @@ public class JwtTokenVerifier {
}
}
- private String extractLogin(String token) throws JwtException {
- Jws<Claims> jws = parseToken(token);
+ private String extractLogin(String token, PublicKey publicKey) throws
JwtException {
+ Jws<Claims> jws = parseToken(token, publicKey);
return jws
.getBody()
.getSubject();
}
public boolean hasAttribute(String attributeName, Object expectedValue,
String token) {
+ return publicKeys.stream()
+ .anyMatch(key -> hasAttribute(attributeName, expectedValue, token,
key));
+ }
+
+ private boolean hasAttribute(String attributeName, Object expectedValue,
String token, PublicKey publicKey) {
try {
Jwts
.parser()
.require(attributeName, expectedValue)
- .setSigningKey(pubKeyProvider.get())
+ .setSigningKey(publicKey)
.parseClaimsJws(token);
return true;
} catch (JwtException e) {
@@ -83,10 +97,10 @@ public class JwtTokenVerifier {
}
}
- private Jws<Claims> parseToken(String token) throws JwtException {
+ private Jws<Claims> parseToken(String token, PublicKey publicKey) throws
JwtException {
return Jwts
.parser()
- .setSigningKey(pubKeyProvider.get())
+ .setSigningKey(publicKey)
.parseClaimsJws(token);
}
}
diff --git
a/server/protocols/jwt/src/main/java/org/apache/james/jwt/PublicKeyProvider.java
b/server/protocols/jwt/src/main/java/org/apache/james/jwt/PublicKeyProvider.java
index 4f102eb..247f283 100644
---
a/server/protocols/jwt/src/main/java/org/apache/james/jwt/PublicKeyProvider.java
+++
b/server/protocols/jwt/src/main/java/org/apache/james/jwt/PublicKeyProvider.java
@@ -19,6 +19,9 @@
package org.apache.james.jwt;
import java.security.PublicKey;
+import java.util.List;
+
+import com.google.common.collect.ImmutableList;
public class PublicKeyProvider {
@@ -30,9 +33,15 @@ public class PublicKeyProvider {
this.reader = reader;
}
- public PublicKey get() throws MissingOrInvalidKeyException {
- return reader.fromPEM(jwtConfiguration.getJwtPublicKeyPem())
- .orElseThrow(MissingOrInvalidKeyException::new);
+ public List<PublicKey> get() throws MissingOrInvalidKeyException {
+ ImmutableList<PublicKey> keys = jwtConfiguration.getJwtPublicKeyPem()
+ .stream()
+ .flatMap(s -> reader.fromPEM(s).stream())
+ .collect(ImmutableList.toImmutableList());
+ if (keys.size() != jwtConfiguration.getJwtPublicKeyPem().size()) {
+ throw new MissingOrInvalidKeyException();
+ }
+ return keys;
}
}
diff --git
a/server/protocols/jwt/src/main/java/org/apache/james/jwt/PublicKeyReader.java
b/server/protocols/jwt/src/main/java/org/apache/james/jwt/PublicKeyReader.java
index 7edb468..98bf8f5 100644
---
a/server/protocols/jwt/src/main/java/org/apache/james/jwt/PublicKeyReader.java
+++
b/server/protocols/jwt/src/main/java/org/apache/james/jwt/PublicKeyReader.java
@@ -34,10 +34,8 @@ public class PublicKeyReader {
private static final Logger LOGGER =
LoggerFactory.getLogger(PublicKeyReader.class);
- public Optional<PublicKey> fromPEM(Optional<String> pemKey) {
- return pemKey
- .map(k -> new PEMParser(new PemReader(new StringReader(k))))
- .flatMap(this::publicKeyFrom);
+ public Optional<PublicKey> fromPEM(String pemKey) {
+ return publicKeyFrom(new PEMParser(new PemReader(new
StringReader(pemKey))));
}
private Optional<PublicKey> publicKeyFrom(PEMParser reader) {
diff --git
a/server/protocols/jwt/src/test/java/org/apache/james/jwt/JwtConfigurationTest.java
b/server/protocols/jwt/src/test/java/org/apache/james/jwt/JwtConfigurationTest.java
index ee8a557..9ea9c23 100644
---
a/server/protocols/jwt/src/test/java/org/apache/james/jwt/JwtConfigurationTest.java
+++
b/server/protocols/jwt/src/test/java/org/apache/james/jwt/JwtConfigurationTest.java
@@ -22,10 +22,10 @@ package org.apache.james.jwt;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import java.util.Optional;
-
import org.junit.jupiter.api.Test;
+import com.google.common.collect.ImmutableList;
+
class JwtConfigurationTest {
private static final String INVALID_PUBLIC_KEY = "invalidPublicKey";
private static final String VALID_PUBLIC_KEY = "-----BEGIN PUBLIC
KEY-----\n" +
@@ -40,9 +40,9 @@ class JwtConfigurationTest {
@Test
void getJwtPublicKeyPemShouldReturnEmptyWhenEmptyPublicKey() {
- JwtConfiguration jwtConfiguration = new
JwtConfiguration(Optional.empty());
+ JwtConfiguration jwtConfiguration = new
JwtConfiguration(ImmutableList.of());
- assertThat(jwtConfiguration.getJwtPublicKeyPem()).isNotPresent();
+ assertThat(jwtConfiguration.getJwtPublicKeyPem()).isEmpty();
}
@Test
@@ -53,20 +53,20 @@ class JwtConfigurationTest {
@Test
void constructorShouldThrowWhenNonePublicKey() {
- assertThatThrownBy(() -> new JwtConfiguration(Optional.of("")))
+ assertThatThrownBy(() -> new JwtConfiguration(ImmutableList.of("")))
.isInstanceOf(IllegalStateException.class);
}
@Test
void constructorShouldThrowWhenInvalidPublicKey() {
- assertThatThrownBy(() -> new
JwtConfiguration(Optional.of(INVALID_PUBLIC_KEY)))
+ assertThatThrownBy(() -> new
JwtConfiguration(ImmutableList.of(INVALID_PUBLIC_KEY)))
.isInstanceOf(IllegalStateException.class);
}
@Test
void getJwtPublicKeyPemShouldReturnWhenValidPublicKey() {
- JwtConfiguration jwtConfiguration = new
JwtConfiguration(Optional.of(VALID_PUBLIC_KEY));
+ JwtConfiguration jwtConfiguration = new
JwtConfiguration(ImmutableList.of(VALID_PUBLIC_KEY));
- assertThat(jwtConfiguration.getJwtPublicKeyPem()).isPresent();
+ assertThat(jwtConfiguration.getJwtPublicKeyPem()).isNotEmpty();
}
}
\ No newline at end of file
diff --git
a/server/protocols/jwt/src/test/java/org/apache/james/jwt/JwtTokenVerifierTest.java
b/server/protocols/jwt/src/test/java/org/apache/james/jwt/JwtTokenVerifierTest.java
index 08c854b..3bfe79b 100644
---
a/server/protocols/jwt/src/test/java/org/apache/james/jwt/JwtTokenVerifierTest.java
+++
b/server/protocols/jwt/src/test/java/org/apache/james/jwt/JwtTokenVerifierTest.java
@@ -21,13 +21,14 @@ package org.apache.james.jwt;
import static org.assertj.core.api.Assertions.assertThat;
import java.security.Security;
-import java.util.Optional;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import com.google.common.collect.ImmutableList;
+
class JwtTokenVerifierTest {
private static final String PUBLIC_PEM_KEY = "-----BEGIN PUBLIC
KEY-----\n" +
@@ -39,6 +40,13 @@ class JwtTokenVerifierTest {
"U1LZUUbJW9/CH45YXz82CYqkrfbnQxqRb2iVbVjs/sHopHd1NTiCfUtwvcYJiBVj\n" +
"kwIDAQAB\n" +
"-----END PUBLIC KEY-----";
+ public static final String PUBLIC_PEM_KEY_2 =
+ "-----BEGIN PUBLIC KEY-----\n" +
+
"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCVnxAOpup/rtGzn+xUaBRFSe34\n" +
+
"H7YyiM6YBD1bh5rkoi9pB6fvs1vDlXzBmR0Zl6kn3g+2ChW0lqMkmv73Y2Lv3WZK\n" +
+
"NZ3DUR3lfBFbvYGQyFyib+e4MY1yWkj3sumMl1wdUB4lKLHLIRv9X1xCqvbSHEtq\n" +
+ "zoZF4vgBYx0VmuJslwIDAQAB\n" +
+ "-----END PUBLIC KEY-----";
private static final String VALID_TOKEN_WITHOUT_ADMIN =
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.T04BTk"
+
"LXkJj24coSZkK13RfG25lpvmSl2MJ7N10KpBk9_-95EGYZdog-BDAn3PJzqVw52z-Bwjh4VOj1-j7cURu0cT4jXehhUrlCxS4n7QHZD"
+
@@ -70,16 +78,19 @@ class JwtTokenVerifierTest {
@BeforeEach
void setup() {
- PublicKeyProvider pubKeyProvider = new
PublicKeyProvider(getJWTConfiguration(), new PublicKeyReader());
+ PublicKeyProvider pubKeyProvider = new PublicKeyProvider(new
JwtConfiguration(ImmutableList.of(PUBLIC_PEM_KEY)), new PublicKeyReader());
sut = new JwtTokenVerifier(pubKeyProvider);
}
- private JwtConfiguration getJWTConfiguration() {
- return new JwtConfiguration(Optional.of(PUBLIC_PEM_KEY));
+ @Test
+ void shouldReturnTrueOnValidSignature() {
+
assertThat(sut.verifyAndExtractLogin(VALID_TOKEN_WITHOUT_ADMIN)).isPresent();
}
@Test
- void shouldReturnTrueOnValidSignature() {
+ void shouldReturnTrueOnValidSignatureWithMultipleKeys() {
+ PublicKeyProvider pubKeyProvider = new PublicKeyProvider(new
JwtConfiguration(ImmutableList.of(PUBLIC_PEM_KEY_2, PUBLIC_PEM_KEY)), new
PublicKeyReader());
+ JwtTokenVerifier sut = new JwtTokenVerifier(pubKeyProvider);
assertThat(sut.verifyAndExtractLogin(VALID_TOKEN_WITHOUT_ADMIN)).isPresent();
}
@@ -93,6 +104,18 @@ class JwtTokenVerifierTest {
}
@Test
+ void shouldReturnFalseOnMismatchingSigningKeyWithMultipleKeys() {
+ String invalidToken =
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.Pd6t82"
+
+
"tPL3EZdkeYxw_DV2KimE1U2FvuLHmfR_mimJ5US3JFU4J2Gd94O7rwpSTGN1B9h-_lsTebo4ua4xHsTtmczZ9xa8a_kWKaSkqFjNFa"
+
+
"Fp6zcoD6ivCu03SlRqsQzSRHXo6TKbnqOt9D6Y2rNa3C4igSwoS0jUE4BgpXbc0";
+
+ PublicKeyProvider pubKeyProvider = new PublicKeyProvider(new
JwtConfiguration(ImmutableList.of(PUBLIC_PEM_KEY_2, PUBLIC_PEM_KEY)), new
PublicKeyReader());
+ JwtTokenVerifier sut = new JwtTokenVerifier(pubKeyProvider);
+
+ assertThat(sut.verifyAndExtractLogin(invalidToken)).isEmpty();
+ }
+
+ @Test
void verifyShouldReturnFalseWhenSubjectIsNull() {
String tokenWithNullSubject =
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOm51bGwsIm5hbWUiOiJKb2huIERvZSJ9.EB"
+
"_1grWDy_kFelXs3AQeiP13ay4eG_134dWB9XPRSeWsuPs8Mz2UY-VHDxLGD-fAqv-xKXr4QFEnS7iZkdpe0tPLNSwIjqeqkC6KqQln"
+
diff --git
a/server/protocols/jwt/src/test/java/org/apache/james/jwt/PublicKeyProviderTest.java
b/server/protocols/jwt/src/test/java/org/apache/james/jwt/PublicKeyProviderTest.java
index a4771b9..b6b6cad 100644
---
a/server/protocols/jwt/src/test/java/org/apache/james/jwt/PublicKeyProviderTest.java
+++
b/server/protocols/jwt/src/test/java/org/apache/james/jwt/PublicKeyProviderTest.java
@@ -23,12 +23,13 @@ import static
org.assertj.core.api.Assertions.assertThatThrownBy;
import java.security.Security;
import java.security.interfaces.RSAPublicKey;
-import java.util.Optional;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
+import com.google.common.collect.ImmutableList;
+
class PublicKeyProviderTest {
private static final String PUBLIC_PEM_KEY = "-----BEGIN PUBLIC
KEY-----\n" +
@@ -48,20 +49,19 @@ class PublicKeyProviderTest {
@Test
void getShouldNotThrowWhenPEMKeyProvided() {
-
- JwtConfiguration configWithPEMKey = new
JwtConfiguration(Optional.of(PUBLIC_PEM_KEY));
+ JwtConfiguration configWithPEMKey = new
JwtConfiguration(ImmutableList.of(PUBLIC_PEM_KEY));
PublicKeyProvider sut = new PublicKeyProvider(configWithPEMKey, new
PublicKeyReader());
- assertThat(sut.get()).isInstanceOf(RSAPublicKey.class);
+ assertThat(sut.get()).allSatisfy(key ->
assertThat(key).isInstanceOf(RSAPublicKey.class));
}
@Test
- void getShouldThrowWhenPEMKeyNotProvided() {
- JwtConfiguration configWithPEMKey = new
JwtConfiguration(Optional.empty());
+ void getShouldNotThrowWhenPEMKeyNotProvided() {
+ JwtConfiguration configWithPEMKey = new
JwtConfiguration(ImmutableList.of());
PublicKeyProvider sut = new PublicKeyProvider(configWithPEMKey, new
PublicKeyReader());
-
assertThatThrownBy(sut::get).isExactlyInstanceOf(MissingOrInvalidKeyException.class);
+ assertThat(sut.get()).isEmpty();
}
}
\ No newline at end of file
diff --git
a/server/protocols/jwt/src/test/java/org/apache/james/jwt/PublicKeyReaderTest.java
b/server/protocols/jwt/src/test/java/org/apache/james/jwt/PublicKeyReaderTest.java
index 655a1ab..159b52d 100644
---
a/server/protocols/jwt/src/test/java/org/apache/james/jwt/PublicKeyReaderTest.java
+++
b/server/protocols/jwt/src/test/java/org/apache/james/jwt/PublicKeyReaderTest.java
@@ -22,7 +22,6 @@ package org.apache.james.jwt;
import static org.assertj.core.api.Assertions.assertThat;
import java.security.Security;
-import java.util.Optional;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.jupiter.api.BeforeAll;
@@ -46,17 +45,12 @@ class PublicKeyReaderTest {
}
@Test
- void fromPEMShouldReturnEmptyWhenEmptyProvided() {
- assertThat(new PublicKeyReader().fromPEM(Optional.empty())).isEmpty();
- }
-
- @Test
void fromPEMShouldReturnEmptyWhenInvalidPEMKey() {
- assertThat(new
PublicKeyReader().fromPEM(Optional.of("blabla"))).isEmpty();
+ assertThat(new PublicKeyReader().fromPEM("blabla")).isEmpty();
}
@Test
void fromPEMShouldReturnRSAPublicKeyWhenValidPEMKey() {
- assertThat(new
PublicKeyReader().fromPEM(Optional.of(PUBLIC_PEM_KEY))).isPresent();
+ assertThat(new PublicKeyReader().fromPEM(PUBLIC_PEM_KEY)).isPresent();
}
}
diff --git
a/server/protocols/webadmin-integration-test/webadmin-integration-test-common/src/main/java/org/apache/james/webadmin/integration/JwtFilterIntegrationTest.java
b/server/protocols/webadmin-integration-test/webadmin-integration-test-common/src/main/java/org/apache/james/webadmin/integration/JwtFilterIntegrationTest.java
index 8406aee..2271a0a 100644
---
a/server/protocols/webadmin-integration-test/webadmin-integration-test-common/src/main/java/org/apache/james/webadmin/integration/JwtFilterIntegrationTest.java
+++
b/server/protocols/webadmin-integration-test/webadmin-integration-test-common/src/main/java/org/apache/james/webadmin/integration/JwtFilterIntegrationTest.java
@@ -23,8 +23,6 @@ import static io.restassured.RestAssured.given;
import static org.apache.james.webadmin.Constants.SEPARATOR;
import static org.assertj.core.api.Assertions.assertThat;
-import java.util.Optional;
-
import org.apache.james.GuiceJamesServer;
import org.apache.james.junit.categories.BasicFeature;
import org.apache.james.jwt.JwtConfiguration;
@@ -38,13 +36,15 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
+import com.google.common.collect.ImmutableList;
+
import io.restassured.RestAssured;
public abstract class JwtFilterIntegrationTest {
protected static JwtConfiguration jwtConfiguration() {
return new JwtConfiguration(
-
Optional.of(ClassLoaderUtils.getSystemResourceAsString("jwt_publickey")));
+
ImmutableList.of(ClassLoaderUtils.getSystemResourceAsString("jwt_publickey")));
}
private static final String DOMAIN = "domain";
diff --git a/src/site/xdoc/server/config-jmap.xml
b/src/site/xdoc/server/config-jmap.xml
index b09dd3a..33ba3a7 100644
--- a/src/site/xdoc/server/config-jmap.xml
+++ b/src/site/xdoc/server/config-jmap.xml
@@ -55,7 +55,8 @@
<dd>Password used to read the keystore</dd>
<dt><strong>jwt.publickeypem.url</strong></dt>
- <dd>Optional. JWT tokens allows request to bypass
authentication</dd>
+ <dd>Optional. Coma separated list of RSA public keys URLs
to validate JWT tokens allowing requests to bypass authentication.
+ Defaults to an empty list.</dd>
<dt><strong>url.prefix</strong></dt>
<dd>Optional. Configuration urlPrefix for JMAP routes.</dd>
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]