You are viewing a plain text version of this content. The canonical link for it is here.
Posted to server-dev@james.apache.org by bt...@apache.org on 2017/03/15 10:01:52 UTC
[06/22] james-project git commit: JAMES-1959 Implement a Jwt
authentication filter for webAdmin
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 <bt...@linagora.com>
Authored: Wed Mar 8 17:54:58 2017 +0700
Committer: benwa <bt...@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("admin@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-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org