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 14295d5a5e2b72449b81847465e04ed3fed7bd60
Author: Benoit Tellier <[email protected]>
AuthorDate: Wed Mar 24 15:55:23 2021 +0700

    JAMES-3522 JMAP routes should position WWW-Authenticate
    
    This enables authentication mechanisms discovery
---
 .../http/AccessTokenAuthenticationStrategy.java    |  8 +++
 ...ParameterAccessTokenAuthenticationStrategy.java |  8 +++
 .../apache/james/jmap/http/AuthenticatorTest.java  | 22 ++++++-
 .../rfc8621/contract/AuthenticationContract.scala  |  1 +
 .../jmap/rfc8621/contract/DownloadContract.scala   |  1 +
 .../jmap/rfc8621/contract/UploadContract.scala     |  1 +
 .../jmap/http/BasicAuthenticationStrategy.scala    |  9 ++-
 .../apache/james/jmap/routes/DownloadRoutes.scala  |  2 +-
 .../james/jmap/routes/EventSourceRoutes.scala      |  7 ++-
 .../apache/james/jmap/routes/JMAPApiRoutes.scala   |  7 ++-
 .../apache/james/jmap/routes/SessionRoutes.scala   |  2 +-
 .../apache/james/jmap/routes/UploadRoutes.scala    |  4 +-
 .../apache/james/jmap/routes/WebSocketRoutes.scala |  7 ++-
 ....java => NoAuthorizationSuppliedException.java} | 17 ++++--
 .../jmap/exceptions/UnauthorizedException.java     |  6 ++
 ...cationStrategy.java => AuthenticateHeader.java} | 28 +++++----
 .../james/jmap/http/AuthenticationChallenge.java   | 68 ++++++++++++++++++++++
 ...tionStrategy.java => AuthenticationScheme.java} | 38 ++++++++----
 .../james/jmap/http/AuthenticationStrategy.java    |  2 +
 .../org/apache/james/jmap/http/Authenticator.java  | 12 +++-
 .../james/jmap/jwt/JWTAuthenticationStrategy.java  | 10 ++++
 21 files changed, 217 insertions(+), 43 deletions(-)

diff --git 
a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/AccessTokenAuthenticationStrategy.java
 
b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/AccessTokenAuthenticationStrategy.java
index ab351cf..c9ca575 100644
--- 
a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/AccessTokenAuthenticationStrategy.java
+++ 
b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/AccessTokenAuthenticationStrategy.java
@@ -29,6 +29,7 @@ import org.apache.james.mailbox.MailboxManager;
 import org.apache.james.mailbox.MailboxSession;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableMap;
 
 import reactor.core.publisher.Mono;
 import reactor.netty.http.server.HttpServerRequest;
@@ -54,4 +55,11 @@ public class AccessTokenAuthenticationStrategy implements 
AuthenticationStrategy
             .onErrorResume(InvalidAccessToken.class, error -> Mono.error(new 
UnauthorizedException("Invalid access token", error)))
             .onErrorResume(NotAnAccessTokenException.class, error -> 
Mono.error(new UnauthorizedException("Not an access token", error)));
     }
+
+    @Override
+    public AuthenticationChallenge correspondingChallenge() {
+        return AuthenticationChallenge.of(
+            AuthenticationScheme.of("Bearer"),
+            ImmutableMap.of("realm", "JMAP Draft access token"));
+    }
 }
diff --git 
a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/QueryParameterAccessTokenAuthenticationStrategy.java
 
b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/QueryParameterAccessTokenAuthenticationStrategy.java
index 7381b55..1dab2ea 100644
--- 
a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/QueryParameterAccessTokenAuthenticationStrategy.java
+++ 
b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/QueryParameterAccessTokenAuthenticationStrategy.java
@@ -32,6 +32,7 @@ import org.apache.james.mailbox.MailboxManager;
 import org.apache.james.mailbox.MailboxSession;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableMap;
 
 import io.netty.handler.codec.http.QueryStringDecoder;
 import reactor.core.publisher.Mono;
@@ -59,6 +60,13 @@ public class QueryParameterAccessTokenAuthenticationStrategy 
implements Authenti
             .map(mailboxManager::createSystemSession);
     }
 
+    @Override
+    public AuthenticationChallenge correspondingChallenge() {
+        return AuthenticationChallenge.of(
+            AuthenticationScheme.of("QueryParameterBearer"),
+            ImmutableMap.of("realm", "JMAP Draft access token over Query 
parameter"));
+    }
+
     private Optional<AttachmentAccessToken> getAccessToken(HttpServerRequest 
httpRequest) {
         try {
 
diff --git 
a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/http/AuthenticatorTest.java
 
b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/http/AuthenticatorTest.java
index 66b5cb4..b87f7b3 100644
--- 
a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/http/AuthenticatorTest.java
+++ 
b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/http/AuthenticatorTest.java
@@ -26,6 +26,7 @@ import static org.mockito.Mockito.when;
 
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Function;
 
 import org.apache.james.core.Username;
 import org.apache.james.jmap.api.access.AccessToken;
@@ -38,6 +39,7 @@ import org.junit.Before;
 import org.junit.Test;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 
 import io.netty.handler.codec.http.HttpHeaders;
 import io.netty.handler.codec.http.HttpMethod;
@@ -49,8 +51,22 @@ public class AuthenticatorTest {
     private static final String AUTHORIZATION_HEADERS = "Authorization";
     private static final Username USERNAME = Username.of("[email protected]");
 
-    private static final AuthenticationStrategy DENY = httpRequest -> 
Mono.error(new UnauthorizedException(null));
-    private static final AuthenticationStrategy ALLOW = httpRequest -> 
Mono.just(mock(MailboxSession.class));
+    public static AuthenticationStrategy 
asAuthStrategy(Function<HttpServerRequest, Mono<MailboxSession>> auth) {
+        return new AuthenticationStrategy() {
+            @Override
+            public Mono<MailboxSession> createMailboxSession(HttpServerRequest 
httpRequest) {
+                return auth.apply(httpRequest);
+            }
+
+            @Override
+            public AuthenticationChallenge correspondingChallenge() {
+                return 
AuthenticationChallenge.of(AuthenticationScheme.of("Testing"), 
ImmutableMap.of());
+            }
+        };
+    }
+
+    private static final AuthenticationStrategy DENY = 
asAuthStrategy(httpRequest -> Mono.error(new UnauthorizedException(null)));
+    private static final AuthenticationStrategy ALLOW = 
asAuthStrategy(httpRequest -> Mono.just(mock(MailboxSession.class)));
 
     private HttpServerRequest mockedRequest;
     private HttpHeaders mockedHeaders;
@@ -96,7 +112,7 @@ public class AuthenticatorTest {
 
         testee = Authenticator.of(new RecordingMetricFactory(),
             ALLOW,
-            req -> Mono.fromRunnable(() -> called.set(true)));
+            asAuthStrategy(req -> Mono.fromRunnable(() -> called.set(true))));
         assertThat(called.get()).isFalse();
 
         testee.authenticate(mockedRequest).block();
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
index 5cb8956..caeeeb1 100644
--- 
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
@@ -78,6 +78,7 @@ trait AuthenticationContract {
         .post
       .`then`
         .statusCode(SC_UNAUTHORIZED)
+        .header("WWW-Authenticate", "Basic realm=\"simple\", Bearer 
realm=\"JWT\"")
         .body("status", equalTo(401))
         .body("type", equalTo("about:blank"))
         .body("detail", equalTo("No valid authentication methods provided"))
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/DownloadContract.scala
 
b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/DownloadContract.scala
index 1c8c401..cd03309 100644
--- 
a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/DownloadContract.scala
+++ 
b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/DownloadContract.scala
@@ -104,6 +104,7 @@ trait DownloadContract {
       .get(s"/download/$accountId/${messageId.serialize()}")
     .`then`
       .statusCode(SC_UNAUTHORIZED)
+      .header("WWW-Authenticate", "Basic realm=\"simple\", Bearer 
realm=\"JWT\"")
       .body("status", equalTo(401))
       .body("type", equalTo("about:blank"))
       .body("detail", equalTo("No valid authentication methods provided"))
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/UploadContract.scala
 
b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/UploadContract.scala
index e200e04..e5d3715 100644
--- 
a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/UploadContract.scala
+++ 
b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/UploadContract.scala
@@ -159,6 +159,7 @@ trait UploadContract {
       .post(s"/upload/$ACCOUNT_ID/")
     .`then`
       .statusCode(SC_UNAUTHORIZED)
+      .header("WWW-Authenticate", "Basic realm=\"simple\", Bearer 
realm=\"JWT\"")
       .body("status", equalTo(401))
       .body("type", equalTo("about:blank"))
       .body("detail", equalTo("No valid authentication methods provided"))
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
index 16a3a86..9ba7198 100644
--- 
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
@@ -38,6 +38,8 @@ import reactor.netty.http.server.HttpServerRequest
 
 import scala.util.{Failure, Success, Try}
 
+import scala.jdk.CollectionConverters._
+
 object UserCredential {
   type BasicAuthenticationHeaderValue = String Refined MatchesRegex["Basic 
[\\d\\w=]++"]
   type CredentialsAsString = String Refined MatchesRegex[".*:.*"]
@@ -88,7 +90,7 @@ 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] = {
+  override def createMailboxSession(httpRequest: HttpServerRequest): 
Mono[MailboxSession] =
     SMono.fromCallable(() => authHeaders(httpRequest))
       .map(parseUserCredentials)
       .handle(publishNext)
@@ -96,7 +98,10 @@ class BasicAuthenticationStrategy @Inject()(val 
usersRepository: UsersRepository
       .map(_.username)
       .map(mailboxManager.createSystemSession)
       .asJava()
-  }
+
+
+  override def correspondingChallenge(): AuthenticationChallenge = 
AuthenticationChallenge.of(
+    AuthenticationScheme.of("Basic"), Map("realm" -> "simple").asJava)
 
   private def publishNext[T]: (Option[T], 
reactor.core.publisher.SynchronousSink[T]) => Unit =
     (maybeT, sink) => maybeT.foreach(t => sink.next(t))
diff --git 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/DownloadRoutes.scala
 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/DownloadRoutes.scala
index 7f817a2..841f3e2 100644
--- 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/DownloadRoutes.scala
+++ 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/DownloadRoutes.scala
@@ -225,7 +225,7 @@ class DownloadRoutes 
@Inject()(@Named(InjectionKeys.RFC_8621) val authenticator:
             FORBIDDEN)
         case e: UnauthorizedException =>
           LOGGER.warn("Unauthorized", e)
-          respondDetails(response,
+          respondDetails(e.addHeaders(response),
             ProblemDetails(status = UNAUTHORIZED, detail = e.getMessage),
             UNAUTHORIZED)
         case _: BlobNotFoundException =>
diff --git 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/EventSourceRoutes.scala
 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/EventSourceRoutes.scala
index b5e0723..16a8797 100644
--- 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/EventSourceRoutes.scala
+++ 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/EventSourceRoutes.scala
@@ -35,6 +35,7 @@ import org.apache.james.jmap.HttpConstants.JSON_CONTENT_TYPE
 import org.apache.james.jmap.JMAPUrls.EVENT_SOURCE
 import org.apache.james.jmap.change.{AccountIdRegistrationKey, 
StateChangeListener, TypeName}
 import org.apache.james.jmap.core.{OutboundMessage, PingMessage, 
ProblemDetails, StateChange}
+import org.apache.james.jmap.exceptions.UnauthorizedException
 import org.apache.james.jmap.http.rfc8621.InjectionKeys
 import org.apache.james.jmap.http.{Authenticator, UserProvisioning}
 import org.apache.james.jmap.json.ResponseSerializer
@@ -215,8 +216,10 @@ class EventSourceRoutes@Inject() 
(@Named(InjectionKeys.RFC_8621) val authenticat
     s"event: $event\ndata: 
${Json.stringify(ResponseSerializer.serialize(outboundMessage))}\n\n"
   }
 
-  private def handleConnectionEstablishmentError(throwable: Throwable, 
response: HttpServerResponse): SMono[Void] =
-    respondDetails(response, ProblemDetails.forThrowable(throwable))
+  private def handleConnectionEstablishmentError(throwable: Throwable, 
response: HttpServerResponse): SMono[Void] = throwable match {
+    case e: UnauthorizedException => respondDetails(e.addHeaders(response), 
ProblemDetails.forThrowable(throwable))
+    case _ => respondDetails(response, ProblemDetails.forThrowable(throwable))
+  }
 
   private def respondDetails(response: HttpServerResponse, details: 
ProblemDetails): SMono[Void] =
     SMono.fromPublisher(response.status(details.status)
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 a09b4cc..6dc68b2 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
@@ -31,6 +31,7 @@ import org.apache.james.jmap.HttpConstants.JSON_CONTENT_TYPE
 import org.apache.james.jmap.JMAPUrls.JMAP
 import org.apache.james.jmap.core.CapabilityIdentifier.CapabilityIdentifier
 import org.apache.james.jmap.core.{ProblemDetails, RequestObject}
+import org.apache.james.jmap.exceptions.UnauthorizedException
 import org.apache.james.jmap.http.rfc8621.InjectionKeys
 import org.apache.james.jmap.http.{Authenticator, UserProvisioning}
 import org.apache.james.jmap.json.ResponseSerializer
@@ -100,8 +101,10 @@ class JMAPApiRoutes @Inject() 
(@Named(InjectionKeys.RFC_8621) val authenticator:
           StandardCharsets.UTF_8)
         .`then`()))
 
-  private def handleError(throwable: Throwable, response: HttpServerResponse): 
SMono[Void] =
-    respondDetails(response, ProblemDetails.forThrowable(throwable))
+  private def handleError(throwable: Throwable, response: HttpServerResponse): 
SMono[Void] = throwable match {
+    case e: UnauthorizedException => respondDetails(e.addHeaders(response), 
ProblemDetails.forThrowable(throwable))
+    case _ => respondDetails(response, ProblemDetails.forThrowable(throwable))
+  }
 
   private def respondDetails(httpServerResponse: HttpServerResponse, details: 
ProblemDetails): SMono[Void] =
     SMono.fromPublisher(httpServerResponse.status(details.status)
diff --git 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionRoutes.scala
 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionRoutes.scala
index e794665..78e950a 100644
--- 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionRoutes.scala
+++ 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionRoutes.scala
@@ -94,7 +94,7 @@ class SessionRoutes @Inject() (@Named(InjectionKeys.RFC_8621) 
val authenticator:
     throwable match {
       case e: UnauthorizedException =>
         LOGGER.warn("Unauthorized", e)
-        respondDetails(response,
+        respondDetails(e.addHeaders(response),
           ProblemDetails(status = UNAUTHORIZED, detail = e.getMessage),
           UNAUTHORIZED)
       case e =>
diff --git 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/UploadRoutes.scala
 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/UploadRoutes.scala
index 5617079..b06fdda 100644
--- 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/UploadRoutes.scala
+++ 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/UploadRoutes.scala
@@ -100,12 +100,12 @@ class UploadRoutes 
@Inject()(@Named(InjectionKeys.RFC_8621) val authenticator: A
   def post(request: HttpServerRequest, response: HttpServerResponse): 
Mono[Void] = {
     request.requestHeaders.get(CONTENT_TYPE) match {
       case contentType => SMono.fromPublisher(
-        authenticator.authenticate(request))
+          authenticator.authenticate(request))
         .flatMap(session => post(request, response, 
ContentType.of(contentType), session))
         .onErrorResume {
           case e: UnauthorizedException =>
             LOGGER.warn("Unauthorized", e)
-            respondDetails(response,
+            respondDetails(e.addHeaders(response),
               ProblemDetails(status = UNAUTHORIZED, detail = e.getMessage),
               UNAUTHORIZED)
           case _: TooBigUploadException =>
diff --git 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/WebSocketRoutes.scala
 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/WebSocketRoutes.scala
index 360e1bc..c308386 100644
--- 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/WebSocketRoutes.scala
+++ 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/WebSocketRoutes.scala
@@ -35,6 +35,7 @@ import 
org.apache.james.jmap.api.change.{EmailChangeRepository, MailboxChangeRep
 import org.apache.james.jmap.api.model.{AccountId => JavaAccountId}
 import org.apache.james.jmap.change.{AccountIdRegistrationKey, 
StateChangeListener, TypeName, _}
 import org.apache.james.jmap.core.{OutboundMessage, ProblemDetails, RequestId, 
WebSocketError, WebSocketPushDisable, WebSocketPushEnable, WebSocketRequest, 
WebSocketResponse, _}
+import org.apache.james.jmap.exceptions.UnauthorizedException
 import org.apache.james.jmap.http.rfc8621.InjectionKeys
 import org.apache.james.jmap.http.{Authenticator, UserProvisioning}
 import org.apache.james.jmap.json.ResponseSerializer
@@ -162,8 +163,10 @@ class WebSocketRoutes @Inject() 
(@Named(InjectionKeys.RFC_8621) val authenticato
       })
   }
 
-  private def handleHttpHandshakeError(throwable: Throwable, response: 
HttpServerResponse): SMono[Void] =
-    respondDetails(response, ProblemDetails.forThrowable(throwable))
+  private def handleHttpHandshakeError(throwable: Throwable, response: 
HttpServerResponse): SMono[Void] = throwable match {
+    case e: UnauthorizedException => respondDetails(e.addHeaders(response), 
ProblemDetails.forThrowable(throwable))
+    case _ => respondDetails(response, ProblemDetails.forThrowable(throwable))
+  }
 
   private def asError(requestId: Option[RequestId])(throwable: Throwable): 
WebSocketError =
     WebSocketError(requestId, ProblemDetails.forThrowable(throwable))
diff --git 
a/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/UnauthorizedException.java
 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/NoAuthorizationSuppliedException.java
similarity index 66%
copy from 
server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/UnauthorizedException.java
copy to 
server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/NoAuthorizationSuppliedException.java
index b33853f..7137872 100644
--- 
a/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/UnauthorizedException.java
+++ 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/NoAuthorizationSuppliedException.java
@@ -19,13 +19,20 @@
 
 package org.apache.james.jmap.exceptions;
 
-public class UnauthorizedException extends RuntimeException {
+import org.apache.james.jmap.http.AuthenticateHeader;
 
-    public UnauthorizedException(String message) {
-        super(message);
+import reactor.netty.http.server.HttpServerResponse;
+
+public class NoAuthorizationSuppliedException extends UnauthorizedException {
+    private final AuthenticateHeader authenticateHeader;
+
+    public NoAuthorizationSuppliedException(AuthenticateHeader 
authenticateHeader) {
+        super("No valid authentication methods provided");
+        this.authenticateHeader = authenticateHeader;
     }
 
-    public UnauthorizedException(String message, Throwable throwable) {
-        super(message, throwable);
+    @Override
+    public HttpServerResponse addHeaders(HttpServerResponse response) {
+        return response.addHeader(AuthenticateHeader.HEADER_NAME, 
authenticateHeader.asHeaderValue());
     }
 }
diff --git 
a/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/UnauthorizedException.java
 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/UnauthorizedException.java
index b33853f..dba30f8 100644
--- 
a/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/UnauthorizedException.java
+++ 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/UnauthorizedException.java
@@ -19,6 +19,8 @@
 
 package org.apache.james.jmap.exceptions;
 
+import reactor.netty.http.server.HttpServerResponse;
+
 public class UnauthorizedException extends RuntimeException {
 
     public UnauthorizedException(String message) {
@@ -28,4 +30,8 @@ public class UnauthorizedException extends RuntimeException {
     public UnauthorizedException(String message, Throwable throwable) {
         super(message, throwable);
     }
+
+    public HttpServerResponse addHeaders(HttpServerResponse response) {
+        return response;
+    }
 }
diff --git 
a/server/protocols/jmap/src/main/java/org/apache/james/jmap/http/AuthenticationStrategy.java
 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/http/AuthenticateHeader.java
similarity index 62%
copy from 
server/protocols/jmap/src/main/java/org/apache/james/jmap/http/AuthenticationStrategy.java
copy to 
server/protocols/jmap/src/main/java/org/apache/james/jmap/http/AuthenticateHeader.java
index 5367a9b..6c338fa 100644
--- 
a/server/protocols/jmap/src/main/java/org/apache/james/jmap/http/AuthenticationStrategy.java
+++ 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/http/AuthenticateHeader.java
@@ -16,24 +16,30 @@
  * specific language governing permissions and limitations      *
  * under the License.                                           *
  ****************************************************************/
+
 package org.apache.james.jmap.http;
 
-import org.apache.james.mailbox.MailboxSession;
+import java.util.Collection;
+import java.util.stream.Collectors;
 
-import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
 
-import reactor.core.publisher.Mono;
-import reactor.netty.http.server.HttpServerRequest;
+public class AuthenticateHeader {
+    public static final String HEADER_NAME = "WWW-Authenticate";
 
-public interface AuthenticationStrategy {
-    Mono<MailboxSession> createMailboxSession(HttpServerRequest httpRequest);
+    public static AuthenticateHeader of(Collection<AuthenticationChallenge> 
challenges) {
+        return new AuthenticateHeader(ImmutableList.copyOf(challenges));
+    }
 
-    String AUTHORIZATION_HEADERS = "Authorization";
+    private final ImmutableList<AuthenticationChallenge> challenges;
 
-    default String authHeaders(HttpServerRequest httpRequest) {
-        Preconditions.checkArgument(httpRequest != null, "'httpRequest' is 
mandatory");
-        
Preconditions.checkArgument(httpRequest.requestHeaders().getAll(AUTHORIZATION_HEADERS).size()
 <= 1, "Only one set of credential is allowed");
+    private AuthenticateHeader(ImmutableList<AuthenticationChallenge> 
challenges) {
+        this.challenges = challenges;
+    }
 
-        return httpRequest.requestHeaders().get(AUTHORIZATION_HEADERS);
+    public String asHeaderValue() {
+        return challenges.stream()
+            .map(AuthenticationChallenge::asString)
+            .collect(Collectors.joining(", "));
     }
 }
diff --git 
a/server/protocols/jmap/src/main/java/org/apache/james/jmap/http/AuthenticationChallenge.java
 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/http/AuthenticationChallenge.java
new file mode 100644
index 0000000..e979c57
--- /dev/null
+++ 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/http/AuthenticationChallenge.java
@@ -0,0 +1,68 @@
+/****************************************************************
+ * 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.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+
+public class AuthenticationChallenge {
+    public static AuthenticationChallenge of(AuthenticationScheme scheme, 
Map<String, String> parameters) {
+        Preconditions.checkNotNull(scheme);
+        Preconditions.checkNotNull(parameters);
+
+        return new AuthenticationChallenge(scheme, 
ImmutableMap.copyOf(parameters));
+    }
+
+    private final AuthenticationScheme scheme;
+    private final ImmutableMap<String, String> parameters;
+
+    public AuthenticationChallenge(AuthenticationScheme scheme, 
ImmutableMap<String, String> parameters) {
+        this.scheme = scheme;
+        this.parameters = parameters;
+    }
+
+    public String asString() {
+        return String.format("%s %s",
+            scheme.asString(),
+            parameters.entrySet().stream()
+                .map(entry -> String.format("%s=\"%s\"", entry.getKey(), 
entry.getValue()))
+                .collect(Collectors.joining(", ")));
+    }
+
+    @Override
+    public final boolean equals(Object o) {
+        if (o instanceof AuthenticationChallenge) {
+            AuthenticationChallenge that = (AuthenticationChallenge) o;
+
+            return Objects.equals(this.scheme, that.scheme)
+                && Objects.equals(this.parameters, that.parameters);
+        }
+        return false;
+    }
+
+    @Override
+    public final int hashCode() {
+        return Objects.hash(scheme, parameters);
+    }
+}
diff --git 
a/server/protocols/jmap/src/main/java/org/apache/james/jmap/http/AuthenticationStrategy.java
 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/http/AuthenticationScheme.java
similarity index 63%
copy from 
server/protocols/jmap/src/main/java/org/apache/james/jmap/http/AuthenticationStrategy.java
copy to 
server/protocols/jmap/src/main/java/org/apache/james/jmap/http/AuthenticationScheme.java
index 5367a9b..18f3af8 100644
--- 
a/server/protocols/jmap/src/main/java/org/apache/james/jmap/http/AuthenticationStrategy.java
+++ 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/http/AuthenticationScheme.java
@@ -16,24 +16,42 @@
  * specific language governing permissions and limitations      *
  * under the License.                                           *
  ****************************************************************/
+
 package org.apache.james.jmap.http;
 
-import org.apache.james.mailbox.MailboxSession;
+import java.util.Objects;
 
 import com.google.common.base.Preconditions;
 
-import reactor.core.publisher.Mono;
-import reactor.netty.http.server.HttpServerRequest;
+public class AuthenticationScheme {
+    public static AuthenticationScheme of(String value) {
+        Preconditions.checkNotNull(value);
+
+        return new AuthenticationScheme(value);
+    }
+
+    private final String value;
 
-public interface AuthenticationStrategy {
-    Mono<MailboxSession> createMailboxSession(HttpServerRequest httpRequest);
+    private AuthenticationScheme(String value) {
+        this.value = value;
+    }
+
+    public String asString() {
+        return value;
+    }
 
-    String AUTHORIZATION_HEADERS = "Authorization";
+    @Override
+    public final boolean equals(Object o) {
+        if (o instanceof AuthenticationScheme) {
+            AuthenticationScheme that = (AuthenticationScheme) o;
 
-    default String authHeaders(HttpServerRequest httpRequest) {
-        Preconditions.checkArgument(httpRequest != null, "'httpRequest' is 
mandatory");
-        
Preconditions.checkArgument(httpRequest.requestHeaders().getAll(AUTHORIZATION_HEADERS).size()
 <= 1, "Only one set of credential is allowed");
+            return Objects.equals(this.value, that.value);
+        }
+        return false;
+    }
 
-        return httpRequest.requestHeaders().get(AUTHORIZATION_HEADERS);
+    @Override
+    public final int hashCode() {
+        return Objects.hash(value);
     }
 }
diff --git 
a/server/protocols/jmap/src/main/java/org/apache/james/jmap/http/AuthenticationStrategy.java
 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/http/AuthenticationStrategy.java
index 5367a9b..7f052a7 100644
--- 
a/server/protocols/jmap/src/main/java/org/apache/james/jmap/http/AuthenticationStrategy.java
+++ 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/http/AuthenticationStrategy.java
@@ -28,6 +28,8 @@ import reactor.netty.http.server.HttpServerRequest;
 public interface AuthenticationStrategy {
     Mono<MailboxSession> createMailboxSession(HttpServerRequest httpRequest);
 
+    AuthenticationChallenge correspondingChallenge();
+
     String AUTHORIZATION_HEADERS = "Authorization";
 
     default String authHeaders(HttpServerRequest httpRequest) {
diff --git 
a/server/protocols/jmap/src/main/java/org/apache/james/jmap/http/Authenticator.java
 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/http/Authenticator.java
index b0782f5..cfd1bcb 100644
--- 
a/server/protocols/jmap/src/main/java/org/apache/james/jmap/http/Authenticator.java
+++ 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/http/Authenticator.java
@@ -21,10 +21,11 @@ package org.apache.james.jmap.http;
 import java.util.Collection;
 import java.util.List;
 
-import org.apache.james.jmap.exceptions.UnauthorizedException;
+import org.apache.james.jmap.exceptions.NoAuthorizationSuppliedException;
 import org.apache.james.mailbox.MailboxSession;
 import org.apache.james.metrics.api.MetricFactory;
 
+import com.github.steveash.guavate.Guavate;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
 
@@ -55,6 +56,13 @@ public class Authenticator {
             Flux.fromIterable(authMethods)
                 .concatMap(auth -> auth.createMailboxSession(request))
                 .next()
-                .switchIfEmpty(Mono.error(new UnauthorizedException("No valid 
authentication methods provided")))));
+                .switchIfEmpty(Mono.error(noAuthSupplied()))));
+    }
+
+    private NoAuthorizationSuppliedException noAuthSupplied() {
+        return new NoAuthorizationSuppliedException(AuthenticateHeader.of(
+            authMethods.stream()
+                .map(AuthenticationStrategy::correspondingChallenge)
+                .collect(Guavate.toImmutableList())));
     }
 }
diff --git 
a/server/protocols/jmap/src/main/java/org/apache/james/jmap/jwt/JWTAuthenticationStrategy.java
 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/jwt/JWTAuthenticationStrategy.java
index 2dc0c8c..4314c31 100644
--- 
a/server/protocols/jmap/src/main/java/org/apache/james/jmap/jwt/JWTAuthenticationStrategy.java
+++ 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/jwt/JWTAuthenticationStrategy.java
@@ -23,6 +23,8 @@ import javax.inject.Named;
 
 import org.apache.james.core.Username;
 import org.apache.james.jmap.exceptions.UnauthorizedException;
+import org.apache.james.jmap.http.AuthenticationChallenge;
+import org.apache.james.jmap.http.AuthenticationScheme;
 import org.apache.james.jmap.http.AuthenticationStrategy;
 import org.apache.james.jwt.JwtTokenVerifier;
 import org.apache.james.mailbox.MailboxManager;
@@ -31,6 +33,7 @@ import org.apache.james.user.api.UsersRepository;
 import org.apache.james.user.api.UsersRepositoryException;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableMap;
 
 import reactor.core.publisher.Mono;
 import reactor.netty.http.server.HttpServerRequest;
@@ -74,4 +77,11 @@ public class JWTAuthenticationStrategy implements 
AuthenticationStrategy {
             })
             .map(mailboxManager::createSystemSession);
     }
+
+    @Override
+    public AuthenticationChallenge correspondingChallenge() {
+        return AuthenticationChallenge.of(
+            AuthenticationScheme.of("Bearer"),
+            ImmutableMap.of("realm", "JWT"));
+    }
 }

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to