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]

Reply via email to