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 <rcord...@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("u...@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("u...@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-unsubscr...@james.apache.org For additional commands, e-mail: server-dev-h...@james.apache.org