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