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]
