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 rc...@apache.org on 2020/03/18 03:03:39 UTC

[james-project] 06/15: JAMES-3078 Authentication routes and filters

This is an automated email from the ASF dual-hosted git repository.

rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit 14a299a9ff15ee910e31c1a1111b3509e2c77e76
Author: Rene Cordier <rc...@linagora.com>
AuthorDate: Wed Mar 11 12:00:40 2020 +0700

    JAMES-3078 Authentication routes and filters
    
    With tests, CORS, access tokens, ...
---
 .../apache/james/metrics/api/MetricFactory.java    |   2 +
 .../dropwizard/DropWizardMetricFactory.java        |   9 +
 .../james/metrics/logger/DefaultMetricFactory.java |   8 +
 .../metrics/tests/RecordingMetricFactory.java      |   9 +
 .../apache/james/jmap/JMAPAuthenticationTest.java  |   2 +
 server/protocols/jmap-draft/pom.xml                |  22 +-
 .../james/jmap/draft/api/AccessTokenManager.java   |  13 +-
 .../jmap/draft/crypto/AccessTokenManagerImpl.java  |  25 +-
 .../jmap/draft/model/AuthenticatedRequest.java     |  16 +-
 .../AccessTokenAuthenticationStrategy.java         |  33 +--
 .../jmap/http/AuthenticationReactiveFilter.java    |  46 ++--
 .../james/jmap/http/AuthenticationRoutes.java      | 269 +++++++++++++++++++++
 .../AuthenticationStrategy.java}                   |  21 +-
 .../AccessTokenManager.java => http/JMAPUrls.java} |  22 +-
 .../{draft => http}/JWTAuthenticationStrategy.java |  21 +-
 ...ParameterAccessTokenAuthenticationStrategy.java |  26 +-
 .../james/jmap/draft/AuthenticationFilterTest.java | 156 ------------
 .../draft/crypto/AccessTokenManagerImplTest.java   |  38 +--
 .../jmap/draft/methods/RequestHandlerTest.java     |  14 +-
 .../AccessTokenAuthenticationStrategyTest.java     |  77 +++---
 .../http/AuthenticationReactiveFilterTest.java     | 157 ++++++++++++
 .../JWTAuthenticationStrategyTest.java             |  61 ++---
 ...meterAccessTokenAuthenticationStrategyTest.java |  20 +-
 23 files changed, 675 insertions(+), 392 deletions(-)

diff --git a/metrics/metrics-api/src/main/java/org/apache/james/metrics/api/MetricFactory.java b/metrics/metrics-api/src/main/java/org/apache/james/metrics/api/MetricFactory.java
index dbc8d93..2c0b464 100644
--- a/metrics/metrics-api/src/main/java/org/apache/james/metrics/api/MetricFactory.java
+++ b/metrics/metrics-api/src/main/java/org/apache/james/metrics/api/MetricFactory.java
@@ -51,6 +51,8 @@ public interface MetricFactory {
 
     <T> Publisher<T> runPublishingTimerMetric(String name, Publisher<T> publisher);
 
+    <T> Publisher<T> runPublishingTimerMetricLogP99(String name, Publisher<T> publisher);
+
     default void runPublishingTimerMetric(String name, Runnable runnable) {
         runPublishingTimerMetric(name, () -> {
             runnable.run();
diff --git a/metrics/metrics-dropwizard/src/main/java/org/apache/james/metrics/dropwizard/DropWizardMetricFactory.java b/metrics/metrics-dropwizard/src/main/java/org/apache/james/metrics/dropwizard/DropWizardMetricFactory.java
index 87e7a62..e65fbb7 100644
--- a/metrics/metrics-dropwizard/src/main/java/org/apache/james/metrics/dropwizard/DropWizardMetricFactory.java
+++ b/metrics/metrics-dropwizard/src/main/java/org/apache/james/metrics/dropwizard/DropWizardMetricFactory.java
@@ -19,6 +19,8 @@
 
 package org.apache.james.metrics.dropwizard;
 
+import static org.apache.james.metrics.api.TimeMetric.ExecutionResult.DEFAULT_100_MS_THRESHOLD;
+
 import javax.annotation.PostConstruct;
 import javax.annotation.PreDestroy;
 import javax.inject.Inject;
@@ -62,6 +64,13 @@ public class DropWizardMetricFactory implements MetricFactory, Startable {
         return Flux.from(publisher).doOnComplete(timer::stopAndPublish);
     }
 
+    @Override
+    public <T> Publisher<T> runPublishingTimerMetricLogP99(String name, Publisher<T> publisher) {
+        TimeMetric timer = timer(name);
+        return Flux.from(publisher)
+            .doOnComplete(() -> timer.stopAndPublish().logWhenExceedP99(DEFAULT_100_MS_THRESHOLD));
+    }
+
     @PostConstruct
     public void start() {
         jmxReporter.start();
diff --git a/metrics/metrics-logger/src/main/java/org/apache/james/metrics/logger/DefaultMetricFactory.java b/metrics/metrics-logger/src/main/java/org/apache/james/metrics/logger/DefaultMetricFactory.java
index c603c71..373d813 100644
--- a/metrics/metrics-logger/src/main/java/org/apache/james/metrics/logger/DefaultMetricFactory.java
+++ b/metrics/metrics-logger/src/main/java/org/apache/james/metrics/logger/DefaultMetricFactory.java
@@ -18,6 +18,8 @@
  ****************************************************************/
 package org.apache.james.metrics.logger;
 
+import static org.apache.james.metrics.api.TimeMetric.ExecutionResult.DEFAULT_100_MS_THRESHOLD;
+
 import org.apache.james.metrics.api.Metric;
 import org.apache.james.metrics.api.MetricFactory;
 import org.apache.james.metrics.api.TimeMetric;
@@ -47,4 +49,10 @@ public class DefaultMetricFactory implements MetricFactory {
         return Flux.from(publisher).doOnComplete(timer::stopAndPublish);
     }
 
+    @Override
+    public <T> Publisher<T> runPublishingTimerMetricLogP99(String name, Publisher<T> publisher) {
+        TimeMetric timer = timer(name);
+        return Flux.from(publisher)
+            .doOnComplete(() -> timer.stopAndPublish().logWhenExceedP99(DEFAULT_100_MS_THRESHOLD));
+    }
 }
diff --git a/metrics/metrics-tests/src/main/java/org/apache/james/metrics/tests/RecordingMetricFactory.java b/metrics/metrics-tests/src/main/java/org/apache/james/metrics/tests/RecordingMetricFactory.java
index 6009e54..99d293f 100644
--- a/metrics/metrics-tests/src/main/java/org/apache/james/metrics/tests/RecordingMetricFactory.java
+++ b/metrics/metrics-tests/src/main/java/org/apache/james/metrics/tests/RecordingMetricFactory.java
@@ -19,6 +19,8 @@
 
 package org.apache.james.metrics.tests;
 
+import static org.apache.james.metrics.api.TimeMetric.ExecutionResult.DEFAULT_100_MS_THRESHOLD;
+
 import java.time.Duration;
 import java.util.Collection;
 import java.util.Map;
@@ -65,6 +67,13 @@ public class RecordingMetricFactory implements MetricFactory {
         return Flux.from(publisher).doOnComplete(timer::stopAndPublish);
     }
 
+    @Override
+    public <T> Publisher<T> runPublishingTimerMetricLogP99(String name, Publisher<T> publisher) {
+        TimeMetric timer = timer(name);
+        return Flux.from(publisher)
+            .doOnComplete(() -> timer.stopAndPublish().logWhenExceedP99(DEFAULT_100_MS_THRESHOLD));
+    }
+
     public Collection<Duration> executionTimesFor(String name) {
         synchronized (executionTimes) {
             return executionTimes.get(name);
diff --git a/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/JMAPAuthenticationTest.java b/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/JMAPAuthenticationTest.java
index d978e66..08018db 100644
--- a/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/JMAPAuthenticationTest.java
+++ b/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/JMAPAuthenticationTest.java
@@ -90,6 +90,7 @@ public abstract class JMAPAuthenticationTest {
     public void mustReturnMalformedRequestWhenContentTypeIsMissing() {
         given()
             .accept(ContentType.JSON)
+            .contentType("")
         .when()
             .post("/authentication")
         .then()
@@ -110,6 +111,7 @@ public abstract class JMAPAuthenticationTest {
     @Test
     public void mustReturnMalformedRequestWhenAcceptIsMissing() {
         given()
+            .accept("")
             .contentType(ContentType.JSON)
         .when()
             .post("/authentication")
diff --git a/server/protocols/jmap-draft/pom.xml b/server/protocols/jmap-draft/pom.xml
index 49ee302..56999dd 100644
--- a/server/protocols/jmap-draft/pom.xml
+++ b/server/protocols/jmap-draft/pom.xml
@@ -110,7 +110,8 @@
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
-            <artifactId>james-server-jetty</artifactId>
+            <artifactId>james-server-jmap</artifactId>
+            <version>${project.version}</version>
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
@@ -201,6 +202,10 @@
             <artifactId>reactor-core</artifactId>
         </dependency>
         <dependency>
+            <groupId>io.projectreactor.netty</groupId>
+            <artifactId>reactor-netty</artifactId>
+        </dependency>
+        <dependency>
             <groupId>io.rest-assured</groupId>
             <artifactId>rest-assured</artifactId>
             <scope>test</scope>
@@ -210,11 +215,6 @@
             <artifactId>javax.inject</artifactId>
         </dependency>
         <dependency>
-            <groupId>javax.servlet</groupId>
-            <artifactId>javax.servlet-api</artifactId>
-            <scope>provided</scope>
-        </dependency>
-        <dependency>
             <groupId>net.javacrumbs.json-unit</groupId>
             <artifactId>json-unit-assertj</artifactId>
             <scope>test</scope>
@@ -251,16 +251,6 @@
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-api</artifactId>
         </dependency>
-        <dependency>
-            <groupId>org.zalando</groupId>
-            <artifactId>logbook-core</artifactId>
-            <version>${zalando.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.zalando</groupId>
-            <artifactId>logbook-servlet</artifactId>
-            <version>${zalando.version}</version>
-        </dependency>
     </dependencies>
 
     <build>
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/api/AccessTokenManager.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/api/AccessTokenManager.java
index 7c9c9a0..f42b2f3 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/api/AccessTokenManager.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/api/AccessTokenManager.java
@@ -22,15 +22,16 @@ package org.apache.james.jmap.draft.api;
 import org.apache.james.core.Username;
 import org.apache.james.jmap.api.access.AccessToken;
 import org.apache.james.jmap.api.access.exceptions.InvalidAccessToken;
+import org.reactivestreams.Publisher;
 
 public interface AccessTokenManager {
 
-    AccessToken grantAccessToken(Username username);
+    Publisher<AccessToken> grantAccessToken(Username username);
 
-    Username getUsernameFromToken(AccessToken token) throws InvalidAccessToken;
-    
-    boolean isValid(AccessToken token);
-    
-    void revoke(AccessToken token);
+    Publisher<Username> getUsernameFromToken(AccessToken token) throws InvalidAccessToken;
+
+    Publisher<Boolean> isValid(AccessToken token);
+
+    Publisher<Void> revoke(AccessToken token);
 
 }
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/crypto/AccessTokenManagerImpl.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/crypto/AccessTokenManagerImpl.java
index 1268086..d473174 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/crypto/AccessTokenManagerImpl.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/crypto/AccessTokenManagerImpl.java
@@ -29,6 +29,8 @@ import org.apache.james.jmap.draft.api.AccessTokenManager;
 
 import com.google.common.base.Preconditions;
 
+import reactor.core.publisher.Mono;
+
 public class AccessTokenManagerImpl implements AccessTokenManager {
 
     private final AccessTokenRepository accessTokenRepository;
@@ -39,31 +41,32 @@ public class AccessTokenManagerImpl implements AccessTokenManager {
     }
 
     @Override
-    public AccessToken grantAccessToken(Username username) {
+    public Mono<AccessToken> grantAccessToken(Username username) {
         Preconditions.checkNotNull(username);
         AccessToken accessToken = AccessToken.generate();
-        accessTokenRepository.addToken(username, accessToken).block();
-        return accessToken;
+
+        return accessTokenRepository.addToken(username, accessToken)
+            .thenReturn(accessToken);
     }
 
     @Override
-    public Username getUsernameFromToken(AccessToken token) throws InvalidAccessToken {
-        return accessTokenRepository.getUsernameFromToken(token).block();
+    public Mono<Username> getUsernameFromToken(AccessToken token) throws InvalidAccessToken {
+        return accessTokenRepository.getUsernameFromToken(token);
     }
     
     @Override
-    public boolean isValid(AccessToken token) throws InvalidAccessToken {
+    public Mono<Boolean> isValid(AccessToken token) throws InvalidAccessToken {
         try {
-            getUsernameFromToken(token);
-            return true;
+            return getUsernameFromToken(token)
+                .thenReturn(true);
         } catch (InvalidAccessToken e) {
-            return false;
+            return Mono.just(false);
         }
     }
 
     @Override
-    public void revoke(AccessToken token) {
-        accessTokenRepository.removeToken(token).block();
+    public Mono<Void> revoke(AccessToken token) {
+        return accessTokenRepository.removeToken(token);
     }
 
 }
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/AuthenticatedRequest.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/AuthenticatedRequest.java
index 62225d5..31a751f 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/AuthenticatedRequest.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/AuthenticatedRequest.java
@@ -18,26 +18,22 @@
  ****************************************************************/
 package org.apache.james.jmap.draft.model;
 
-import javax.servlet.http.HttpServletRequest;
-
-import org.apache.james.jmap.draft.AuthenticationFilter;
 import org.apache.james.mailbox.MailboxSession;
 
 public class AuthenticatedRequest extends InvocationRequest {
-    
-    public static AuthenticatedRequest decorate(InvocationRequest request, HttpServletRequest httpServletRequest) {
-        return new AuthenticatedRequest(request, httpServletRequest);
+    public static AuthenticatedRequest decorate(InvocationRequest request, MailboxSession session) {
+        return new AuthenticatedRequest(request, session);
     }
 
-    private final HttpServletRequest httpServletRequest;
+    private final MailboxSession session;
 
-    private AuthenticatedRequest(InvocationRequest request, HttpServletRequest httpServletRequest) {
+    private AuthenticatedRequest(InvocationRequest request, MailboxSession session) {
         super(request.getMethodName(), request.getParameters(), request.getMethodCallId());
-        this.httpServletRequest = httpServletRequest;
+        this.session = session;
         
     }
 
     public MailboxSession getMailboxSession() {
-        return (MailboxSession) httpServletRequest.getAttribute(AuthenticationFilter.MAILBOX_SESSION);
+        return session;
     }
 }
\ No newline at end of file
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/AccessTokenAuthenticationStrategy.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/AccessTokenAuthenticationStrategy.java
similarity index 67%
rename from server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/AccessTokenAuthenticationStrategy.java
rename to server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/AccessTokenAuthenticationStrategy.java
index 6334a11..13d2a1c 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/AccessTokenAuthenticationStrategy.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/AccessTokenAuthenticationStrategy.java
@@ -16,49 +16,40 @@
  * specific language governing permissions and limitations      *
  * under the License.                                           *
  ****************************************************************/
-package org.apache.james.jmap.draft;
-
-import java.util.Optional;
+package org.apache.james.jmap.http;
 
 import javax.inject.Inject;
-import javax.servlet.http.HttpServletRequest;
 
-import org.apache.james.core.Username;
 import org.apache.james.jmap.api.access.AccessToken;
 import org.apache.james.jmap.draft.api.AccessTokenManager;
 import org.apache.james.jmap.draft.exceptions.NoValidAuthHeaderException;
-import org.apache.james.jmap.draft.utils.HeadersAuthenticationExtractor;
 import org.apache.james.mailbox.MailboxManager;
 import org.apache.james.mailbox.MailboxSession;
 
 import com.google.common.annotations.VisibleForTesting;
 
-public class AccessTokenAuthenticationStrategy implements AuthenticationStrategy {
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.netty.http.server.HttpServerRequest;
 
+public class AccessTokenAuthenticationStrategy implements AuthenticationStrategy {
     private final AccessTokenManager accessTokenManager;
     private final MailboxManager mailboxManager;
-    private final HeadersAuthenticationExtractor authenticationExtractor;
 
     @Inject
     @VisibleForTesting
-    AccessTokenAuthenticationStrategy(AccessTokenManager accessTokenManager, MailboxManager mailboxManager, HeadersAuthenticationExtractor authenticationExtractor) {
+    AccessTokenAuthenticationStrategy(AccessTokenManager accessTokenManager, MailboxManager mailboxManager) {
         this.accessTokenManager = accessTokenManager;
         this.mailboxManager = mailboxManager;
-        this.authenticationExtractor = authenticationExtractor;
     }
 
     @Override
-    public MailboxSession createMailboxSession(HttpServletRequest httpRequest) throws NoValidAuthHeaderException {
-
-        Optional<Username> username = authenticationExtractor.authHeaders(httpRequest)
+    public Mono<MailboxSession> createMailboxSession(HttpServerRequest httpRequest) throws NoValidAuthHeaderException {
+        return Flux.fromStream(authHeaders(httpRequest))
             .map(AccessToken::fromString)
-            .filter(accessTokenManager::isValid)
-            .map(accessTokenManager::getUsernameFromToken)
-            .findFirst();
-
-        if (username.isPresent()) {
-            return mailboxManager.createSystemSession(username.get());
-        }
-        throw new NoValidAuthHeaderException();
+            .filterWhen(accessTokenManager::isValid)
+            .flatMap(accessTokenManager::getUsernameFromToken)
+            .map(mailboxManager::createSystemSession)
+            .singleOrEmpty();
     }
 }
diff --git a/metrics/metrics-logger/src/main/java/org/apache/james/metrics/logger/DefaultMetricFactory.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/AuthenticationReactiveFilter.java
similarity index 50%
copy from metrics/metrics-logger/src/main/java/org/apache/james/metrics/logger/DefaultMetricFactory.java
copy to server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/AuthenticationReactiveFilter.java
index c603c71..2a68298 100644
--- a/metrics/metrics-logger/src/main/java/org/apache/james/metrics/logger/DefaultMetricFactory.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/AuthenticationReactiveFilter.java
@@ -16,35 +16,43 @@
  * specific language governing permissions and limitations      *
  * under the License.                                           *
  ****************************************************************/
-package org.apache.james.metrics.logger;
+package org.apache.james.jmap.http;
 
-import org.apache.james.metrics.api.Metric;
+import java.util.List;
+
+import javax.inject.Inject;
+
+import org.apache.james.jmap.draft.exceptions.UnauthorizedException;
+import org.apache.james.mailbox.MailboxSession;
 import org.apache.james.metrics.api.MetricFactory;
-import org.apache.james.metrics.api.TimeMetric;
-import org.reactivestreams.Publisher;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import reactor.core.publisher.Flux;
+import com.google.common.annotations.VisibleForTesting;
 
-public class DefaultMetricFactory implements MetricFactory {
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.netty.http.server.HttpServerRequest;
 
-    public static final Logger LOGGER = LoggerFactory.getLogger(DefaultMetricFactory.class);
+public class AuthenticationReactiveFilter {
+    private static final Logger LOGGER = LoggerFactory.getLogger(AuthenticationReactiveFilter.class);
 
-    @Override
-    public Metric generate(String name) {
-        return new DefaultMetric(name);
-    }
+    private final List<AuthenticationStrategy> authMethods;
+    private final MetricFactory metricFactory;
 
-    @Override
-    public TimeMetric timer(String name) {
-        return new DefaultTimeMetric(name);
+    @Inject
+    @VisibleForTesting
+    AuthenticationReactiveFilter(List<AuthenticationStrategy> authMethods, MetricFactory metricFactory) {
+        this.authMethods = authMethods;
+        this.metricFactory = metricFactory;
     }
 
-    @Override
-    public <T> Publisher<T> runPublishingTimerMetric(String name, Publisher<T> publisher) {
-        TimeMetric timer = timer(name);
-        return Flux.from(publisher).doOnComplete(timer::stopAndPublish);
+    public Mono<MailboxSession> authenticate(HttpServerRequest request) {
+        return Mono.from(metricFactory.runPublishingTimerMetric("JMAP-authentication-filter",
+            Flux.fromStream(authMethods.stream())
+                .flatMap(auth -> auth.createMailboxSession(request))
+                .onErrorContinue((throwable, nothing) -> LOGGER.error("Error while trying to authenticate with JMAP", throwable))
+                .singleOrEmpty()
+                .switchIfEmpty(Mono.error(new UnauthorizedException()))));
     }
-
 }
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/AuthenticationRoutes.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/AuthenticationRoutes.java
new file mode 100644
index 0000000..586da3a
--- /dev/null
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/AuthenticationRoutes.java
@@ -0,0 +1,269 @@
+/****************************************************************
+ * 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.jmap.http;
+
+import static io.netty.handler.codec.http.HttpHeaderNames.ACCEPT;
+import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;
+import static io.netty.handler.codec.http.HttpResponseStatus.CREATED;
+import static io.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN;
+import static io.netty.handler.codec.http.HttpResponseStatus.NO_CONTENT;
+import static io.netty.handler.codec.http.HttpResponseStatus.OK;
+import static io.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED;
+import static org.apache.james.jmap.HttpConstants.JSON_CONTENT_TYPE;
+import static org.apache.james.jmap.HttpConstants.JSON_CONTENT_TYPE_UTF8;
+import static org.apache.james.jmap.http.JMAPUrls.AUTHENTICATION;
+
+import java.io.IOException;
+import java.util.Objects;
+
+import javax.inject.Inject;
+
+import org.apache.james.core.Username;
+import org.apache.james.jmap.JMAPRoutes;
+import org.apache.james.jmap.api.access.AccessToken;
+import org.apache.james.jmap.draft.api.AccessTokenManager;
+import org.apache.james.jmap.draft.api.SimpleTokenFactory;
+import org.apache.james.jmap.draft.api.SimpleTokenManager;
+import org.apache.james.jmap.draft.exceptions.BadRequestException;
+import org.apache.james.jmap.draft.exceptions.InternalErrorException;
+import org.apache.james.jmap.draft.exceptions.UnauthorizedException;
+import org.apache.james.jmap.draft.json.MultipleObjectMapperBuilder;
+import org.apache.james.jmap.draft.model.AccessTokenRequest;
+import org.apache.james.jmap.draft.model.AccessTokenResponse;
+import org.apache.james.jmap.draft.model.ContinuationTokenRequest;
+import org.apache.james.jmap.draft.model.ContinuationTokenResponse;
+import org.apache.james.jmap.draft.model.EndPointsResponse;
+import org.apache.james.metrics.api.MetricFactory;
+import org.apache.james.user.api.UsersRepository;
+import org.apache.james.user.api.UsersRepositoryException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import reactor.core.publisher.Mono;
+import reactor.core.scheduler.Schedulers;
+import reactor.netty.http.server.HttpServerRequest;
+import reactor.netty.http.server.HttpServerResponse;
+import reactor.netty.http.server.HttpServerRoutes;
+
+public class AuthenticationRoutes implements JMAPRoutes {
+    private static final Logger LOGGER = LoggerFactory.getLogger(AuthenticationRoutes.class);
+
+    private final ObjectMapper mapper;
+    private final UsersRepository usersRepository;
+    private final SimpleTokenManager simpleTokenManager;
+    private final AccessTokenManager accessTokenManager;
+    private final SimpleTokenFactory simpleTokenFactory;
+    private final MetricFactory metricFactory;
+    private final AuthenticationReactiveFilter authenticationReactiveFilter;
+
+    @Inject
+    public AuthenticationRoutes(UsersRepository usersRepository, SimpleTokenManager simpleTokenManager, AccessTokenManager accessTokenManager, SimpleTokenFactory simpleTokenFactory, MetricFactory metricFactory, AuthenticationReactiveFilter authenticationReactiveFilter) {
+        this.mapper = new MultipleObjectMapperBuilder()
+            .registerClass(ContinuationTokenRequest.UNIQUE_JSON_PATH, ContinuationTokenRequest.class)
+            .registerClass(AccessTokenRequest.UNIQUE_JSON_PATH, AccessTokenRequest.class)
+            .build();
+        this.usersRepository = usersRepository;
+        this.simpleTokenManager = simpleTokenManager;
+        this.accessTokenManager = accessTokenManager;
+        this.simpleTokenFactory = simpleTokenFactory;
+        this.metricFactory = metricFactory;
+        this.authenticationReactiveFilter = authenticationReactiveFilter;
+    }
+
+    @Override
+    public Logger logger() {
+        return LOGGER;
+    }
+
+    @Override
+    public HttpServerRoutes define(HttpServerRoutes builder) {
+        return builder
+            .post(AUTHENTICATION, this::post)
+            .get(AUTHENTICATION, this::returnEndPointsResponse)
+            .delete(AUTHENTICATION, this::delete)
+            .options(AUTHENTICATION, CORS_CONTROL);
+    }
+
+    private Mono<Void> post(HttpServerRequest request, HttpServerResponse response) {
+        return Mono.from(metricFactory.runPublishingTimerMetricLogP99("JMAP-authentication-post",
+            Mono.just(request)
+                .map(this::assertJsonContentType)
+                .map(this::assertAcceptJsonOnly)
+                .flatMap(this::deserialize)
+                .flatMap(objectRequest -> {
+                    if (objectRequest instanceof ContinuationTokenRequest) {
+                        return handleContinuationTokenRequest((ContinuationTokenRequest) objectRequest, response);
+                    } else if (objectRequest instanceof AccessTokenRequest) {
+                        return handleAccessTokenRequest((AccessTokenRequest) objectRequest, response);
+                    } else {
+                        throw new RuntimeException(objectRequest.getClass() + " " + objectRequest);
+                    }
+                })
+                .onErrorResume(BadRequestException.class, e -> handleBadRequest(response, e))
+                .onErrorResume(e -> handleInternalError(response, e))))
+            .subscribeOn(Schedulers.elastic());
+    }
+
+    private Mono<Void> returnEndPointsResponse(HttpServerRequest req, HttpServerResponse resp) {
+        try {
+            return authenticationReactiveFilter.authenticate(req)
+                .then(resp.status(OK)
+                    .header(CONTENT_TYPE, JSON_CONTENT_TYPE_UTF8)
+                    .sendString(Mono.just(mapper.writeValueAsString(EndPointsResponse
+                        .builder()
+                        .api(JMAPUrls.JMAP)
+                        .eventSource(JMAPUrls.NOT_IMPLEMENTED)
+                        .upload(JMAPUrls.UPLOAD)
+                        .download(JMAPUrls.DOWNLOAD)
+                        .build())))
+                    .then())
+                .onErrorResume(BadRequestException.class, e -> handleBadRequest(resp, e))
+                .onErrorResume(InternalErrorException.class, e -> handleInternalError(resp, e))
+                .onErrorResume(UnauthorizedException.class, e -> handleAuthenticationFailure(resp, e))
+                .subscribeOn(Schedulers.elastic());
+        } catch (JsonProcessingException e) {
+            throw new InternalErrorException("Error serializing endpoint response", e);
+        }
+    }
+
+    private Mono<Void> delete(HttpServerRequest req, HttpServerResponse resp) {
+        String authorizationHeader = req.requestHeaders().get("Authorization");
+
+        return authenticationReactiveFilter.authenticate(req)
+            .flatMap(session -> Mono.from(accessTokenManager.revoke(AccessToken.fromString(authorizationHeader)))
+                .then(resp.status(NO_CONTENT).send().then()))
+            .onErrorResume(UnauthorizedException.class, e -> handleAuthenticationFailure(resp, e))
+            .subscribeOn(Schedulers.elastic());
+    }
+
+    private HttpServerRequest assertJsonContentType(HttpServerRequest req) {
+        if (!Objects.equals(req.requestHeaders().get(CONTENT_TYPE), JSON_CONTENT_TYPE_UTF8)) {
+            throw new BadRequestException("Request ContentType header must be set to: " + JSON_CONTENT_TYPE_UTF8);
+        }
+        return req;
+    }
+
+    private HttpServerRequest assertAcceptJsonOnly(HttpServerRequest req) {
+        String accept = req.requestHeaders().get(ACCEPT);
+        if (accept == null || !accept.contains(JSON_CONTENT_TYPE)) {
+            throw new BadRequestException("Request Accept header must be set to JSON content type");
+        }
+        return req;
+    }
+
+    private Mono<Object> deserialize(HttpServerRequest req) {
+        return req.receive().aggregate().asInputStream()
+            .map(inputStream -> {
+                try {
+                    return mapper.readValue(inputStream, Object.class);
+                } catch (IOException e) {
+                    throw new BadRequestException("Request can't be deserialized", e);
+                }
+            })
+            .switchIfEmpty(Mono.error(new BadRequestException("Empty body")));
+    }
+
+    private Mono<Void> handleContinuationTokenRequest(ContinuationTokenRequest request, HttpServerResponse resp) {
+        try {
+            ContinuationTokenResponse continuationTokenResponse = ContinuationTokenResponse
+                .builder()
+                .continuationToken(simpleTokenFactory.generateContinuationToken(request.getUsername()))
+                .methods(ContinuationTokenResponse.AuthenticationMethod.PASSWORD)
+                .build();
+            return resp.header(CONTENT_TYPE, JSON_CONTENT_TYPE_UTF8)
+                .sendString(Mono.just(mapper.writeValueAsString(continuationTokenResponse)))
+                .then();
+        } catch (Exception e) {
+            throw new InternalErrorException("Error while responding to continuation token", e);
+        }
+    }
+
+    private Mono<Void> handleAccessTokenRequest(AccessTokenRequest request, HttpServerResponse resp) {
+        SimpleTokenManager.TokenStatus validity = simpleTokenManager.getValidity(request.getToken());
+        switch (validity) {
+            case EXPIRED:
+                return returnForbiddenAuthentication(resp);
+            case INVALID:
+                LOGGER.warn("Use of an invalid ContinuationToken : {}", request.getToken().serialize());
+                return returnUnauthorizedResponse(resp);
+            case OK:
+                return manageAuthenticationResponse(request, resp);
+            default:
+                throw new InternalErrorException(String.format("Validity %s is not implemented", validity));
+        }
+    }
+
+    private Mono<Void> manageAuthenticationResponse(AccessTokenRequest request, HttpServerResponse resp) {
+        Username username = Username.of(request.getToken().getUsername());
+
+        return authenticate(request, username)
+            .flatMap(success -> {
+                if (success) {
+                    return returnAccessTokenResponse(resp, username);
+                } else {
+                    LOGGER.info("Authentication failure for {}", username);
+                    return returnUnauthorizedResponse(resp);
+                }
+            });
+    }
+
+    private Mono<Boolean> authenticate(AccessTokenRequest request, Username username) {
+        return Mono.fromCallable(() -> {
+            try {
+                return usersRepository.test(username, request.getPassword());
+            } catch (UsersRepositoryException e) {
+                LOGGER.error("Error while trying to validate authentication for user '{}'", username, e);
+                return false;
+            }
+        }).subscribeOn(Schedulers.elastic());
+    }
+
+    private Mono<Void> returnAccessTokenResponse(HttpServerResponse resp, Username username) {
+        return Mono.from(accessTokenManager.grantAccessToken(username))
+            .map(accessToken -> AccessTokenResponse.builder()
+                .accessToken(accessToken)
+                .api(JMAPUrls.JMAP)
+                .eventSource(JMAPUrls.NOT_IMPLEMENTED)
+                .upload(JMAPUrls.UPLOAD)
+                .download(JMAPUrls.DOWNLOAD)
+                .build())
+            .flatMap(accessTokenResponse -> {
+                try {
+                    return resp.status(CREATED)
+                        .header(CONTENT_TYPE, JSON_CONTENT_TYPE_UTF8)
+                        .sendString(Mono.just(mapper.writeValueAsString(accessTokenResponse)))
+                        .then();
+                } catch (JsonProcessingException e) {
+                    throw new InternalErrorException("Could not serialize access token response", e);
+                }
+            });
+    }
+
+    private Mono<Void> returnUnauthorizedResponse(HttpServerResponse resp) {
+        return resp.status(UNAUTHORIZED).send().then();
+    }
+
+    private Mono<Void> returnForbiddenAuthentication(HttpServerResponse resp) {
+        return resp.status(FORBIDDEN).send().then();
+    }
+}
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/utils/HeadersAuthenticationExtractor.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/AuthenticationStrategy.java
similarity index 72%
rename from server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/utils/HeadersAuthenticationExtractor.java
rename to server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/AuthenticationStrategy.java
index e12f174..991e00a 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/utils/HeadersAuthenticationExtractor.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/AuthenticationStrategy.java
@@ -16,26 +16,25 @@
  * specific language governing permissions and limitations      *
  * under the License.                                           *
  ****************************************************************/
+package org.apache.james.jmap.http;
 
-package org.apache.james.jmap.draft.utils;
-
-import java.util.Collections;
-import java.util.Enumeration;
 import java.util.stream.Stream;
 
-import javax.servlet.http.HttpServletRequest;
+import org.apache.james.mailbox.MailboxSession;
 
 import com.google.common.base.Preconditions;
 
-public class HeadersAuthenticationExtractor {
+import reactor.core.publisher.Mono;
+import reactor.netty.http.server.HttpServerRequest;
+
+public interface AuthenticationStrategy {
+    Mono<MailboxSession> createMailboxSession(HttpServerRequest httpRequest);
 
-    private static final String AUTHORIZATION_HEADERS = "Authorization";
+    String AUTHORIZATION_HEADERS = "Authorization";
 
-    public Stream<String> authHeaders(HttpServletRequest httpRequest) {
+    default Stream<String> authHeaders(HttpServerRequest httpRequest) {
         Preconditions.checkArgument(httpRequest != null, "'httpRequest' is mandatory");
-        Enumeration<String> authHeaders = httpRequest.getHeaders(AUTHORIZATION_HEADERS);
 
-        return authHeaders != null ? Collections.list(authHeaders).stream() : Stream.of();
+        return httpRequest.requestHeaders().getAll(AUTHORIZATION_HEADERS).stream();
     }
-
 }
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/api/AccessTokenManager.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/JMAPUrls.java
similarity index 71%
copy from server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/api/AccessTokenManager.java
copy to server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/JMAPUrls.java
index 7c9c9a0..9252165 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/api/AccessTokenManager.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/JMAPUrls.java
@@ -17,20 +17,12 @@
  * under the License.                                           *
  ****************************************************************/
 
-package org.apache.james.jmap.draft.api;
-
-import org.apache.james.core.Username;
-import org.apache.james.jmap.api.access.AccessToken;
-import org.apache.james.jmap.api.access.exceptions.InvalidAccessToken;
-
-public interface AccessTokenManager {
-
-    AccessToken grantAccessToken(Username username);
-
-    Username getUsernameFromToken(AccessToken token) throws InvalidAccessToken;
-    
-    boolean isValid(AccessToken token);
-    
-    void revoke(AccessToken token);
+package org.apache.james.jmap.http;
 
+public interface JMAPUrls {
+    String JMAP = "/jmap";
+    String AUTHENTICATION = "/authentication";
+    String DOWNLOAD = "/download";
+    String UPLOAD = "/upload";
+    String NOT_IMPLEMENTED = "/notImplemented";
 }
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/JWTAuthenticationStrategy.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/JWTAuthenticationStrategy.java
similarity index 80%
rename from server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/JWTAuthenticationStrategy.java
rename to server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/JWTAuthenticationStrategy.java
index 8f88f06..823b24b 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/JWTAuthenticationStrategy.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/JWTAuthenticationStrategy.java
@@ -16,17 +16,15 @@
  * specific language governing permissions and limitations      *
  * under the License.                                           *
  ****************************************************************/
-package org.apache.james.jmap.draft;
+package org.apache.james.jmap.http;
 
 import java.util.stream.Stream;
 
 import javax.inject.Inject;
-import javax.servlet.http.HttpServletRequest;
 
 import org.apache.james.core.Username;
 import org.apache.james.jmap.draft.exceptions.MailboxSessionCreationException;
 import org.apache.james.jmap.draft.exceptions.NoValidAuthHeaderException;
-import org.apache.james.jmap.draft.utils.HeadersAuthenticationExtractor;
 import org.apache.james.jwt.JwtTokenVerifier;
 import org.apache.james.mailbox.MailboxManager;
 import org.apache.james.mailbox.MailboxSession;
@@ -35,27 +33,27 @@ import org.apache.james.user.api.UsersRepositoryException;
 
 import com.google.common.annotations.VisibleForTesting;
 
+import reactor.core.publisher.Mono;
+import reactor.netty.http.server.HttpServerRequest;
+
 public class JWTAuthenticationStrategy implements AuthenticationStrategy {
 
     @VisibleForTesting static final String AUTHORIZATION_HEADER_PREFIX = "Bearer ";
     private final JwtTokenVerifier tokenManager;
     private final MailboxManager mailboxManager;
-    private final HeadersAuthenticationExtractor authenticationExtractor;
     private final UsersRepository usersRepository;
 
     @Inject
     @VisibleForTesting
-    JWTAuthenticationStrategy(JwtTokenVerifier tokenManager, MailboxManager mailboxManager, HeadersAuthenticationExtractor authenticationExtractor, UsersRepository usersRepository) {
+    JWTAuthenticationStrategy(JwtTokenVerifier tokenManager, MailboxManager mailboxManager, UsersRepository usersRepository) {
         this.tokenManager = tokenManager;
         this.mailboxManager = mailboxManager;
-        this.authenticationExtractor = authenticationExtractor;
         this.usersRepository = usersRepository;
     }
 
     @Override
-    public MailboxSession createMailboxSession(HttpServletRequest httpRequest) throws MailboxSessionCreationException, NoValidAuthHeaderException {
-
-        Stream<Username> userLoginStream = extractTokensFromAuthHeaders(authenticationExtractor.authHeaders(httpRequest))
+    public Mono<MailboxSession> createMailboxSession(HttpServerRequest httpRequest) throws MailboxSessionCreationException, NoValidAuthHeaderException {
+        Stream<Username> userLoginStream = extractTokensFromAuthHeaders(authHeaders(httpRequest))
             .filter(tokenManager::verify)
             .map(tokenManager::extractLogin)
             .map(Username::of)
@@ -70,9 +68,8 @@ public class JWTAuthenticationStrategy implements AuthenticationStrategy {
         Stream<MailboxSession> mailboxSessionStream = userLoginStream
                 .map(mailboxManager::createSystemSession);
 
-        return mailboxSessionStream
-                .findFirst()
-                .orElseThrow(NoValidAuthHeaderException::new);
+        return Mono.justOrEmpty(mailboxSessionStream.findFirst())
+            .switchIfEmpty(Mono.error(new NoValidAuthHeaderException()));
     }
 
     private Stream<String> extractTokensFromAuthHeaders(Stream<String> authHeaders) {
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/QueryParameterAccessTokenAuthenticationStrategy.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/QueryParameterAccessTokenAuthenticationStrategy.java
similarity index 76%
rename from server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/QueryParameterAccessTokenAuthenticationStrategy.java
rename to server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/QueryParameterAccessTokenAuthenticationStrategy.java
index ec615c8..1aa6e5b 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/QueryParameterAccessTokenAuthenticationStrategy.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/QueryParameterAccessTokenAuthenticationStrategy.java
@@ -16,23 +16,24 @@
  * specific language governing permissions and limitations      *
  * under the License.                                           *
  ****************************************************************/
-package org.apache.james.jmap.draft;
+package org.apache.james.jmap.http;
 
-import java.util.Optional;
+import static org.apache.james.jmap.http.DownloadRoutes.BLOB_ID_PATH_PARAM;
 
 import javax.inject.Inject;
-import javax.servlet.http.HttpServletRequest;
 
 import org.apache.james.core.Username;
 import org.apache.james.jmap.draft.api.SimpleTokenManager;
 import org.apache.james.jmap.draft.exceptions.UnauthorizedException;
 import org.apache.james.jmap.draft.model.AttachmentAccessToken;
-import org.apache.james.jmap.draft.utils.DownloadPath;
 import org.apache.james.mailbox.MailboxManager;
 import org.apache.james.mailbox.MailboxSession;
 
 import com.google.common.annotations.VisibleForTesting;
 
+import reactor.core.publisher.Mono;
+import reactor.netty.http.server.HttpServerRequest;
+
 public class QueryParameterAccessTokenAuthenticationStrategy implements AuthenticationStrategy {
     private static final String AUTHENTICATION_PARAMETER = "access_token";
 
@@ -47,26 +48,21 @@ public class QueryParameterAccessTokenAuthenticationStrategy implements Authenti
     }
 
     @Override
-    public MailboxSession createMailboxSession(HttpServletRequest httpRequest) {
-
+    public Mono<MailboxSession> createMailboxSession(HttpServerRequest httpRequest) {
         return getAccessToken(httpRequest)
             .filter(tokenManager::isValid)
             .map(AttachmentAccessToken::getUsername)
             .map(Username::of)
             .map(mailboxManager::createSystemSession)
-            .orElseThrow(UnauthorizedException::new);
+            .switchIfEmpty(Mono.error(new UnauthorizedException()));
     }
 
-    private Optional<AttachmentAccessToken> getAccessToken(HttpServletRequest httpRequest) {
+    private Mono<AttachmentAccessToken> getAccessToken(HttpServerRequest httpRequest) {
         try {
-            return Optional.of(AttachmentAccessToken.from(httpRequest.getParameter(AUTHENTICATION_PARAMETER), getBlobId(httpRequest)));
+            return Mono.justOrEmpty(httpRequest.param(BLOB_ID_PATH_PARAM))
+                .map(blobId -> AttachmentAccessToken.from(httpRequest.param(AUTHENTICATION_PARAMETER), blobId));
         } catch (IllegalArgumentException e) {
-            return Optional.empty();
+            return Mono.empty();
         }
     }
-
-    private String getBlobId(HttpServletRequest httpRequest) {
-        String pathInfo = httpRequest.getPathInfo();
-        return DownloadPath.from(pathInfo).getBlobId();
-    }
 }
diff --git a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/AuthenticationFilterTest.java b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/AuthenticationFilterTest.java
deleted file mode 100644
index adfe5ad..0000000
--- a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/AuthenticationFilterTest.java
+++ /dev/null
@@ -1,156 +0,0 @@
-/****************************************************************
- * 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.jmap.draft;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-
-import javax.servlet.FilterChain;
-import javax.servlet.ServletRequest;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-import org.apache.james.core.Username;
-import org.apache.james.jmap.api.access.AccessToken;
-import org.apache.james.jmap.api.access.AccessTokenRepository;
-import org.apache.james.jmap.draft.exceptions.MailboxSessionCreationException;
-import org.apache.james.jmap.memory.access.MemoryAccessTokenRepository;
-import org.apache.james.mailbox.MailboxSession;
-import org.apache.james.metrics.tests.RecordingMetricFactory;
-import org.junit.Before;
-import org.junit.Test;
-
-import com.google.common.collect.ImmutableList;
-
-public class AuthenticationFilterTest {
-    private static final String TOKEN = "df991d2a-1c5a-4910-a90f-808b6eda133e";
-    public static final Username USERNAME = Username.of("user@domain.tld");
-
-    private HttpServletRequest mockedRequest;
-    private HttpServletResponse mockedResponse;
-    private AccessTokenRepository accessTokenRepository;
-    private AuthenticationFilter testee;
-    private FilterChain filterChain;
-
-    @Before
-    public void setup() throws Exception {
-        mockedRequest = mock(HttpServletRequest.class);
-        mockedResponse = mock(HttpServletResponse.class);
-
-        accessTokenRepository = new MemoryAccessTokenRepository(TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS));
-
-        when(mockedRequest.getMethod()).thenReturn("POST");
-        List<AuthenticationStrategy> fakeAuthenticationStrategies = ImmutableList.of(new FakeAuthenticationStrategy(false));
-
-        testee = new AuthenticationFilter(fakeAuthenticationStrategies, new RecordingMetricFactory());
-        filterChain = mock(FilterChain.class);
-    }
-
-    @Test
-    public void filterShouldReturnUnauthorizedOnNullAuthorizationHeader() throws Exception {
-        when(mockedRequest.getHeader("Authorization"))
-            .thenReturn(null);
-
-        testee.doFilter(mockedRequest, mockedResponse, filterChain);
-
-        verify(mockedResponse).sendError(HttpServletResponse.SC_UNAUTHORIZED);
-    }
-    
-    @Test
-    public void filterShouldReturnUnauthorizedOnInvalidAuthorizationHeader() throws Exception {
-        when(mockedRequest.getHeader("Authorization"))
-            .thenReturn(TOKEN);
-
-        testee.doFilter(mockedRequest, mockedResponse, filterChain);
-
-        verify(mockedResponse).sendError(HttpServletResponse.SC_UNAUTHORIZED);
-    }
-
-    @Test
-    public void filterShouldChainOnValidAuthorizationHeader() throws Exception {
-        AccessToken token = AccessToken.fromString(TOKEN);
-        when(mockedRequest.getHeader("Authorization"))
-            .thenReturn(TOKEN);
-
-        accessTokenRepository.addToken(USERNAME, token).block();
-
-        AuthenticationFilter sut = new AuthenticationFilter(ImmutableList.of(new FakeAuthenticationStrategy(true)), new RecordingMetricFactory());
-        sut.doFilter(mockedRequest, mockedResponse, filterChain);
-
-        verify(filterChain).doFilter(any(ServletRequest.class), eq(mockedResponse));
-    }
-
-    @Test
-    public void filterShouldChainAuthorizationStrategy() throws Exception {
-        AccessToken token = AccessToken.fromString(TOKEN);
-        when(mockedRequest.getHeader("Authorization"))
-            .thenReturn(TOKEN);
-
-        accessTokenRepository.addToken(USERNAME, token).block();
-
-        AuthenticationFilter sut = new AuthenticationFilter(ImmutableList.of(new FakeAuthenticationStrategy(false), new FakeAuthenticationStrategy(true)), new RecordingMetricFactory());
-        sut.doFilter(mockedRequest, mockedResponse, filterChain);
-
-        verify(filterChain).doFilter(any(ServletRequest.class), eq(mockedResponse));
-    }
-
-    @Test
-    public void filterShouldReturnUnauthorizedOnBadAuthorizationHeader() throws Exception {
-        when(mockedRequest.getHeader("Authorization"))
-            .thenReturn("bad");
-
-        testee.doFilter(mockedRequest, mockedResponse, filterChain);
-        
-        verify(mockedResponse).sendError(HttpServletResponse.SC_UNAUTHORIZED);
-    }
-
-    @Test
-    public void filterShouldReturnUnauthorizedWhenNoStrategy() throws Exception {
-        when(mockedRequest.getHeader("Authorization"))
-                .thenReturn(TOKEN);
-
-        AuthenticationFilter sut = new AuthenticationFilter(ImmutableList.of(), new RecordingMetricFactory());
-        sut.doFilter(mockedRequest, mockedResponse, filterChain);
-
-        verify(mockedResponse).sendError(HttpServletResponse.SC_UNAUTHORIZED);
-    }
-
-    private static class FakeAuthenticationStrategy implements AuthenticationStrategy {
-
-        private final boolean isAuthorized;
-
-        private FakeAuthenticationStrategy(boolean isAuthorized) {
-            this.isAuthorized = isAuthorized;
-        }
-
-        @Override
-        public MailboxSession createMailboxSession(HttpServletRequest httpRequest) {
-            if (!isAuthorized) {
-                throw new MailboxSessionCreationException(null);
-            }
-            return mock(MailboxSession.class);
-        }
-    }
-}
diff --git a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/crypto/AccessTokenManagerImplTest.java b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/crypto/AccessTokenManagerImplTest.java
index 1787530..8ee847e 100644
--- a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/crypto/AccessTokenManagerImplTest.java
+++ b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/crypto/AccessTokenManagerImplTest.java
@@ -35,7 +35,7 @@ import org.junit.jupiter.api.Test;
 
 import reactor.core.publisher.Mono;
 
-public class AccessTokenManagerImplTest {
+class AccessTokenManagerImplTest {
     private static final Username USERNAME = Username.of("username");
 
     private AccessTokenManager accessTokenManager;
@@ -60,33 +60,33 @@ public class AccessTokenManagerImplTest {
 
     @Test
     void grantShouldStoreATokenOnUsername() {
-        AccessToken token = accessTokenManager.grantAccessToken(USERNAME);
+        AccessToken token = Mono.from(accessTokenManager.grantAccessToken(USERNAME)).block();
         assertThat(accessTokenRepository.getUsernameFromToken(token).block()).isEqualTo(USERNAME);
     }
     
     @Test
     void getUsernameShouldThrowWhenNullToken() {
         assertThatNullPointerException()
-            .isThrownBy(() -> accessTokenManager.getUsernameFromToken(null));
+            .isThrownBy(() -> Mono.from(accessTokenManager.getUsernameFromToken(null)).block());
     }
 
     @Test
     void getUsernameShouldThrowWhenUnknownToken() {
-        assertThatThrownBy(() -> accessTokenManager.getUsernameFromToken(AccessToken.generate()))
+        assertThatThrownBy(() -> Mono.from(accessTokenManager.getUsernameFromToken(AccessToken.generate())).block())
             .isExactlyInstanceOf(InvalidAccessToken.class);
     }
 
     @Test
     void getUsernameShouldThrowWhenOtherToken() {
         accessTokenManager.grantAccessToken(USERNAME);
-        assertThatThrownBy(() -> accessTokenManager.getUsernameFromToken(AccessToken.generate()))
+        assertThatThrownBy(() -> Mono.from(accessTokenManager.getUsernameFromToken(AccessToken.generate())).block())
             .isExactlyInstanceOf(InvalidAccessToken.class);
     }
 
     @Test
     void getUsernameShouldReturnUsernameWhenExistingUsername() {
-        AccessToken token = accessTokenManager.grantAccessToken(USERNAME);
-        assertThat(accessTokenManager.getUsernameFromToken(token)).isEqualTo(USERNAME);
+        AccessToken token = Mono.from(accessTokenManager.grantAccessToken(USERNAME)).block();
+        assertThat(Mono.from(accessTokenManager.getUsernameFromToken(token)).block()).isEqualTo(USERNAME);
     }
     
     @Test
@@ -97,44 +97,44 @@ public class AccessTokenManagerImplTest {
     
     @Test
     void isValidShouldReturnFalseOnUnknownToken() {
-        assertThat(accessTokenManager.isValid(AccessToken.generate())).isFalse();
+        assertThat(Mono.from(accessTokenManager.isValid(AccessToken.generate())).block()).isFalse();
     }
     
     @Test
     void isValidShouldReturnFalseWhenOtherToken() {
         accessTokenManager.grantAccessToken(USERNAME);
-        assertThat(accessTokenManager.isValid(AccessToken.generate())).isFalse();
+        assertThat(Mono.from(accessTokenManager.isValid(AccessToken.generate())).block()).isFalse();
     }
     
     @Test
     void isValidShouldReturnTrueWhenValidToken() {
-        AccessToken accessToken = accessTokenManager.grantAccessToken(USERNAME);
-        assertThat(accessTokenManager.isValid(accessToken)).isTrue();
+        AccessToken accessToken = Mono.from(accessTokenManager.grantAccessToken(USERNAME)).block();
+        assertThat(Mono.from(accessTokenManager.isValid(accessToken)).block()).isTrue();
     }
     
     @Test
     void revokeShouldThrowWhenNullToken() {
         assertThatNullPointerException()
-            .isThrownBy(() -> accessTokenManager.revoke(null));
+            .isThrownBy(() -> Mono.from(accessTokenManager.revoke(null)).block());
     }
     
     @Test
     void revokeShouldNoopOnUnknownToken() {
-        accessTokenManager.revoke(AccessToken.generate());
+        Mono.from(accessTokenManager.revoke(AccessToken.generate())).block();
     }
     
     @Test
     void revokeShouldNoopOnRevokingTwice() {
         AccessToken token = AccessToken.generate();
-        accessTokenManager.revoke(token);
-        accessTokenManager.revoke(token);
+        Mono.from(accessTokenManager.revoke(token)).block();
+        Mono.from(accessTokenManager.revoke(token)).block();
     }
     
     @Test
     void revokeShouldInvalidExistingToken() {
-        AccessToken token = accessTokenManager.grantAccessToken(USERNAME);
-        accessTokenManager.revoke(token);
-        assertThat(accessTokenManager.isValid(token)).isFalse();
+        AccessToken token = Mono.from(accessTokenManager.grantAccessToken(USERNAME)).block();
+        Mono.from(accessTokenManager.revoke(token)).block();
+        assertThat(Mono.from(accessTokenManager.isValid(token)).block()).isFalse();
     }
 
     @Test
@@ -145,7 +145,7 @@ public class AccessTokenManagerImplTest {
         AccessToken accessToken = AccessToken.generate();
         when(accessTokenRepository.getUsernameFromToken(accessToken)).thenReturn(Mono.error(new InvalidAccessToken(accessToken)));
 
-        assertThatThrownBy(() -> accessTokenManager.getUsernameFromToken(accessToken))
+        assertThatThrownBy(() -> Mono.from(accessTokenManager.getUsernameFromToken(accessToken)).block())
             .isExactlyInstanceOf(InvalidAccessToken.class);
     }
 }
diff --git a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/methods/RequestHandlerTest.java b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/methods/RequestHandlerTest.java
index 3494a73..0eed4d8 100644
--- a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/methods/RequestHandlerTest.java
+++ b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/methods/RequestHandlerTest.java
@@ -21,21 +21,21 @@ package org.apache.james.jmap.draft.methods;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.tuple;
-import static org.mockito.Mockito.mock;
 
 import java.util.List;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import javax.inject.Inject;
-import javax.servlet.http.HttpServletRequest;
 
+import org.apache.james.core.Username;
 import org.apache.james.jmap.draft.json.ObjectMapperFactory;
 import org.apache.james.jmap.draft.model.AuthenticatedRequest;
-import org.apache.james.jmap.draft.model.MethodCallId;
 import org.apache.james.jmap.draft.model.InvocationRequest;
 import org.apache.james.jmap.draft.model.InvocationResponse;
+import org.apache.james.jmap.draft.model.MethodCallId;
 import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.mailbox.MailboxSessionUtil;
 import org.apache.james.mailbox.inmemory.InMemoryId;
 import org.apache.james.mailbox.inmemory.InMemoryMessageId;
 import org.junit.Before;
@@ -121,14 +121,14 @@ public class RequestHandlerTest {
     private RequestHandler testee;
     private JmapRequestParser jmapRequestParser;
     private JmapResponseWriter jmapResponseWriter;
-    private HttpServletRequest mockHttpServletRequest;
+    private MailboxSession session;
 
     @Before
     public void setup() {
         ObjectMapperFactory objectMapperFactory = new ObjectMapperFactory(new InMemoryId.Factory(), new InMemoryMessageId.Factory());
         jmapRequestParser = new JmapRequestParserImpl(objectMapperFactory);
         jmapResponseWriter = new JmapResponseWriterImpl(objectMapperFactory);
-        mockHttpServletRequest = mock(HttpServletRequest.class);
+        session = MailboxSessionUtil.create(Username.of("bob"));
         testee = new RequestHandler(ImmutableSet.of(new TestMethod()), jmapRequestParser, jmapResponseWriter);
     }
 
@@ -140,7 +140,7 @@ public class RequestHandlerTest {
                 new ObjectNode(new JsonNodeFactory(false)).textNode("#1")};
 
         RequestHandler requestHandler = new RequestHandler(ImmutableSet.of(), jmapRequestParser, jmapResponseWriter);
-        requestHandler.handle(AuthenticatedRequest.decorate(InvocationRequest.deserialize(nodes), mockHttpServletRequest));
+        requestHandler.handle(AuthenticatedRequest.decorate(InvocationRequest.deserialize(nodes), session));
     }
 
     @Test(expected = IllegalStateException.class)
@@ -208,7 +208,7 @@ public class RequestHandlerTest {
                 parameters,
                 new ObjectNode(new JsonNodeFactory(false)).textNode("#1")};
 
-        List<InvocationResponse> responses = testee.handle(AuthenticatedRequest.decorate(InvocationRequest.deserialize(nodes), mockHttpServletRequest))
+        List<InvocationResponse> responses = testee.handle(AuthenticatedRequest.decorate(InvocationRequest.deserialize(nodes), session))
                 .collect(Collectors.toList());
 
         assertThat(responses).hasSize(1)
diff --git a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/AccessTokenAuthenticationStrategyTest.java b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/http/AccessTokenAuthenticationStrategyTest.java
similarity index 61%
rename from server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/AccessTokenAuthenticationStrategyTest.java
rename to server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/http/AccessTokenAuthenticationStrategyTest.java
index 296222b..b48f305 100644
--- a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/AccessTokenAuthenticationStrategyTest.java
+++ b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/http/AccessTokenAuthenticationStrategyTest.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations      *
  * under the License.                                           *
  ****************************************************************/
-package org.apache.james.jmap.draft;
+package org.apache.james.jmap.http;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -25,74 +25,81 @@ import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import java.util.UUID;
-import java.util.stream.Stream;
-
-import javax.servlet.http.HttpServletRequest;
 
 import org.apache.james.core.Username;
 import org.apache.james.jmap.api.access.AccessToken;
 import org.apache.james.jmap.api.access.exceptions.NotAnAccessTokenException;
 import org.apache.james.jmap.draft.crypto.AccessTokenManagerImpl;
-import org.apache.james.jmap.draft.exceptions.NoValidAuthHeaderException;
-import org.apache.james.jmap.draft.utils.HeadersAuthenticationExtractor;
 import org.apache.james.mailbox.MailboxManager;
 import org.apache.james.mailbox.MailboxSession;
 import org.junit.Before;
 import org.junit.Test;
 
+import com.google.common.collect.ImmutableList;
+
+import io.netty.handler.codec.http.HttpHeaders;
+import reactor.core.publisher.Mono;
+import reactor.netty.http.server.HttpServerRequest;
+
 public class AccessTokenAuthenticationStrategyTest {
+    private static final String AUTHORIZATION_HEADERS = "Authorization";
 
     private AccessTokenManagerImpl mockedAccessTokenManager;
     private MailboxManager mockedMailboxManager;
     private AccessTokenAuthenticationStrategy testee;
-    private HttpServletRequest request;
-    private HeadersAuthenticationExtractor mockAuthenticationExtractor;
+    private HttpServerRequest mockedRequest;
+    private HttpHeaders mockedHeaders;
 
     @Before
     public void setup() {
         mockedAccessTokenManager = mock(AccessTokenManagerImpl.class);
         mockedMailboxManager = mock(MailboxManager.class);
-        mockAuthenticationExtractor = mock(HeadersAuthenticationExtractor.class);
-        request = mock(HttpServletRequest.class);
+        mockedRequest = mock(HttpServerRequest.class);
+        mockedHeaders = mock(HttpHeaders.class);
+
+        when(mockedRequest.requestHeaders())
+            .thenReturn(mockedHeaders);
 
-        testee = new AccessTokenAuthenticationStrategy(mockedAccessTokenManager, mockedMailboxManager, mockAuthenticationExtractor);
+        testee = new AccessTokenAuthenticationStrategy(mockedAccessTokenManager, mockedMailboxManager);
     }
 
     @Test
-    public void createMailboxSessionShouldThrowWhenNoAuthProvided() {
-        when(mockAuthenticationExtractor.authHeaders(request))
-            .thenReturn(Stream.empty());
+    public void createMailboxSessionShouldReturnEmptyWhenNoAuthProvided() {
+        when(mockedHeaders.getAll(AUTHORIZATION_HEADERS))
+            .thenReturn(ImmutableList.of());
 
-        assertThatThrownBy(() -> testee.createMailboxSession(request))
-            .isExactlyInstanceOf(NoValidAuthHeaderException.class);
+        assertThat(testee.createMailboxSession(mockedRequest).blockOptional())
+            .isEmpty();
     }
 
     @Test
     public void createMailboxSessionShouldThrowWhenAuthHeaderIsNotAnUUID() {
-        when(mockAuthenticationExtractor.authHeaders(request))
-            .thenReturn(Stream.of("bad"));
+        when(mockedHeaders.getAll(AUTHORIZATION_HEADERS))
+            .thenReturn(ImmutableList.of("bad"));
 
-        assertThatThrownBy(() -> testee.createMailboxSession(request))
+        assertThatThrownBy(() -> testee.createMailboxSession(mockedRequest).block())
                 .isExactlyInstanceOf(NotAnAccessTokenException.class);
     }
 
     @Test
-    public void createMailboxSessionShouldThrowWhenAuthHeaderIsInvalid() {
+    public void createMailboxSessionShouldReturnEmptyWhenAuthHeaderIsInvalid() {
         Username username = Username.of("123456789");
         MailboxSession fakeMailboxSession = mock(MailboxSession.class);
 
         when(mockedMailboxManager.createSystemSession(eq(username)))
-                .thenReturn(fakeMailboxSession);
+            .thenReturn(fakeMailboxSession);
 
         UUID authHeader = UUID.randomUUID();
-        when(mockedAccessTokenManager.getUsernameFromToken(AccessToken.fromString(authHeader.toString())))
-                .thenReturn(username);
-        when(mockAuthenticationExtractor.authHeaders(request))
-            .thenReturn(Stream.of(authHeader.toString()));
-
+        AccessToken accessToken = AccessToken.fromString(authHeader.toString());
+        when(mockedAccessTokenManager.getUsernameFromToken(accessToken))
+                .thenReturn(Mono.just(username));
+        when(mockedHeaders.getAll(AUTHORIZATION_HEADERS))
+            .thenReturn(ImmutableList.of(authHeader.toString()));
+        when(mockedAccessTokenManager.isValid(accessToken))
+            .thenReturn(Mono.just(false));
 
-        assertThatThrownBy(() -> testee.createMailboxSession(request))
-            .isExactlyInstanceOf(NoValidAuthHeaderException.class);
+        assertThat(testee.createMailboxSession(mockedRequest).blockOptional())
+            .isEmpty();
     }
 
     @Test
@@ -101,19 +108,19 @@ public class AccessTokenAuthenticationStrategyTest {
         MailboxSession fakeMailboxSession = mock(MailboxSession.class);
 
         when(mockedMailboxManager.createSystemSession(eq(username)))
-                .thenReturn(fakeMailboxSession);
+            .thenReturn(fakeMailboxSession);
 
         UUID authHeader = UUID.randomUUID();
         AccessToken accessToken = AccessToken.fromString(authHeader.toString());
         when(mockedAccessTokenManager.getUsernameFromToken(accessToken))
-                .thenReturn(username);
-        when(mockAuthenticationExtractor.authHeaders(request))
-            .thenReturn(Stream.of(authHeader.toString()));
+            .thenReturn(Mono.just(username));
+        when(mockedHeaders.getAll(AUTHORIZATION_HEADERS))
+            .thenReturn(ImmutableList.of(authHeader.toString()));
         when(mockedAccessTokenManager.isValid(accessToken))
-            .thenReturn(true);
+            .thenReturn(Mono.just(true));
 
 
-        MailboxSession result = testee.createMailboxSession(request);
+        MailboxSession result = testee.createMailboxSession(mockedRequest).block();
         assertThat(result).isEqualTo(fakeMailboxSession);
     }
-}
\ No newline at end of file
+}
diff --git a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/http/AuthenticationReactiveFilterTest.java b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/http/AuthenticationReactiveFilterTest.java
new file mode 100644
index 0000000..b236aa8
--- /dev/null
+++ b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/http/AuthenticationReactiveFilterTest.java
@@ -0,0 +1,157 @@
+/****************************************************************
+ * 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.jmap.http;
+
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.james.core.Username;
+import org.apache.james.jmap.api.access.AccessToken;
+import org.apache.james.jmap.api.access.AccessTokenRepository;
+import org.apache.james.jmap.draft.exceptions.MailboxSessionCreationException;
+import org.apache.james.jmap.draft.exceptions.UnauthorizedException;
+import org.apache.james.jmap.memory.access.MemoryAccessTokenRepository;
+import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.metrics.tests.RecordingMetricFactory;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+
+import io.netty.handler.codec.http.HttpHeaders;
+import io.netty.handler.codec.http.HttpMethod;
+import reactor.core.publisher.Mono;
+import reactor.netty.http.server.HttpServerRequest;
+
+public class AuthenticationReactiveFilterTest {
+    private static final boolean AUTHORIZED = true;
+    private static final String TOKEN = "df991d2a-1c5a-4910-a90f-808b6eda133e";
+    private static final String AUTHORIZATION_HEADERS = "Authorization";
+    private static final Username USERNAME = Username.of("user@domain.tld");
+
+    private HttpServerRequest mockedRequest;
+    private HttpHeaders mockedHeaders;
+    private AccessTokenRepository accessTokenRepository;
+    private AuthenticationReactiveFilter testee;
+
+    @Before
+    public void setup() throws Exception {
+        mockedRequest = mock(HttpServerRequest.class);
+        mockedHeaders = mock(HttpHeaders.class);
+
+        accessTokenRepository = new MemoryAccessTokenRepository(TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS));
+
+        when(mockedRequest.method())
+            .thenReturn(HttpMethod.POST);
+
+        when(mockedRequest.requestHeaders())
+            .thenReturn(mockedHeaders);
+
+        List<AuthenticationStrategy> fakeAuthenticationStrategies = ImmutableList.of(new FakeAuthenticationStrategy(!AUTHORIZED));
+
+        testee = new AuthenticationReactiveFilter(fakeAuthenticationStrategies, new RecordingMetricFactory());
+    }
+
+    @Test
+    public void filterShouldReturnUnauthorizedOnNullAuthorizationHeader() {
+        when(mockedHeaders.get(AUTHORIZATION_HEADERS))
+            .thenReturn(null);
+
+        assertThatThrownBy(() -> testee.authenticate(mockedRequest).block())
+            .isInstanceOf(UnauthorizedException.class);
+    }
+    
+    @Test
+    public void filterShouldReturnUnauthorizedOnInvalidAuthorizationHeader() {
+        when(mockedHeaders.get(AUTHORIZATION_HEADERS))
+            .thenReturn(TOKEN);
+
+        assertThatThrownBy(() -> testee.authenticate(mockedRequest).block())
+            .isInstanceOf(UnauthorizedException.class);
+    }
+
+    @Test
+    public void filterShouldReturnUnauthorizedOnBadAuthorizationHeader() {
+        when(mockedHeaders.get(AUTHORIZATION_HEADERS))
+            .thenReturn("bad");
+
+        assertThatThrownBy(() -> testee.authenticate(mockedRequest).block())
+            .isInstanceOf(UnauthorizedException.class);
+    }
+
+    @Test
+    public void filterShouldReturnUnauthorizedWhenNoStrategy() {
+        when(mockedHeaders.get(AUTHORIZATION_HEADERS))
+            .thenReturn(TOKEN);
+
+        AuthenticationReactiveFilter authFilter = new AuthenticationReactiveFilter(ImmutableList.of(), new RecordingMetricFactory());
+        assertThatThrownBy(() -> authFilter.authenticate(mockedRequest).block())
+            .isInstanceOf(UnauthorizedException.class);
+    }
+
+    @Test
+    public void filterShouldNotThrowOnValidAuthorizationHeader() {
+        AccessToken token = AccessToken.fromString(TOKEN);
+        when(mockedHeaders.get(AUTHORIZATION_HEADERS))
+            .thenReturn(TOKEN);
+
+        accessTokenRepository.addToken(USERNAME, token).block();
+
+        AuthenticationReactiveFilter authFilter = new AuthenticationReactiveFilter(ImmutableList.of(new FakeAuthenticationStrategy(AUTHORIZED)), new RecordingMetricFactory());
+
+        assertThatCode(() -> authFilter.authenticate(mockedRequest).block())
+            .doesNotThrowAnyException();
+    }
+
+    @Test
+    public void filterShouldNotThrowWhenChainingAuthorizationStrategies() {
+        AccessToken token = AccessToken.fromString(TOKEN);
+        when(mockedHeaders.get(AUTHORIZATION_HEADERS))
+            .thenReturn(TOKEN);
+
+        accessTokenRepository.addToken(USERNAME, token).block();
+
+        AuthenticationReactiveFilter authFilter = new AuthenticationReactiveFilter(ImmutableList.of(new FakeAuthenticationStrategy(!AUTHORIZED), new FakeAuthenticationStrategy(AUTHORIZED)), new RecordingMetricFactory());
+
+        assertThatCode(() -> authFilter.authenticate(mockedRequest).block())
+            .doesNotThrowAnyException();
+    }
+
+    private static class FakeAuthenticationStrategy implements AuthenticationStrategy {
+
+        private final boolean isAuthorized;
+
+        private FakeAuthenticationStrategy(boolean isAuthorized) {
+            this.isAuthorized = isAuthorized;
+        }
+
+        @Override
+        public Mono<MailboxSession> createMailboxSession(HttpServerRequest httpRequest) {
+            if (!isAuthorized) {
+                return Mono.error(new MailboxSessionCreationException(null));
+            }
+            return Mono.just(mock(MailboxSession.class));
+        }
+    }
+}
diff --git a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/JWTAuthenticationStrategyTest.java b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/http/JWTAuthenticationStrategyTest.java
similarity index 74%
rename from server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/JWTAuthenticationStrategyTest.java
rename to server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/http/JWTAuthenticationStrategyTest.java
index 5cd9e0f..1475312 100644
--- a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/JWTAuthenticationStrategyTest.java
+++ b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/http/JWTAuthenticationStrategyTest.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations      *
  * under the License.                                           *
  ****************************************************************/
-package org.apache.james.jmap.draft;
+package org.apache.james.jmap.http;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -24,15 +24,10 @@ import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
-import java.util.stream.Stream;
-
-import javax.servlet.http.HttpServletRequest;
-
 import org.apache.james.core.Username;
 import org.apache.james.domainlist.api.DomainList;
 import org.apache.james.jmap.draft.exceptions.MailboxSessionCreationException;
 import org.apache.james.jmap.draft.exceptions.NoValidAuthHeaderException;
-import org.apache.james.jmap.draft.utils.HeadersAuthenticationExtractor;
 import org.apache.james.jwt.JwtTokenVerifier;
 import org.apache.james.mailbox.MailboxManager;
 import org.apache.james.mailbox.MailboxSession;
@@ -40,33 +35,42 @@ import org.apache.james.user.memory.MemoryUsersRepository;
 import org.junit.Before;
 import org.junit.Test;
 
+import com.google.common.collect.ImmutableList;
+
+import io.netty.handler.codec.http.HttpHeaders;
+import reactor.netty.http.server.HttpServerRequest;
+
 public class JWTAuthenticationStrategyTest {
+    private static final String AUTHORIZATION_HEADERS = "Authorization";
     private static final DomainList NO_DOMAIN_LIST = null;
 
     private JWTAuthenticationStrategy testee;
     private MailboxManager mockedMailboxManager;
     private JwtTokenVerifier stubTokenVerifier;
-    private HttpServletRequest request;
-    private HeadersAuthenticationExtractor mockAuthenticationExtractor;
+    private HttpServerRequest mockedRequest;
+    private HttpHeaders mockedHeaders;
 
     @Before
     public void setup() {
         stubTokenVerifier = mock(JwtTokenVerifier.class);
         mockedMailboxManager = mock(MailboxManager.class);
-        mockAuthenticationExtractor = mock(HeadersAuthenticationExtractor.class);
-        request = mock(HttpServletRequest.class);
+        mockedRequest = mock(HttpServerRequest.class);
+        mockedHeaders = mock(HttpHeaders.class);
         MemoryUsersRepository usersRepository = MemoryUsersRepository.withoutVirtualHosting(NO_DOMAIN_LIST);
 
-        testee = new JWTAuthenticationStrategy(stubTokenVerifier, mockedMailboxManager, mockAuthenticationExtractor, usersRepository);
+        when(mockedRequest.requestHeaders())
+            .thenReturn(mockedHeaders);
+
+        testee = new JWTAuthenticationStrategy(stubTokenVerifier, mockedMailboxManager, usersRepository);
     }
 
 
     @Test
     public void createMailboxSessionShouldThrowWhenAuthHeaderIsEmpty() {
-        when(mockAuthenticationExtractor.authHeaders(request))
-            .thenReturn(Stream.empty());
+        when(mockedHeaders.getAll(AUTHORIZATION_HEADERS))
+            .thenReturn(ImmutableList.of());
 
-        assertThatThrownBy(() -> testee.createMailboxSession(request))
+        assertThatThrownBy(() -> testee.createMailboxSession(mockedRequest).block())
             .isExactlyInstanceOf(NoValidAuthHeaderException.class);
     }
 
@@ -81,20 +85,20 @@ public class JWTAuthenticationStrategyTest {
         when(stubTokenVerifier.extractLogin(validAuthHeader)).thenReturn(username);
         when(mockedMailboxManager.createSystemSession(eq(Username.of(username))))
                 .thenReturn(fakeMailboxSession);
-        when(mockAuthenticationExtractor.authHeaders(request))
-            .thenReturn(Stream.of(fakeAuthHeaderWithPrefix));
 
+        when(mockedHeaders.getAll(AUTHORIZATION_HEADERS))
+            .thenReturn(ImmutableList.of(fakeAuthHeaderWithPrefix));
 
-        assertThatThrownBy(() -> testee.createMailboxSession(request))
+        assertThatThrownBy(() -> testee.createMailboxSession(mockedRequest).block())
             .isExactlyInstanceOf(NoValidAuthHeaderException.class);
     }
 
     @Test
-    public void createMailboxSessionShouldReturnEmptyWhenAuthHeaderIsInvalid() {
-        when(mockAuthenticationExtractor.authHeaders(request))
-            .thenReturn(Stream.of("bad"));
+    public void createMailboxSessionShouldThrowWhenAuthHeaderIsInvalid() {
+        when(mockedHeaders.getAll(AUTHORIZATION_HEADERS))
+            .thenReturn(ImmutableList.of("bad"));
 
-        assertThatThrownBy(() -> testee.createMailboxSession(request))
+        assertThatThrownBy(() -> testee.createMailboxSession(mockedRequest).block())
             .isExactlyInstanceOf(NoValidAuthHeaderException.class);
     }
 
@@ -109,11 +113,10 @@ public class JWTAuthenticationStrategyTest {
         when(stubTokenVerifier.extractLogin(validAuthHeader)).thenReturn(username);
         when(mockedMailboxManager.createSystemSession(eq(Username.of(username))))
                 .thenReturn(fakeMailboxSession);
-        when(mockAuthenticationExtractor.authHeaders(request))
-            .thenReturn(Stream.of(fakeAuthHeaderWithPrefix));
-
+        when(mockedHeaders.getAll(AUTHORIZATION_HEADERS))
+            .thenReturn(ImmutableList.of(fakeAuthHeaderWithPrefix));
 
-        MailboxSession result = testee.createMailboxSession(request);
+        MailboxSession result = testee.createMailboxSession(mockedRequest).block();
         assertThat(result).isEqualTo(fakeMailboxSession);
     }
 
@@ -128,11 +131,11 @@ public class JWTAuthenticationStrategyTest {
         when(stubTokenVerifier.extractLogin(validAuthHeader)).thenReturn(username);
         when(mockedMailboxManager.createSystemSession(eq(Username.of(username))))
                 .thenReturn(fakeMailboxSession);
-        when(mockAuthenticationExtractor.authHeaders(request))
-            .thenReturn(Stream.of(fakeAuthHeaderWithPrefix));
+        when(mockedHeaders.getAll(AUTHORIZATION_HEADERS))
+            .thenReturn(ImmutableList.of(fakeAuthHeaderWithPrefix));
 
 
-        assertThatThrownBy(() -> testee.createMailboxSession(request))
+        assertThatThrownBy(() -> testee.createMailboxSession(mockedRequest).block())
             .isInstanceOf(MailboxSessionCreationException.class);
     }
-}
\ No newline at end of file
+}
diff --git a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/QueryParameterAccessTokenAuthenticationStrategyTest.java b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/http/QueryParameterAccessTokenAuthenticationStrategyTest.java
similarity index 81%
rename from server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/QueryParameterAccessTokenAuthenticationStrategyTest.java
rename to server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/http/QueryParameterAccessTokenAuthenticationStrategyTest.java
index e1fc93c..0761a8e 100644
--- a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/QueryParameterAccessTokenAuthenticationStrategyTest.java
+++ b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/http/QueryParameterAccessTokenAuthenticationStrategyTest.java
@@ -16,49 +16,49 @@
  * specific language governing permissions and limitations      *
  * under the License.                                           *
  ****************************************************************/
-package org.apache.james.jmap.draft;
+package org.apache.james.jmap.http;
 
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
-import javax.servlet.http.HttpServletRequest;
-
 import org.apache.james.jmap.draft.api.SimpleTokenManager;
 import org.apache.james.jmap.draft.exceptions.UnauthorizedException;
 import org.apache.james.mailbox.MailboxManager;
 import org.junit.Before;
 import org.junit.Test;
 
+import reactor.netty.http.server.HttpServerRequest;
+
 public class QueryParameterAccessTokenAuthenticationStrategyTest {
 
     private QueryParameterAccessTokenAuthenticationStrategy testee;
-    private HttpServletRequest request;
+    private HttpServerRequest mockedRequest;
 
     @Before
     public void setup() {
         SimpleTokenManager mockedSimpleTokenManager = mock(SimpleTokenManager.class);
         MailboxManager mockedMailboxManager = mock(MailboxManager.class);
-        request = mock(HttpServletRequest.class);
+        mockedRequest = mock(HttpServerRequest.class);
 
         testee = new QueryParameterAccessTokenAuthenticationStrategy(mockedSimpleTokenManager, mockedMailboxManager);
     }
 
     @Test
     public void createMailboxSessionShouldThrowWhenNoAccessTokenProvided() {
-        when(request.getParameter("access_token"))
+        when(mockedRequest.param("access_token"))
             .thenReturn(null);
 
-        assertThatThrownBy(() -> testee.createMailboxSession(request))
+        assertThatThrownBy(() -> testee.createMailboxSession(mockedRequest).block())
             .isExactlyInstanceOf(UnauthorizedException.class);
     }
 
     @Test
     public void createMailboxSessionShouldThrowWhenAccessTokenIsNotValid() {
-        when(request.getParameter("access_token"))
+        when(mockedRequest.param("access_token"))
             .thenReturn("bad");
 
-        assertThatThrownBy(() -> testee.createMailboxSession(request))
-                .isExactlyInstanceOf(UnauthorizedException.class);
+        assertThatThrownBy(() -> testee.createMailboxSession(mockedRequest).block())
+            .isExactlyInstanceOf(UnauthorizedException.class);
     }
 }
\ No newline at end of file


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