You are viewing a plain text version of this content. The canonical link for it is here.
Posted to server-dev@james.apache.org by bt...@apache.org on 2020/05/29 03:03:15 UTC

[james-project] 08/08: JAMES-3093 Add BasicAuthenticationStrategy for JMAP RFC-8621

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

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

commit 81880375289776284c9fbc9f524dc1f5d3b22dab
Author: duc91 <du...@gmail.com>
AuthorDate: Tue May 26 14:54:14 2020 +0700

    JAMES-3093 Add BasicAuthenticationStrategy for JMAP RFC-8621
---
 .../DistributedBasicAuthenticationTest.java        |  51 +++++++
 .../rfc8621/contract/AuthenticationContract.scala  | 151 +++++++++++++++++++++
 .../jmap/rfc8621/contract/EchoMethodContract.scala |  83 +++--------
 .../james/jmap/rfc8621/contract/Fixture.scala      |  94 +++++++++++++
 .../rfc8621/memory/MemoryAuthenticationTest.java   |  38 ++++++
 server/protocols/jmap-rfc-8621/pom.xml             |  60 +++++---
 .../jmap/http/BasicAuthenticationStrategy.scala    | 109 +++++++++++++++
 .../org/apache/james/jmap/http/SessionRoutes.scala |   7 +-
 .../apache/james/jmap/routes/JMAPApiRoutes.scala   |  34 +++--
 .../james/jmap/http/UserCredentialParserTest.scala | 135 ++++++++++++++++++
 .../james/jmap/routes/JMAPApiRoutesTest.scala      | 130 +++++++++++++-----
 11 files changed, 760 insertions(+), 132 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedBasicAuthenticationTest.java b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedBasicAuthenticationTest.java
new file mode 100644
index 0000000..1d7932b
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedBasicAuthenticationTest.java
@@ -0,0 +1,51 @@
+/****************************************************************
+ * 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.rfc8621.distributed;
+
+import org.apache.james.CassandraExtension;
+import org.apache.james.CassandraRabbitMQJamesConfiguration;
+import org.apache.james.CassandraRabbitMQJamesServerMain;
+import org.apache.james.DockerElasticSearchExtension;
+import org.apache.james.GuiceJamesServer;
+import org.apache.james.JamesServerBuilder;
+import org.apache.james.JamesServerExtension;
+import org.apache.james.jmap.rfc8621.contract.AuthenticationContract;
+import org.apache.james.modules.AwsS3BlobStoreExtension;
+import org.apache.james.modules.RabbitMQExtension;
+import org.apache.james.modules.TestJMAPServerModule;
+import org.apache.james.modules.blobstore.BlobStoreConfiguration;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+public class DistributedBasicAuthenticationTest implements AuthenticationContract {
+    @RegisterExtension
+    static JamesServerExtension testExtension = new JamesServerBuilder<CassandraRabbitMQJamesConfiguration>(tmpDir ->
+        CassandraRabbitMQJamesConfiguration.builder()
+            .workingDirectory(tmpDir)
+            .configurationFromClasspath()
+            .blobStore(BlobStoreConfiguration.objectStorage().disableCache())
+            .build())
+        .extension(new DockerElasticSearchExtension())
+        .extension(new CassandraExtension())
+        .extension(new RabbitMQExtension())
+        .extension(new AwsS3BlobStoreExtension())
+        .server(configuration -> CassandraRabbitMQJamesServerMain.createServer(configuration)
+            .overrideWith(new TestJMAPServerModule()))
+        .build();
+}
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/AuthenticationContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/AuthenticationContract.scala
new file mode 100644
index 0000000..6716c1a
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/AuthenticationContract.scala
@@ -0,0 +1,151 @@
+/****************************************************************
+ * 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.rfc8621.contract
+
+import java.nio.charset.StandardCharsets.UTF_8
+import java.util.Base64
+
+import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
+import io.restassured.RestAssured._
+import io.restassured.authentication.NoAuthScheme
+import io.restassured.http.{Header, Headers}
+import org.apache.http.HttpStatus.{SC_OK, SC_UNAUTHORIZED}
+import org.apache.james.GuiceJamesServer
+import org.apache.james.jmap.rfc8621.contract.AuthenticationContract._
+import org.apache.james.jmap.rfc8621.contract.Fixture._
+import org.apache.james.jmap.rfc8621.contract.tags.CategoryTags
+import org.apache.james.utils.DataProbeImpl
+import org.junit.jupiter.api.{BeforeEach, Tag, Test}
+
+object AuthenticationContract {
+  private val AUTHORIZATION_HEADER: String = "Authorization"
+}
+
+trait AuthenticationContract {
+  @BeforeEach
+  def setUp(server: GuiceJamesServer): Unit = {
+    server.getProbe(classOf[DataProbeImpl])
+      .fluent()
+      .addDomain(DOMAIN.asString())
+      .addUser(BOB.asString(), BOB_PASSWORD)
+      .addDomain(_2_DOT_DOMAIN.asString())
+      .addUser(ALICE.asString(), ALICE_PASSWORD)
+
+    requestSpecification = baseRequestSpecBuilder(server)
+      .setAuth(new NoAuthScheme())
+      .build
+  }
+
+  @Test
+  def postShouldRespondUnauthorizedWhenNoAuthorizationHeader(): Unit = {
+    `given`()
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(ECHO_REQUEST_OBJECT)
+    .when()
+      .post()
+    .then
+      .statusCode(SC_UNAUTHORIZED)
+  }
+
+  @Test
+  @Tag(CategoryTags.BASIC_FEATURE)
+  def postShouldRespond200WhenHasCredentials(): Unit = {
+    val authHeader: Header = new Header(AUTHORIZATION_HEADER, s"Basic ${toBase64(s"${BOB.asString}:$BOB_PASSWORD")}")
+    `given`()
+      .headers(getHeadersWith(authHeader))
+      .body(ECHO_REQUEST_OBJECT)
+    .when()
+      .post()
+    .then
+      .statusCode(SC_OK)
+  }
+
+  @Test
+  def postShouldRespond401WhenCredentialsWithInvalidUser(): Unit = {
+    val authHeader: Header = new Header(AUTHORIZATION_HEADER, s"Basic ${toBase64(s"${BOB.getLocalPart}@@$DOMAIN:$BOB_PASSWORD")}")
+    `given`()
+      .headers(getHeadersWith(authHeader))
+      .body(ECHO_REQUEST_OBJECT)
+    .when()
+      .post()
+    .then
+      .statusCode(SC_UNAUTHORIZED)
+  }
+
+  @Test
+  def postShouldRespondOKWhenCredentialsWith2DotDomain(): Unit = {
+    val authHeader: Header = new Header(AUTHORIZATION_HEADER, s"Basic ${toBase64(s"${ALICE.asString}:$ALICE_PASSWORD")}")
+    `given`()
+      .headers(getHeadersWith(authHeader))
+      .body(ECHO_REQUEST_OBJECT)
+    .when()
+      .post()
+    .then
+      .statusCode(SC_OK)
+  }
+
+  @Test
+  def postShouldRespond401WhenCredentialsWithSpaceDomain(): Unit = {
+    val authHeader: Header = new Header(AUTHORIZATION_HEADER, s"Basic ${toBase64(s"${BOB.getLocalPart}@$DOMAIN_WITH_SPACE:$BOB_PASSWORD")}")
+    `given`()
+      .headers(getHeadersWith(authHeader))
+      .body(ECHO_REQUEST_OBJECT)
+    .when()
+      .post()
+    .then
+      .statusCode(SC_UNAUTHORIZED)
+  }
+
+  @Test
+  def postShouldRespond401WhenUserNotFound(): Unit = {
+    val authHeader: Header = new Header(AUTHORIZATION_HEADER, s"Basic ${toBase64(s"usernotfound@$DOMAIN:$BOB_PASSWORD")}")
+    `given`()
+      .headers(getHeadersWith(authHeader))
+      .body(ECHO_REQUEST_OBJECT)
+    .when()
+      .post()
+    .then
+      .statusCode(SC_UNAUTHORIZED)
+  }
+
+  @Test
+  @Tag(CategoryTags.BASIC_FEATURE)
+  def postShouldRespond401WhenWrongPassword(): Unit = {
+    val authHeader: Header = new Header(AUTHORIZATION_HEADER, s"Basic ${toBase64(s"${BOB.asString}:WRONG_PASSWORD")}")
+    `given`()
+      .headers(getHeadersWith(authHeader))
+      .body(ECHO_REQUEST_OBJECT)
+    .when()
+      .post()
+    .then
+      .statusCode(SC_UNAUTHORIZED)
+  }
+
+  private def getHeadersWith(authHeader: Header): Headers = {
+    new Headers(
+      new Header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER),
+      authHeader
+    )
+  }
+
+  private def toBase64(stringValue: String): String = {
+    Base64.getEncoder.encodeToString(stringValue.getBytes(UTF_8))
+  }
+}
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EchoMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EchoMethodContract.scala
index 79578fc..509106d 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EchoMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EchoMethodContract.scala
@@ -18,41 +18,20 @@
  ****************************************************************/
 package org.apache.james.jmap.rfc8621.contract
 
-import java.nio.charset.StandardCharsets
-
 import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
-import io.restassured.RestAssured
-import io.restassured.builder.RequestSpecBuilder
-import io.restassured.config.EncoderConfig.encoderConfig
-import io.restassured.config.RestAssuredConfig.newConfig
-import io.restassured.http.ContentType
+import io.restassured.RestAssured._
+import io.restassured.http.ContentType.JSON
 import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
-import org.apache.http.HttpStatus
+import org.apache.http.HttpStatus.SC_OK
 import org.apache.james.GuiceJamesServer
-import org.apache.james.jmap.JMAPUrls.JMAP
-import org.apache.james.jmap.draft.JmapGuiceProbe
+import org.apache.james.jmap.http.UserCredential
 import org.apache.james.jmap.rfc8621.contract.EchoMethodContract._
+import org.apache.james.jmap.rfc8621.contract.Fixture._
 import org.apache.james.jmap.rfc8621.contract.tags.CategoryTags
+import org.apache.james.utils.DataProbeImpl
 import org.junit.jupiter.api.{BeforeEach, Tag, Test}
 
 object EchoMethodContract {
-
-  private val REQUEST_OBJECT: String =
-    """{
-      |  "using": [
-      |    "urn:ietf:params:jmap:core"
-      |  ],
-      |  "methodCalls": [
-      |    [
-      |      "Core/echo",
-      |      {
-      |        "arg1": "arg1data",
-      |        "arg2": "arg2data"
-      |      },
-      |      "c1"
-      |    ]
-      |  ]
-      |}""".stripMargin
   private val REQUEST_OBJECT_WITH_UNSUPPORTED_METHOD: String =
     """{
       |  "using": [
@@ -77,20 +56,6 @@ object EchoMethodContract {
       |  ]
       |}""".stripMargin
 
-  private val RESPONSE_OBJECT: String =
-    """{
-      |  "sessionState": "75128aab4b1b",
-      |  "methodResponses": [
-      |    [
-      |      "Core/echo",
-      |      {
-      |        "arg1": "arg1data",
-      |        "arg2": "arg2data"
-      |      },
-      |      "c1"
-      |    ]
-      |  ]
-      |}""".stripMargin
   private val RESPONSE_OBJECT_WITH_UNSUPPORTED_METHOD: String =
     """{
       |  "sessionState": "75128aab4b1b",
@@ -112,55 +77,51 @@ object EchoMethodContract {
       |    ]
       |  ]
       |}""".stripMargin
-
-  private val ACCEPT_RFC8621_VERSION_HEADER: String = """application/json; jmapVersion=rfc-8621"""
 }
 
 trait EchoMethodContract {
 
   @BeforeEach
   def setUp(server: GuiceJamesServer): Unit = {
-    RestAssured.requestSpecification = new RequestSpecBuilder()
-      .setContentType(ContentType.JSON)
-      .setAccept(ContentType.JSON)
-      .setConfig(newConfig.encoderConfig(encoderConfig.defaultContentCharset(StandardCharsets.UTF_8)))
-      .setPort(server.getProbe(classOf[JmapGuiceProbe])
-        .getJmapPort
-        .getValue)
-      .setBasePath(JMAP)
+    server.getProbe(classOf[DataProbeImpl])
+      .fluent()
+      .addDomain(DOMAIN.asString())
+      .addUser(BOB.asString(), BOB_PASSWORD)
+
+    requestSpecification = baseRequestSpecBuilder(server)
+        .setAuth(authScheme(UserCredential(BOB, BOB_PASSWORD)))
       .build
   }
 
   @Test
   @Tag(CategoryTags.BASIC_FEATURE)
   def echoMethodShouldRespondOKWithRFC8621VersionAndSupportedMethod(): Unit = {
-    val response: String = RestAssured
-      .`given`()
+
+    val response: String = `given`()
         .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
-        .body(REQUEST_OBJECT)
+        .body(ECHO_REQUEST_OBJECT)
       .when()
         .post()
       .then
-        .statusCode(HttpStatus.SC_OK)
-        .contentType(ContentType.JSON)
+        .statusCode(SC_OK)
+        .contentType(JSON)
       .extract()
         .body()
         .asString()
 
-    assertThatJson(response).isEqualTo(RESPONSE_OBJECT)
+    assertThatJson(response).isEqualTo(ECHO_RESPONSE_OBJECT)
   }
 
   @Test
   def echoMethodShouldRespondWithRFC8621VersionAndUnsupportedMethod(): Unit = {
-    val response: String = RestAssured
-      .`given`()
+    val response: String = `given`()
         .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
         .body(REQUEST_OBJECT_WITH_UNSUPPORTED_METHOD)
       .when()
         .post()
       .then
-        .statusCode(HttpStatus.SC_OK)
-        .contentType(ContentType.JSON)
+        .statusCode(SC_OK)
+        .contentType(JSON)
       .extract()
         .body()
         .asString()
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/Fixture.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/Fixture.scala
new file mode 100644
index 0000000..56be582
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/Fixture.scala
@@ -0,0 +1,94 @@
+/****************************************************************
+ * 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.rfc8621.contract
+
+import java.nio.charset.StandardCharsets
+
+import io.restassured.authentication.PreemptiveBasicAuthScheme
+import io.restassured.builder.RequestSpecBuilder
+import io.restassured.config.EncoderConfig.encoderConfig
+import io.restassured.config.RestAssuredConfig.newConfig
+import io.restassured.http.ContentType
+import org.apache.james.GuiceJamesServer
+import org.apache.james.core.{Domain, Username}
+import org.apache.james.jmap.JMAPUrls.JMAP
+import org.apache.james.jmap.draft.JmapGuiceProbe
+import org.apache.james.jmap.http.UserCredential
+
+object Fixture {
+  def baseRequestSpecBuilder(server: GuiceJamesServer) = new RequestSpecBuilder()
+    .setContentType(ContentType.JSON)
+    .setAccept(ContentType.JSON)
+    .setConfig(newConfig.encoderConfig(encoderConfig.defaultContentCharset(StandardCharsets.UTF_8)))
+    .setPort(server.getProbe(classOf[JmapGuiceProbe])
+      .getJmapPort
+      .getValue)
+    .setBasePath(JMAP)
+
+  def authScheme(userCredential: UserCredential): PreemptiveBasicAuthScheme = {
+    val authScheme: PreemptiveBasicAuthScheme = new PreemptiveBasicAuthScheme
+    authScheme.setUserName(userCredential.username.asString())
+    authScheme.setPassword(userCredential.password)
+
+    authScheme
+  }
+
+  val DOMAIN: Domain = Domain.of("domain.tld")
+  val DOMAIN_WITH_SPACE: Domain = Domain.of("dom ain.tld")
+  val _2_DOT_DOMAIN: Domain = Domain.of("do.main.tld")
+  val BOB: Username = Username.fromLocalPartWithDomain("bob", DOMAIN)
+  val ALICE: Username = Username.fromLocalPartWithDomain("alice", _2_DOT_DOMAIN)
+  val BOB_PASSWORD: String = "bobpassword"
+  val ALICE_PASSWORD: String = "alicepassword"
+
+  val ECHO_REQUEST_OBJECT: String =
+    """{
+      |  "using": [
+      |    "urn:ietf:params:jmap:core"
+      |  ],
+      |  "methodCalls": [
+      |    [
+      |      "Core/echo",
+      |      {
+      |        "arg1": "arg1data",
+      |        "arg2": "arg2data"
+      |      },
+      |      "c1"
+      |    ]
+      |  ]
+      |}""".stripMargin
+
+  val ECHO_RESPONSE_OBJECT: String =
+    """{
+      |  "sessionState": "75128aab4b1b",
+      |  "methodResponses": [
+      |    [
+      |      "Core/echo",
+      |      {
+      |        "arg1": "arg1data",
+      |        "arg2": "arg2data"
+      |      },
+      |      "c1"
+      |    ]
+      |  ]
+      |}""".stripMargin
+
+  val ACCEPT_RFC8621_VERSION_HEADER: String = "application/json; jmapVersion=rfc-8621"
+}
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryAuthenticationTest.java b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryAuthenticationTest.java
new file mode 100644
index 0000000..c99bf0f
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryAuthenticationTest.java
@@ -0,0 +1,38 @@
+/****************************************************************
+ * 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.rfc8621.memory;
+
+import static org.apache.james.MemoryJamesServerMain.IN_MEMORY_SERVER_AGGREGATE_MODULE;
+
+import org.apache.james.GuiceJamesServer;
+import org.apache.james.JamesServerBuilder;
+import org.apache.james.JamesServerExtension;
+import org.apache.james.jmap.rfc8621.contract.AuthenticationContract;
+import org.apache.james.modules.TestJMAPServerModule;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+public class MemoryAuthenticationTest implements AuthenticationContract {
+    @RegisterExtension
+    static JamesServerExtension testExtension = new JamesServerBuilder<>(JamesServerBuilder.defaultConfigurationProvider())
+        .server(configuration -> GuiceJamesServer.forConfiguration(configuration)
+            .combineWith(IN_MEMORY_SERVER_AGGREGATE_MODULE)
+            .overrideWith(new TestJMAPServerModule()))
+        .build();
+}
diff --git a/server/protocols/jmap-rfc-8621/pom.xml b/server/protocols/jmap-rfc-8621/pom.xml
index ba30cd2..feccb0c 100644
--- a/server/protocols/jmap-rfc-8621/pom.xml
+++ b/server/protocols/jmap-rfc-8621/pom.xml
@@ -34,26 +34,25 @@
 
     <dependencies>
         <dependency>
-            <groupId>com.typesafe.play</groupId>
-            <artifactId>play-json_${scala.base}</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>eu.timepit</groupId>
-            <artifactId>refined_${scala.base}</artifactId>
-            <version>0.9.13</version>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>apache-james-mailbox-api</artifactId>
         </dependency>
         <dependency>
-            <groupId>io.rest-assured</groupId>
-            <artifactId>rest-assured</artifactId>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>apache-james-mailbox-api</artifactId>
+            <type>test-jar</type>
             <scope>test</scope>
         </dependency>
         <dependency>
-            <groupId>io.projectreactor.netty</groupId>
-            <artifactId>reactor-netty</artifactId>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>apache-james-mailbox-memory</artifactId>
+            <scope>test</scope>
         </dependency>
         <dependency>
-            <groupId>io.projectreactor</groupId>
-            <artifactId>reactor-scala-extensions_${scala.base}</artifactId>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>apache-james-mailbox-memory</artifactId>
+            <scope>test</scope>
+            <type>test-jar</type>
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
@@ -61,26 +60,47 @@
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
-            <artifactId>james-server-jmap</artifactId>
+            <artifactId>james-server-data-api</artifactId>
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
-            <artifactId>testing-base</artifactId>
+            <artifactId>james-server-data-memory</artifactId>
             <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
-            <artifactId>apache-james-mailbox-api</artifactId>
+            <artifactId>james-server-jmap</artifactId>
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
-            <artifactId>apache-james-mailbox-api</artifactId>
-            <type>test-jar</type>
+            <artifactId>metrics-tests</artifactId>
             <scope>test</scope>
         </dependency>
         <dependency>
-            <groupId>net.javacrumbs.json-unit</groupId>
-            <artifactId>json-unit-assertj</artifactId>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>testing-base</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.typesafe.play</groupId>
+            <artifactId>play-json_${scala.base}</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>eu.timepit</groupId>
+            <artifactId>refined_${scala.base}</artifactId>
+            <version>0.9.13</version>
+        </dependency>
+        <dependency>
+            <groupId>io.projectreactor</groupId>
+            <artifactId>reactor-scala-extensions_${scala.base}</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>
         </dependency>
         <dependency>
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/BasicAuthenticationStrategy.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/BasicAuthenticationStrategy.scala
new file mode 100644
index 0000000..52d9127
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/BasicAuthenticationStrategy.scala
@@ -0,0 +1,109 @@
+/** **************************************************************
+ * 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 java.util.Base64
+
+import eu.timepit.refined.api.Refined
+import eu.timepit.refined.auto._
+import eu.timepit.refined.refineV
+import eu.timepit.refined.string.MatchesRegex
+import javax.inject.Inject
+import org.apache.james.core.Username
+import org.apache.james.jmap.http.UserCredential._
+import org.apache.james.mailbox.{MailboxManager, MailboxSession}
+import org.apache.james.user.api.UsersRepository
+import org.slf4j.LoggerFactory
+import reactor.core.publisher.Mono
+import reactor.core.scala.publisher.SFlux
+import reactor.netty.http.server.HttpServerRequest
+
+import scala.compat.java8.StreamConverters._
+import scala.util.{Failure, Success, Try}
+
+object UserCredential {
+  type BasicAuthenticationHeaderValue = String Refined MatchesRegex["Basic [\\d\\w=]++"]
+  type CredentialsAsString = String Refined MatchesRegex[".*:.*"]
+
+  private val logger = LoggerFactory.getLogger(classOf[UserCredential])
+  private val BASIC_AUTHENTICATION_PREFIX: String = "Basic "
+
+  def parseUserCredentials(token: String): Option[UserCredential] = {
+    val refinedValue: Either[String, BasicAuthenticationHeaderValue] = refineV(token)
+
+    refinedValue match {
+      // Ignore Authentication headers not being Basic Auth
+      case Left(_) => None
+      case Right(value) => extractUserCredentialsAsString(value)
+    }
+  }
+
+  private def extractUserCredentialsAsString(token: BasicAuthenticationHeaderValue): Option[UserCredential] = {
+    val encodedCredentials = token.replace(BASIC_AUTHENTICATION_PREFIX, "")
+    val decodedCredentialsString = new String(Base64.getDecoder.decode(encodedCredentials))
+    val refinedValue: Either[String, CredentialsAsString] = refineV(decodedCredentialsString)
+
+    refinedValue match {
+      case Left(errorMessage: String) =>
+        logger.info(s"Supplied basic authentication credentials do not match expected format. $errorMessage")
+        None
+      case Right(value) => toCredential(value)
+    }
+  }
+
+  private def toCredential(token: CredentialsAsString): Option[UserCredential] = {
+    val partSeparatorIndex: Int = token.indexOf(':')
+    val usernameString: String = token.substring(0, partSeparatorIndex)
+    val passwordString: String = token.substring(partSeparatorIndex + 1)
+
+    Try(UserCredential(Username.of(usernameString), passwordString)) match {
+      case Success(credential) => Some(credential)
+      case Failure(throwable:IllegalArgumentException) =>
+        logger.info("Username is not valid", throwable)
+        None
+      case Failure(unexpectedException) =>
+        logger.error("Unexpected Exception", unexpectedException)
+        None
+    }
+  }
+}
+
+case class UserCredential(username: Username, password: String)
+
+class BasicAuthenticationStrategy @Inject()(val usersRepository: UsersRepository,
+                                            val mailboxManager: MailboxManager) extends AuthenticationStrategy {
+
+  override def createMailboxSession(httpRequest: HttpServerRequest): Mono[MailboxSession] = {
+    SFlux.fromStream(() => authHeaders(httpRequest).toScala[Stream])
+      .map(parseUserCredentials)
+      .handle(publishNext)
+      .filter(isValid)
+      .map(_.username)
+      .map(mailboxManager.createSystemSession)
+      .singleOrEmpty()
+      .asJava()
+  }
+
+  private def publishNext[T]: (Option[T], reactor.core.publisher.SynchronousSink[T]) => Unit =
+    (maybeT, sink) => maybeT.foreach(t => sink.next(t))
+
+  private def isValid(userCredential: UserCredential): Boolean =
+    usersRepository.test(userCredential.username, userCredential.password)
+}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/SessionRoutes.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/SessionRoutes.scala
index 465c212..a7efeab 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/SessionRoutes.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/SessionRoutes.scala
@@ -24,12 +24,13 @@ import java.util.stream.Stream
 import io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE
 import io.netty.handler.codec.http.HttpMethod
 import io.netty.handler.codec.http.HttpResponseStatus.OK
-import javax.inject.Inject
+import javax.inject.{Inject, Named}
 import org.apache.james.jmap.HttpConstants.JSON_CONTENT_TYPE_UTF8
 import org.apache.james.jmap.JMAPRoutes.CORS_CONTROL
 import org.apache.james.jmap.JMAPUrls.AUTHENTICATION
 import org.apache.james.jmap.exceptions.UnauthorizedException
 import org.apache.james.jmap.http.SessionRoutes.{JMAP_SESSION, LOGGER}
+import org.apache.james.jmap.http.rfc8621.InjectionKeys
 import org.apache.james.jmap.json.Serializer
 import org.apache.james.jmap.model.Session
 import org.apache.james.jmap.{Endpoint, JMAPRoute, JMAPRoutes}
@@ -46,8 +47,8 @@ object SessionRoutes {
 }
 
 @Inject
-class SessionRoutes(val serializer: Serializer,
-                    val authenticator: Authenticator,
+class SessionRoutes(@Named(InjectionKeys.RFC_8621) val authenticator: Authenticator,
+                    val serializer: Serializer,
                     val sessionSupplier: SessionSupplier = new SessionSupplier()) extends JMAPRoutes {
 
   private val generateSession: JMAPRoute.Action =
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
index 8ea7002..410ad77 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
@@ -27,22 +27,31 @@ import eu.timepit.refined.auto._
 import io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE
 import io.netty.handler.codec.http.HttpMethod
 import io.netty.handler.codec.http.HttpResponseStatus.OK
-import javax.inject.Inject
+import javax.inject.{Inject, Named}
 import org.apache.http.HttpStatus.SC_BAD_REQUEST
 import org.apache.james.jmap.HttpConstants.JSON_CONTENT_TYPE
 import org.apache.james.jmap.JMAPUrls.JMAP
+import org.apache.james.jmap.exceptions.UnauthorizedException
+import org.apache.james.jmap.http.Authenticator
+import org.apache.james.jmap.http.rfc8621.InjectionKeys
 import org.apache.james.jmap.json.Serializer
 import org.apache.james.jmap.method.CoreEcho
 import org.apache.james.jmap.model.Invocation.{Arguments, MethodName}
 import org.apache.james.jmap.model.{Invocation, RequestObject, ResponseObject}
 import org.apache.james.jmap.{Endpoint, JMAPRoute, JMAPRoutes}
+import org.slf4j.{Logger, LoggerFactory}
 import play.api.libs.json.{JsError, JsSuccess, Json}
 import reactor.core.publisher.Mono
 import reactor.core.scala.publisher.{SFlux, SMono}
 import reactor.core.scheduler.Schedulers
 import reactor.netty.http.server.{HttpServerRequest, HttpServerResponse}
 
-class JMAPApiRoutes @Inject() (serializer: Serializer) extends JMAPRoutes {
+object JMAPApiRoutes {
+  val LOGGER: Logger = LoggerFactory.getLogger(classOf[JMAPApiRoutes])
+}
+
+class JMAPApiRoutes @Inject() (@Named(InjectionKeys.RFC_8621) val authenticator: Authenticator,
+                               val serializer: Serializer) extends JMAPRoutes {
   private val coreEcho = new CoreEcho
 
   override def routes(): stream.Stream[JMAPRoute] = Stream.of(
@@ -56,8 +65,9 @@ class JMAPApiRoutes @Inject() (serializer: Serializer) extends JMAPRoutes {
       .corsHeaders())
 
   private def post(httpServerRequest: HttpServerRequest, httpServerResponse: HttpServerResponse): Mono[Void] =
-    this.requestAsJsonStream(httpServerRequest)
-      .flatMap(requestObject => this.process(requestObject, httpServerResponse))
+    SMono(authenticator.authenticate(httpServerRequest))
+      .flatMap(_ => this.requestAsJsonStream(httpServerRequest)
+        .flatMap(requestObject => this.process(requestObject, httpServerResponse)))
       .onErrorResume(throwable => handleError(throwable, httpServerResponse))
       .subscribeOn(Schedulers.elastic)
       .asJava()
@@ -101,14 +111,12 @@ class JMAPApiRoutes @Inject() (serializer: Serializer) extends JMAPRoutes {
       invocation.methodCallId))
   }
 
-  private def handleError(throwable: Throwable, httpServerResponse: HttpServerResponse): SMono[Void] = {
-    if (throwable.isInstanceOf[IllegalArgumentException]) {
-      return SMono.fromPublisher(httpServerResponse.status(SC_BAD_REQUEST)
-        .header(CONTENT_TYPE, JSON_CONTENT_TYPE)
-        .sendString(SMono.fromCallable(() => throwable.getMessage), StandardCharsets.UTF_8)
-        .`then`())
-    }
-
-    SMono.fromPublisher(handleInternalError(httpServerResponse, throwable))
+  private def handleError(throwable: Throwable, httpServerResponse: HttpServerResponse): SMono[Void] = throwable match {
+    case exception: IllegalArgumentException => SMono.fromPublisher(httpServerResponse.status(SC_BAD_REQUEST)
+      .header(CONTENT_TYPE, JSON_CONTENT_TYPE)
+      .sendString(SMono.fromCallable(() => exception.getMessage), StandardCharsets.UTF_8)
+      .`then`())
+    case exception: UnauthorizedException => SMono(handleAuthenticationFailure(httpServerResponse, JMAPApiRoutes.LOGGER, exception))
+    case _ => SMono.fromPublisher(handleInternalError(httpServerResponse, throwable))
   }
 }
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/http/UserCredentialParserTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/http/UserCredentialParserTest.scala
new file mode 100644
index 0000000..8ac66c8
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/http/UserCredentialParserTest.scala
@@ -0,0 +1,135 @@
+/** **************************************************************
+ * 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 java.nio.charset.StandardCharsets
+import java.util.Base64
+
+import org.apache.james.core.Username
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+
+class UserCredentialParserTest {
+  @Test
+  def shouldReturnCredentialWhenUsernamePasswordToken(): Unit = {
+    val token: String = "Basic " + toBase64("user1:password")
+
+    assertThat(UserCredential.parseUserCredentials(token))
+      .isEqualTo(Some(UserCredential(Username.of("user1"), "password")))
+  }
+
+  @Test
+  def shouldAcceptPartSeparatorAsPartOfPassword(): Unit = {
+    val token: String = "Basic " + toBase64("user1:pass:word")
+
+    assertThat(UserCredential.parseUserCredentials(token))
+      .isEqualTo(Some(UserCredential(Username.of("user1"), "pass:word")))
+  }
+
+  @Test
+  def shouldReturnCredentialWhenRandomSpecialCharacterInUsernameToken(): Unit = {
+    val token: String = "Basic " + toBase64("fd2*#jk:password")
+
+    assertThat(UserCredential.parseUserCredentials(token))
+      .isEqualTo(Some(UserCredential(Username.of("fd2*#jk"), "password")))
+  }
+
+  @Test
+  def shouldReturnCredentialsWhenRandomSpecialCharacterInBothUsernamePasswoedToken(): Unit = {
+    val token: String = "Basic " + toBase64("fd2*#jk:password@fd23*&^$%")
+
+    assertThat(UserCredential.parseUserCredentials(token))
+      .isEqualTo(Some(UserCredential(Username.of("fd2*#jk"), "password@fd23*&^$%")))
+  }
+
+  @Test
+  def shouldReturnCredentialWhenUsernameDomainPasswordToken(): Unit = {
+    val token: String = "Basic " + toBase64("user1@domain.tld:password")
+
+    assertThat(UserCredential.parseUserCredentials(token))
+      .isEqualTo(Some(UserCredential(Username.of("user1@domain.tld"), "password")))
+  }
+
+  @Test
+  def shouldReturnCredentialWhenUsernameDomainNoPasswordToken(): Unit = {
+    val token: String = "Basic " + toBase64("user1@domain.tld:")
+
+    assertThat(UserCredential.parseUserCredentials(token))
+      .isEqualTo(Some(UserCredential(Username.of("user1@domain.tld"), "")))
+  }
+
+  @Test
+  def shouldReturnNoneWhenPayloadIsNotBase64(): Unit = {
+    val token: String = "Basic user1:password"
+
+    assertThat(UserCredential.parseUserCredentials(token))
+      .isEqualTo(None)
+  }
+
+  @Test
+  def shouldReturnNoneWhenEmptyToken(): Unit = {
+    assertThat(UserCredential.parseUserCredentials(""))
+      .isEqualTo(None)
+  }
+
+  @Test
+  def shouldReturnNoneWhenWrongFormatCredential(): Unit = {
+    val token: String = "Basic " + toBase64("user1@password")
+
+    assertThat(UserCredential.parseUserCredentials(token))
+       .isEqualTo(None)
+  }
+
+  @Test
+  def shouldReturnNoneWhenUpperCaseToken(): Unit = {
+    val token: String = "BASIC " + toBase64("user1@password")
+
+    assertThat(UserCredential.parseUserCredentials(token))
+      .isEqualTo(None)
+  }
+
+  @Test
+  def shouldReturnNoneWhenLowerCaseToken(): Unit = {
+    val token: String = "basic " + toBase64("user1:password")
+
+    assertThat(UserCredential.parseUserCredentials(token))
+      .isEqualTo(None)
+  }
+
+  @Test
+  def shouldReturnNoneWhenCredentialWithNoPassword(): Unit = {
+    val token: String = "Basic " + toBase64("user1:")
+
+    assertThat(UserCredential.parseUserCredentials(token))
+      .isEqualTo(Some(UserCredential(Username.of("user1"), "")))
+  }
+
+  @Test
+  def shouldReturnEmptyWhenCredentialWithNoUsername(): Unit = {
+    val token: String = "Basic " + toBase64(":pass")
+
+    assertThat(UserCredential.parseUserCredentials(token))
+      .isEqualTo(None)
+  }
+
+  private def toBase64(stringValue: String): String = {
+    Base64.getEncoder.encodeToString(stringValue.getBytes(StandardCharsets.UTF_8))
+  }
+}
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/JMAPApiRoutesTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/JMAPApiRoutesTest.scala
index 9bf7ba8..f23e835 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/JMAPApiRoutesTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/JMAPApiRoutesTest.scala
@@ -19,6 +19,7 @@
 package org.apache.james.jmap.routes
 
 import java.nio.charset.StandardCharsets
+import java.util.Base64
 
 import com.google.common.collect.ImmutableSet
 import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
@@ -26,28 +27,51 @@ import io.restassured.RestAssured
 import io.restassured.builder.RequestSpecBuilder
 import io.restassured.config.EncoderConfig.encoderConfig
 import io.restassured.config.RestAssuredConfig.newConfig
-import io.restassured.http.ContentType
+import io.restassured.http.{ContentType, Header, Headers}
 import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
 import org.apache.http.HttpStatus
+import org.apache.james.core.{Domain, Username}
+import org.apache.james.dnsservice.api.DNSService
+import org.apache.james.domainlist.memory.MemoryDomainList
 import org.apache.james.jmap.JMAPUrls.JMAP
-import org.apache.james.jmap.json.Serializer
 import org.apache.james.jmap._
+import org.apache.james.jmap.http.{Authenticator, BasicAuthenticationStrategy}
+import org.apache.james.jmap.json.Serializer
+import org.apache.james.jmap.routes.JMAPApiRoutesTest._
+import org.apache.james.mailbox.MailboxManager
+import org.apache.james.mailbox.extension.PreDeletionHook
+import org.apache.james.mailbox.inmemory.MemoryMailboxManagerProvider
 import org.apache.james.mailbox.model.TestId
+import org.apache.james.metrics.tests.RecordingMetricFactory
+import org.apache.james.user.memory.MemoryUsersRepository
+import org.mockito.Mockito.mock
 import org.scalatest.BeforeAndAfter
 import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.matchers.should.Matchers
 
-class JMAPApiRoutesTest extends AnyFlatSpec with BeforeAndAfter with Matchers {
+object JMAPApiRoutesTest {
   private val SERIALIZER: Serializer = new Serializer(new TestId.Factory)
-
   private val TEST_CONFIGURATION: JMAPConfiguration = JMAPConfiguration.builder().enable().randomPort().build()
   private val ACCEPT_JMAP_VERSION_HEADER = "application/json; jmapVersion="
   private val ACCEPT_DRAFT_VERSION_HEADER = ACCEPT_JMAP_VERSION_HEADER + Version.DRAFT.asString()
   private val ACCEPT_RFC8621_VERSION_HEADER = ACCEPT_JMAP_VERSION_HEADER + Version.RFC8621.asString()
 
-  private val JMAP_API_ROUTE: JMAPApiRoutes = new JMAPApiRoutes(SERIALIZER)
+  private val empty_set: ImmutableSet[PreDeletionHook] = ImmutableSet.of()
+  private val dnsService = mock(classOf[DNSService])
+  private val domainList = new MemoryDomainList(dnsService)
+  domainList.addDomain(Domain.of("james.org"))
+
+  private val usersRepository = MemoryUsersRepository.withoutVirtualHosting(domainList)
+  usersRepository.addUser(Username.of("user1"), "password")
+
+  private val mailboxManager: MailboxManager = MemoryMailboxManagerProvider.provideMailboxManager(empty_set)
+  private val authenticationStrategy: BasicAuthenticationStrategy = new BasicAuthenticationStrategy(usersRepository, mailboxManager)
+  private val AUTHENTICATOR: Authenticator = Authenticator.of(new RecordingMetricFactory, authenticationStrategy)
+  private val JMAP_API_ROUTE: JMAPApiRoutes = new JMAPApiRoutes(AUTHENTICATOR, SERIALIZER)
   private val ROUTES_HANDLER: ImmutableSet[JMAPRoutesHandler] = ImmutableSet.of(new JMAPRoutesHandler(Version.RFC8621, JMAP_API_ROUTE))
 
+  private val userBase64String: String = Base64.getEncoder.encodeToString("user1:password".getBytes(StandardCharsets.UTF_8))
+  
   private val REQUEST_OBJECT: String =
     """{
       |  "using": [
@@ -136,6 +160,9 @@ class JMAPApiRoutesTest extends AnyFlatSpec with BeforeAndAfter with Matchers {
       |    }
       |}
       |""".stripMargin
+}
+
+class JMAPApiRoutesTest extends AnyFlatSpec with BeforeAndAfter with Matchers {
 
   var jmapServer: JMAPServer = _
 
@@ -158,88 +185,121 @@ class JMAPApiRoutesTest extends AnyFlatSpec with BeforeAndAfter with Matchers {
   }
 
   "RFC-8621 version, GET" should "not supported and return 404 status" in {
+    val headers: Headers = Headers.headers(
+      new Header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER),
+      new Header("Authorization", s"Basic ${userBase64String}")
+    )
+
     RestAssured
       .`given`()
-      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+        .headers(headers)
       .when()
-      .get
+        .get
       .then
-      .statusCode(HttpStatus.SC_NOT_FOUND)
+        .statusCode(HttpStatus.SC_NOT_FOUND)
   }
 
   "RFC-8621 version, POST, without body" should "return 200 status" in {
+    val headers: Headers = Headers.headers(
+      new Header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER),
+      new Header("Authorization", s"Basic ${userBase64String}")
+    )
+
     RestAssured
       .`given`()
-      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+        .headers(headers)
       .when()
-      .post
+        .post
       .then
-      .statusCode(HttpStatus.SC_OK)
+        .statusCode(HttpStatus.SC_OK)
   }
 
   "RFC-8621 version, POST, methods include supported" should "return OK status" in {
+    val headers: Headers = Headers.headers(
+      new Header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER),
+      new Header("Authorization", s"Basic ${userBase64String}")
+    )
+
     val response = RestAssured
       .`given`()
-      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
-      .body(REQUEST_OBJECT)
+        .headers(headers)
+        .body(REQUEST_OBJECT)
       .when()
-      .post()
+        .post()
       .then
-      .statusCode(HttpStatus.SC_OK)
-      .contentType(ContentType.JSON)
+        .statusCode(HttpStatus.SC_OK)
+        .contentType(ContentType.JSON)
       .extract()
-      .body()
-      .asString()
+        .body()
+        .asString()
 
     assertThatJson(response).isEqualTo(RESPONSE_OBJECT)
   }
 
   "RFC-8621 version, POST, with methods" should "return OK status, ResponseObject depend on method" in {
 
+    val headers: Headers = Headers.headers(
+      new Header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER),
+      new Header("Authorization", s"Basic ${userBase64String}")
+    )
+
     val response = RestAssured
       .`given`()
-      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
-      .body(REQUEST_OBJECT_WITH_UNSUPPORTED_METHOD)
+        .headers(headers)
+        .body(REQUEST_OBJECT_WITH_UNSUPPORTED_METHOD)
       .when()
-      .post()
+        .post()
       .then
-      .statusCode(HttpStatus.SC_OK)
-      .contentType(ContentType.JSON)
+        .statusCode(HttpStatus.SC_OK)
+        .contentType(ContentType.JSON)
       .extract()
-      .body()
-      .asString()
+        .body()
+        .asString()
 
     assertThatJson(response).isEqualTo(RESPONSE_OBJECT_WITH_UNSUPPORTED_METHOD)
   }
 
   "Draft version, GET" should "return 404 status" in {
+    val headers: Headers = Headers.headers(
+      new Header(ACCEPT.toString, ACCEPT_DRAFT_VERSION_HEADER),
+      new Header("Authorization", s"Basic ${userBase64String}")
+    )
+
     RestAssured
       .`given`()
-      .header(ACCEPT.toString, ACCEPT_DRAFT_VERSION_HEADER)
+        .headers(headers)
       .when()
-      .get
+        .get
       .then
-      .statusCode(HttpStatus.SC_NOT_FOUND)
+        .statusCode(HttpStatus.SC_NOT_FOUND)
   }
 
   "Draft version, POST, without body" should "return 400 status" in {
+    val headers: Headers = Headers.headers(
+      new Header(ACCEPT.toString, ACCEPT_DRAFT_VERSION_HEADER),
+      new Header("Authorization", s"Basic ${userBase64String}")
+    )
     RestAssured
       .`given`()
-      .header(ACCEPT.toString, ACCEPT_DRAFT_VERSION_HEADER)
+        .headers(headers)
       .when()
-      .post
+        .post
       .then
-      .statusCode(HttpStatus.SC_NOT_FOUND)
+        .statusCode(HttpStatus.SC_NOT_FOUND)
   }
 
   "RFC-8621 version, POST, with wrong requestObject body" should "return 400 status" in {
+    val headers: Headers = Headers.headers(
+      new Header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER),
+      new Header("Authorization", s"Basic ${userBase64String}")
+    )
     RestAssured
       .`given`()
-      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
-      .body(WRONG_OBJECT_REQUEST)
+        .headers(headers)
+        .body(WRONG_OBJECT_REQUEST)
       .when()
-      .post
+        .post
       .then
-      .statusCode(HttpStatus.SC_BAD_REQUEST)
+        .statusCode(HttpStatus.SC_BAD_REQUEST)
   }
 }


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