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