JAMES-1959 Implement a Jwt authentication filter for webAdmin

Project: http://git-wip-us.apache.org/repos/asf/james-project/repo
Commit: http://git-wip-us.apache.org/repos/asf/james-project/commit/3d327f72
Tree: http://git-wip-us.apache.org/repos/asf/james-project/tree/3d327f72
Diff: http://git-wip-us.apache.org/repos/asf/james-project/diff/3d327f72

Branch: refs/heads/master
Commit: 3d327f720772a841844cb2f1033830c13d8e63ac
Parents: f1a087f
Author: benwa <btell...@linagora.com>
Authored: Wed Mar 8 17:54:58 2017 +0700
Committer: benwa <btell...@linagora.com>
Committed: Wed Mar 15 09:01:53 2017 +0700

----------------------------------------------------------------------
 .../destination/conf/webadmin.properties        |   5 +-
 .../destination/conf/webadmin.properties        |   5 +-
 .../modules/server/WebAdminServerModule.java    |  20 +++
 .../org/apache/james/jwt/JwtTokenVerifier.java  |  33 ++++-
 .../apache/james/jwt/JwtTokenVerifierTest.java  |  63 +++++++---
 server/protocols/webadmin/pom.xml               |   4 +
 .../apache/james/webadmin/WebAdminServer.java   |  10 +-
 .../authentication/AuthenticationFilter.java    |  25 ++++
 .../webadmin/authentication/JwtFilter.java      |  73 +++++++++++
 .../authentication/NoAuthenticationFilter.java  |  31 +++++
 .../webadmin/authentication/JwtFilterTest.java  | 124 +++++++++++++++++++
 11 files changed, 364 insertions(+), 29 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/james-project/blob/3d327f72/dockerfiles/run/guice/cassandra-ldap/destination/conf/webadmin.properties
----------------------------------------------------------------------
diff --git 
a/dockerfiles/run/guice/cassandra-ldap/destination/conf/webadmin.properties 
b/dockerfiles/run/guice/cassandra-ldap/destination/conf/webadmin.properties
index 3a1e755..a9aced0 100644
--- a/dockerfiles/run/guice/cassandra-ldap/destination/conf/webadmin.properties
+++ b/dockerfiles/run/guice/cassandra-ldap/destination/conf/webadmin.properties
@@ -30,4 +30,7 @@ https.enabled=false
 
 # Optional when enabling HTTPS (self signed)
 #https.trust.keystore
-#https.trust.password
\ No newline at end of file
+#https.trust.password
+
+# Defaults to false
+#jwt.enabled=true
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/james-project/blob/3d327f72/dockerfiles/run/guice/cassandra/destination/conf/webadmin.properties
----------------------------------------------------------------------
diff --git 
a/dockerfiles/run/guice/cassandra/destination/conf/webadmin.properties 
b/dockerfiles/run/guice/cassandra/destination/conf/webadmin.properties
index 3a1e755..a9aced0 100644
--- a/dockerfiles/run/guice/cassandra/destination/conf/webadmin.properties
+++ b/dockerfiles/run/guice/cassandra/destination/conf/webadmin.properties
@@ -30,4 +30,7 @@ https.enabled=false
 
 # Optional when enabling HTTPS (self signed)
 #https.trust.keystore
-#https.trust.password
\ No newline at end of file
+#https.trust.password
+
+# Defaults to false
+#jwt.enabled=true
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/james-project/blob/3d327f72/server/container/guice/protocols/webadmin/src/main/java/org/apache/james/modules/server/WebAdminServerModule.java
----------------------------------------------------------------------
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 29c6223..68e1b43 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
@@ -26,6 +26,7 @@ import java.util.List;
 
 import org.apache.commons.configuration.ConfigurationException;
 import org.apache.commons.configuration.PropertiesConfiguration;
+import org.apache.james.jwt.JwtTokenVerifier;
 import org.apache.james.lifecycle.api.Configurable;
 import org.apache.james.utils.ConfigurationPerformer;
 import org.apache.james.utils.GuiceProbe;
@@ -36,6 +37,9 @@ import org.apache.james.webadmin.HttpsConfiguration;
 import org.apache.james.webadmin.Routes;
 import org.apache.james.webadmin.WebAdminConfiguration;
 import org.apache.james.webadmin.WebAdminServer;
+import org.apache.james.webadmin.authentication.AuthenticationFilter;
+import org.apache.james.webadmin.authentication.JwtFilter;
+import org.apache.james.webadmin.authentication.NoAuthenticationFilter;
 import org.apache.james.webadmin.routes.DomainRoutes;
 import org.apache.james.webadmin.routes.UserMailboxesRoutes;
 import org.apache.james.webadmin.routes.UserRoutes;
@@ -52,6 +56,8 @@ import com.google.inject.multibindings.Multibinder;
 
 public class WebAdminServerModule extends AbstractModule {
 
+    public static final boolean DEFAULT_JWT_DISABLED = false;
+
     @Override
     protected void configure() {
         bind(JsonTransformer.class).in(Scopes.SINGLETON);
@@ -82,6 +88,20 @@ public class WebAdminServerModule extends AbstractModule {
         }
     }
 
+    @Provides
+    public AuthenticationFilter 
providesAuthenticationFilter(PropertiesProvider propertiesProvider,
+                                                             JwtTokenVerifier 
jwtTokenVerifier) throws Exception {
+        try {
+            PropertiesConfiguration configurationFile = 
propertiesProvider.getConfiguration("webadmin");
+            if (configurationFile.getBoolean("jwt.enabled", 
DEFAULT_JWT_DISABLED)) {
+                return new JwtFilter(jwtTokenVerifier);
+            }
+            return new NoAuthenticationFilter();
+        } catch (FileNotFoundException e) {
+            return new NoAuthenticationFilter();
+        }
+    }
+
     private HttpsConfiguration readHttpsConfiguration(PropertiesConfiguration 
configurationFile) {
         boolean enabled = configurationFile.getBoolean("https.enabled", 
DEFAULT_HTTPS_DISABLED());
         if (enabled) {

http://git-wip-us.apache.org/repos/asf/james-project/blob/3d327f72/server/protocols/jwt/src/main/java/org/apache/james/jwt/JwtTokenVerifier.java
----------------------------------------------------------------------
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 661302f..cffc5fb 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
@@ -20,6 +20,9 @@ package org.apache.james.jwt;
 
 import javax.inject.Inject;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
 
@@ -31,6 +34,7 @@ import io.jsonwebtoken.MalformedJwtException;
 
 public class JwtTokenVerifier {
 
+    private static Logger LOGGER = 
LoggerFactory.getLogger(JwtTokenVerifier.class);
     private final PublicKeyProvider pubKeyProvider;
 
     @Inject
@@ -39,12 +43,17 @@ public class JwtTokenVerifier {
         this.pubKeyProvider = pubKeyProvider;
     }
 
-    public boolean verify(String token) throws JwtException {
-        String subject = extractLogin(token);
-        if (Strings.isNullOrEmpty(subject)) {
-            throw new MalformedJwtException("'subject' field in token is 
mandatory");
+    public boolean verify(String token) {
+        try {
+            String subject = extractLogin(token);
+            if (Strings.isNullOrEmpty(subject)) {
+                throw new MalformedJwtException("'subject' field in token is 
mandatory");
+            }
+            return true;
+        } catch (JwtException e) {
+            LOGGER.info("Failed Jwt verification");
+            return false;
         }
-        return true;
     }
 
     public String extractLogin(String token) throws JwtException {
@@ -54,6 +63,20 @@ public class JwtTokenVerifier {
                 .getSubject();
     }
 
+    public boolean hasAttribute(String attributeName, Object expectedValue, 
String token) {
+        try {
+            Jwts
+                .parser()
+                .require(attributeName, expectedValue)
+                .setSigningKey(pubKeyProvider.get())
+                .parseClaimsJws(token);
+            return true;
+        } catch (JwtException e) {
+            LOGGER.info("Jwt validation failed for claim " + attributeName + " 
to " + expectedValue, e);
+            return false;
+        }
+    }
+
     private Jws<Claims> parseToken(String token) throws JwtException {
         return Jwts
                 .parser()

http://git-wip-us.apache.org/repos/asf/james-project/blob/3d327f72/server/protocols/jwt/src/test/java/org/apache/james/jwt/JwtTokenVerifierTest.java
----------------------------------------------------------------------
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 ac5cc94..698b28d 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
@@ -19,7 +19,6 @@
 package org.apache.james.jwt;
 
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 import java.security.Security;
 import java.util.Optional;
@@ -27,10 +26,9 @@ import java.util.Optional;
 import org.bouncycastle.jce.provider.BouncyCastleProvider;
 import org.junit.Before;
 import org.junit.BeforeClass;
+import org.junit.Rule;
 import org.junit.Test;
-
-import io.jsonwebtoken.MalformedJwtException;
-import io.jsonwebtoken.SignatureException;
+import org.junit.rules.ExpectedException;
 
 public class JwtTokenVerifierTest {
 
@@ -44,14 +42,27 @@ public class JwtTokenVerifierTest {
             "kwIDAQAB\n" +
             "-----END PUBLIC KEY-----";
     
-    private static final String VALID_TOKEN = 
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.T04BTk"
 +
+    private static final String VALID_TOKEN_WITHOUT_ADMIN = 
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.T04BTk"
 +
             
"LXkJj24coSZkK13RfG25lpvmSl2MJ7N10KpBk9_-95EGYZdog-BDAn3PJzqVw52z-Bwjh4VOj1-j7cURu0cT4jXehhUrlCxS4n7QHZD"
 +
             
"N_bsEYGu7KzjWTpTsUiHe-rN7izXVFxDGG1TGwlmBCBnPW-EFCf9ylUsJi0r2BKNdaaPRfMIrHptH1zJBkkUziWpBN1RNLjmvlAUf49"
 +
             
"t1Tbv21ZqYM5Ht2vrhJWczFbuC-TD-8zJkXhjTmA1GVgomIX5dx1cH-dZX1wANNmshUJGHgepWlPU-5VIYxPEhb219RMLJIELMY2qN"
 +
             "OR8Q31ydinyqzXvCSzVJOf6T60-w";
 
+    private static final String VALID_TOKEN_ADMIN_TRUE = 
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbkBvcGVuL" +
+        
"XBhYXMub3JnIiwiYWRtaW4iOnRydWUsImlhdCI6MTQ4OTAzODQzOH0.rgxCkdWEa-92a4R-72a9Z49k4LRvQDShgci5Y7qWRUP9IGJCK-lMkrHF"
 +
+        
"4H0a6L87BYppxVW701zaZ6dNxRMvHnjLBBWnPsC2B0rkkr2hEL2zfz7sb-iNGV-J4ICx97t8-TfQ5rz3VOX0FwdusPL_rJtmlGEGRivPkR6_aBe1"
 +
+        
"kQnvMlwpqF_3ox58EUqYJk6lK_6rjKEV3Xfre31IMpuQUy6c7TKc95sL2-13cknelTierBEmZ00RzTtv9SHIEfzZTfaUK2Wm0PvnQjmU2nIdEvU"
 +
+        "EqE-jrM3yYXcQzoO-YTQnEhdl-iqbCfmEpYkl2Bx3eIq7gRxxnr7BPsX6HrCB0w";
+    private static final String VALID_TOKEN_ADMIN_FALSE = 
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbkBvcGVu" +
+        
"LXBhYXMub3JnIiwiYWRtaW4iOmZhbHNlLCJpYXQiOjE0ODkwNDA4Njd9.reQc3DiVvbQHF08oW1qOUyDJyv3tfzDNk8jhVZequiCdOI9vXnRlOe"
 +
+        
"-yDYktd4WT8MYhqY7MgS-wR0vO9jZFv8ZCgd_MkKCvCO0HmMjP5iQPZ0kqGkgWUH7X123tfR38MfbCVAdPDba-K3MfkogV1xvDhlkPScFr_6MxE"
 +
+        
"xtedOK2JnQZn7t9sUzSrcyjWverm7gZkPptkIVoS8TsEeMMME5vFXe_nqkEG69q3kuBUm_33tbR5oNS0ZGZKlG9r41lHBjyf9J1xN4UYV8n866d"
 +
+        "a7RPPCzshIWUtO0q9T2umWTnp-6OnOdBCkndrZmRR6pPxsD5YL0_77Wq8KT_5__fGA";
     private JwtTokenVerifier sut;
 
+    @Rule
+    public ExpectedException expectedException = ExpectedException.none();
+
     @BeforeClass
     public static void init() {
         Security.addProvider(new BouncyCastleProvider());
@@ -64,56 +75,68 @@ public class JwtTokenVerifierTest {
     }
 
     private JwtConfiguration getJWTConfiguration() {
-
         return new JwtConfiguration(Optional.of(PUBLIC_PEM_KEY));
     }
 
     @Test
     public void shouldReturnTrueOnValidSignature() {
-
-        assertThat(sut.verify(VALID_TOKEN)).isTrue();
+        assertThat(sut.verify(VALID_TOKEN_WITHOUT_ADMIN)).isTrue();
     }
 
     @Test
-    public void shouldThrowOnMismatchingSigningKey() {
+    public void shouldReturnFalseOnMismatchingSigningKey() {
         String invalidToken = 
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.Pd6t82"
 +
                 
"tPL3EZdkeYxw_DV2KimE1U2FvuLHmfR_mimJ5US3JFU4J2Gd94O7rwpSTGN1B9h-_lsTebo4ua4xHsTtmczZ9xa8a_kWKaSkqFjNFa"
 +
                 
"Fp6zcoD6ivCu03SlRqsQzSRHXo6TKbnqOt9D6Y2rNa3C4igSwoS0jUE4BgpXbc0";
 
-        assertThatThrownBy(() -> sut.verify(invalidToken))
-            .isInstanceOf(SignatureException.class);
+        assertThat(sut.verify(invalidToken)).isFalse();
     }
 
     @Test
-    public void verifyShouldThrowWhenSubjectIsNull() {
+    public void verifyShouldReturnFalseWhenSubjectIsNull() {
         String tokenWithNullSubject = 
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOm51bGwsIm5hbWUiOiJKb2huIERvZSJ9.EB"
 +
                 
"_1grWDy_kFelXs3AQeiP13ay4eG_134dWB9XPRSeWsuPs8Mz2UY-VHDxLGD-fAqv-xKXr4QFEnS7iZkdpe0tPLNSwIjqeqkC6KqQln"
 +
                 
"oC1okqWVWBDOcf7Acp1Jzp_cFTUhL5LkHvZDsyCdq5T9OOVVkzO4A9RrzIUsTrYPtRCBuYJ3ggR33cKpw191PulPGNH70rZqpUfDXe"
 +
                 
"VPY3q15vWzZH9O9IJzB2KdHRMPxl2luRjzDbh4DLp56NhZuLX_2a9UAlmbV8MQX4Z_04ybhAYrcBfxR3MgJyr0jlxSibqSbXrkXuo-"
 +
                 "PyybfZCIhK_qXUlO5OS6sO7AQhKZO9p0MQ";
 
-        assertThatThrownBy(() -> sut.verify(tokenWithNullSubject))
-            .isInstanceOf(MalformedJwtException.class)
-            .hasMessage("'subject' field in token is mandatory");
+        assertThat(sut.verify(tokenWithNullSubject)).isFalse();
     }
     
     @Test
-    public void verifyShouldThrowWhenEmptySubject() {
+    public void verifyShouldReturnFalseWhenEmptySubject() {
         String tokenWithEmptySubject = 
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIiLCJuYW1lIjoiSm9obiBEb2UifQ.UdY"
 +
                 
"s2PPzFCegUYspoDCnlJR_bJm8_z1InOv4v3tq8SJETQUarOXlhb_n6y6ujVvmJiSx9dI24Hc3Czi3RGUOXbnBDj1WPfd0aVSiUSqZr"
 +
                 
"MCHBt5vjCYqAseDaP3w4aiiFb6EV3tteJFeBLZx8XlKPYxlzRLLUADDyDSQvrFBBPxfsvCETZovKdo9ofIN64o-yx23ss63yE6oIOd"
 +
                 
"zJZ1Id40KSR2d7l3kIQJPLKUWJDnro5RAh4DOGOWNSq0JSbMhk7Zn3cXIBUpv3R8p79tui1UQpzwHMC0e6OSuWEDNQHtq-Cz85u8GG"
 +
                 "sUSbogmgObA_BimNtUq_Q1p0SGtIYBXmQ";
 
-        assertThatThrownBy(() -> sut.verify(tokenWithEmptySubject))
-            .isInstanceOf(MalformedJwtException.class)
-            .hasMessage("'subject' field in token is mandatory");
+
+        assertThat(sut.verify(tokenWithEmptySubject)).isFalse();
     }
 
     @Test
     public void shouldReturnUserLoginFromValidToken() {
+        
assertThat(sut.extractLogin(VALID_TOKEN_WITHOUT_ADMIN)).isEqualTo("1234567890");
+    }
+
+    @Test
+    public void hasAttributeShouldReturnTrueIfClaimValid() throws Exception {
+        boolean authorized = sut.hasAttribute("admin", true, 
VALID_TOKEN_ADMIN_TRUE);
+
+        assertThat(authorized).isTrue();
+    }
+
+    @Test
+    public void extractLoginShouldWorkWithAdminClaim() {
+        
assertThat(sut.extractLogin(VALID_TOKEN_ADMIN_TRUE)).isEqualTo("ad...@open-paas.org");
+    }
+
+    @Test
+    public void hasAttributeShouldThrowIfClaimInvalid() throws Exception {
+        boolean authorized = sut.hasAttribute("admin", true, 
VALID_TOKEN_ADMIN_FALSE);
 
-        assertThat(sut.extractLogin(VALID_TOKEN)).isEqualTo("1234567890");
+        assertThat(authorized).isFalse();
     }
 
 }

http://git-wip-us.apache.org/repos/asf/james-project/blob/3d327f72/server/protocols/webadmin/pom.xml
----------------------------------------------------------------------
diff --git a/server/protocols/webadmin/pom.xml 
b/server/protocols/webadmin/pom.xml
index 8ae6689..da9d076 100644
--- a/server/protocols/webadmin/pom.xml
+++ b/server/protocols/webadmin/pom.xml
@@ -166,6 +166,10 @@
                 </dependency>
                 <dependency>
                     <groupId>org.apache.james</groupId>
+                    <artifactId>james-server-jwt</artifactId>
+                </dependency>
+                <dependency>
+                    <groupId>org.apache.james</groupId>
                     <artifactId>james-server-data-api</artifactId>
                 </dependency>
                 <dependency>

http://git-wip-us.apache.org/repos/asf/james-project/blob/3d327f72/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/WebAdminServer.java
----------------------------------------------------------------------
diff --git 
a/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/WebAdminServer.java
 
b/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/WebAdminServer.java
index c1e1cad..7457219 100644
--- 
a/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/WebAdminServer.java
+++ 
b/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/WebAdminServer.java
@@ -28,6 +28,8 @@ import javax.inject.Inject;
 import org.apache.commons.configuration.ConfigurationException;
 import org.apache.commons.configuration.HierarchicalConfiguration;
 import org.apache.james.lifecycle.api.Configurable;
+import org.apache.james.webadmin.authentication.AuthenticationFilter;
+import org.apache.james.webadmin.authentication.NoAuthenticationFilter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -45,12 +47,14 @@ public class WebAdminServer implements Configurable {
     private final WebAdminConfiguration configuration;
     private final Set<Routes> routesList;
     private final Service service;
+    private final AuthenticationFilter authenticationFilter;
 
     // Spark do not allow to retrieve allocated port when using a random port. 
Thus we generate the port.
     @Inject
-    private WebAdminServer(WebAdminConfiguration configuration, Set<Routes> 
routesList) {
+    private WebAdminServer(WebAdminConfiguration configuration, Set<Routes> 
routesList, AuthenticationFilter authenticationFilter) {
         this.configuration = configuration;
         this.routesList = routesList;
+        this.authenticationFilter = authenticationFilter;
         this.service = Service.ignite();
     }
 
@@ -60,7 +64,8 @@ public class WebAdminServer implements Configurable {
             .enabled()
             .port(new RandomPort())
             .build(),
-            ImmutableSet.copyOf(routes));
+            ImmutableSet.copyOf(routes),
+            new NoAuthenticationFilter());
     }
 
     @Override
@@ -75,6 +80,7 @@ public class WebAdminServer implements Configurable {
                     httpsConfiguration.getTruststorePassword());
                 LOGGER.info("Web admin set up to use HTTPS");
             }
+            service.before(authenticationFilter);
             routesList.forEach(routes -> routes.define(service));
             LOGGER.info("Web admin server started");
         }

http://git-wip-us.apache.org/repos/asf/james-project/blob/3d327f72/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/authentication/AuthenticationFilter.java
----------------------------------------------------------------------
diff --git 
a/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/authentication/AuthenticationFilter.java
 
b/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/authentication/AuthenticationFilter.java
new file mode 100644
index 0000000..f73b818
--- /dev/null
+++ 
b/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/authentication/AuthenticationFilter.java
@@ -0,0 +1,25 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.webadmin.authentication;
+
+import spark.Filter;
+
+public interface AuthenticationFilter extends Filter {
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/3d327f72/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/authentication/JwtFilter.java
----------------------------------------------------------------------
diff --git 
a/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/authentication/JwtFilter.java
 
b/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/authentication/JwtFilter.java
new file mode 100644
index 0000000..991f412
--- /dev/null
+++ 
b/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/authentication/JwtFilter.java
@@ -0,0 +1,73 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.webadmin.authentication;
+
+import static spark.Spark.halt;
+
+import java.util.Optional;
+
+import javax.inject.Inject;
+
+import org.apache.james.jwt.JwtTokenVerifier;
+
+import spark.Request;
+import spark.Response;
+
+public class JwtFilter implements AuthenticationFilter {
+    public static final String AUTHORIZATION_HEADER_PREFIX = "Bearer ";
+    public static final String AUTHORIZATION_HEADER_NAME = "Authorization";
+
+    private final JwtTokenVerifier jwtTokenVerifier;
+
+    @Inject
+    public JwtFilter(JwtTokenVerifier jwtTokenVerifier) {
+        this.jwtTokenVerifier = jwtTokenVerifier;
+    }
+
+    @Override
+    public void handle(Request request, Response response) throws Exception {
+        Optional<String> bearer = 
Optional.ofNullable(request.headers(AUTHORIZATION_HEADER_NAME))
+            .filter(value -> value.startsWith(AUTHORIZATION_HEADER_PREFIX))
+            .map(value -> 
value.substring(AUTHORIZATION_HEADER_PREFIX.length()));
+
+        checkHeaderPresent(bearer);
+        checkValidSignature(bearer);
+        checkIsAdmin(bearer);
+    }
+
+    private void checkHeaderPresent(Optional<String> bearer) {
+        if (!bearer.isPresent()) {
+            halt(401, "No Bearer header.");
+        }
+    }
+
+    private void checkValidSignature(Optional<String> bearer) {
+        if (!jwtTokenVerifier.verify(bearer.get())) {
+            halt(401, "Invalid Bearer header.");
+        }
+    }
+
+    private void checkIsAdmin(Optional<String> bearer) {
+        if (!jwtTokenVerifier.hasAttribute("admin", true, bearer.get())) {
+            halt(401, "Non authorized user.");
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/3d327f72/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/authentication/NoAuthenticationFilter.java
----------------------------------------------------------------------
diff --git 
a/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/authentication/NoAuthenticationFilter.java
 
b/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/authentication/NoAuthenticationFilter.java
new file mode 100644
index 0000000..45a2f5b
--- /dev/null
+++ 
b/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/authentication/NoAuthenticationFilter.java
@@ -0,0 +1,31 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.webadmin.authentication;
+
+import spark.Request;
+import spark.Response;
+
+public class NoAuthenticationFilter implements AuthenticationFilter {
+
+    @Override
+    public void handle(Request request, Response response) throws Exception {
+
+    }
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/3d327f72/server/protocols/webadmin/src/test/java/org/apache/james/webadmin/authentication/JwtFilterTest.java
----------------------------------------------------------------------
diff --git 
a/server/protocols/webadmin/src/test/java/org/apache/james/webadmin/authentication/JwtFilterTest.java
 
b/server/protocols/webadmin/src/test/java/org/apache/james/webadmin/authentication/JwtFilterTest.java
new file mode 100644
index 0000000..6512ddb
--- /dev/null
+++ 
b/server/protocols/webadmin/src/test/java/org/apache/james/webadmin/authentication/JwtFilterTest.java
@@ -0,0 +1,124 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.webadmin.authentication;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.apache.james.jwt.JwtTokenVerifier;
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import com.google.common.collect.ImmutableSet;
+
+import spark.HaltException;
+import spark.Request;
+import spark.Response;
+
+public class JwtFilterTest {
+
+    public static final Matcher<HaltException> STATUS_CODE_MATCHER_401 = new 
BaseMatcher<HaltException>() {
+        @Override
+        public boolean matches(Object o) {
+            if (o instanceof HaltException) {
+                HaltException haltException = (HaltException) o;
+                return haltException.statusCode() == 401;
+            }
+            return false;
+        }
+
+        @Override
+        public void describeTo(Description description) {}
+    };
+
+    private JwtTokenVerifier jwtTokenVerifier;
+    private JwtFilter jwtFilter;
+
+    @Rule
+    public ExpectedException expectedException = ExpectedException.none();
+
+    @Before
+    public void setUp() {
+        jwtTokenVerifier = mock(JwtTokenVerifier.class);
+        jwtFilter = new JwtFilter(jwtTokenVerifier);
+    }
+
+    @Test
+    public void handleShouldRejectRequestWithHeaders() throws Exception {
+        Request request = mock(Request.class);
+        when(request.headers()).thenReturn(ImmutableSet.of());
+
+        expectedException.expect(HaltException.class);
+        expectedException.expect(STATUS_CODE_MATCHER_401);
+
+        jwtFilter.handle(request, mock(Response.class));
+    }
+
+    @Test
+    public void handleShouldRejectRequestWithBearersHeaders() throws Exception 
{
+        Request request = mock(Request.class);
+        
when(request.headers(JwtFilter.AUTHORIZATION_HEADER_NAME)).thenReturn("Invalid 
value");
+
+        expectedException.expect(HaltException.class);
+        expectedException.expect(STATUS_CODE_MATCHER_401);
+
+        jwtFilter.handle(request, mock(Response.class));
+    }
+
+    @Test
+    public void handleShouldRejectRequestWithInvalidBearerHeaders() throws 
Exception {
+        Request request = mock(Request.class);
+        
when(request.headers(JwtFilter.AUTHORIZATION_HEADER_NAME)).thenReturn("Bearer 
value");
+        when(jwtTokenVerifier.verify("value")).thenReturn(false);
+
+        expectedException.expect(HaltException.class);
+        expectedException.expect(STATUS_CODE_MATCHER_401);
+
+        jwtFilter.handle(request, mock(Response.class));
+    }
+
+    @Test
+    public void handleShouldRejectRequestWithoutAdminClaim() throws Exception {
+        Request request = mock(Request.class);
+        
when(request.headers(JwtFilter.AUTHORIZATION_HEADER_NAME)).thenReturn("Bearer 
value");
+        when(jwtTokenVerifier.verify("value")).thenReturn(true);
+        when(jwtTokenVerifier.hasAttribute("admin", true, 
"value")).thenReturn(false);
+
+        expectedException.expect(HaltException.class);
+        expectedException.expect(STATUS_CODE_MATCHER_401);
+
+        jwtFilter.handle(request, mock(Response.class));
+    }
+
+    @Test
+    public void handleShouldAcceptValidJwt() throws Exception {
+        Request request = mock(Request.class);
+        
when(request.headers(JwtFilter.AUTHORIZATION_HEADER_NAME)).thenReturn("Bearer 
value");
+        when(jwtTokenVerifier.verify("value")).thenReturn(true);
+        when(jwtTokenVerifier.hasAttribute("admin", true, 
"value")).thenReturn(true);
+
+        jwtFilter.handle(request, mock(Response.class));
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscr...@james.apache.org
For additional commands, e-mail: server-dev-h...@james.apache.org

Reply via email to