Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.apache.james.jmap.http.AuthenticationRoutes Maven / Gradle / Ivy
/****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one *
* or more contributor license agreements. See the NOTICE file *
* distributed with this work for additional information *
* regarding copyright ownership. The ASF licenses this file *
* to you under the Apache License, Version 2.0 (the *
* "License"); you may not use this file except in compliance *
* with the License. You may obtain a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, *
* software distributed under the License is distributed on an *
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
* KIND, either express or implied. See the License for the *
* specific language governing permissions and limitations *
* under the License. *
****************************************************************/
package org.apache.james.jmap.http;
import static io.netty.handler.codec.http.HttpHeaderNames.ACCEPT;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;
import static io.netty.handler.codec.http.HttpResponseStatus.CREATED;
import static io.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN;
import static io.netty.handler.codec.http.HttpResponseStatus.NO_CONTENT;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
import static io.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED;
import static org.apache.james.jmap.HttpConstants.JSON_CONTENT_TYPE;
import static org.apache.james.jmap.HttpConstants.JSON_CONTENT_TYPE_UTF8;
import static org.apache.james.jmap.http.JMAPUrls.AUTHENTICATION;
import static org.apache.james.jmap.http.LoggingHelper.jmapAction;
import static org.apache.james.jmap.http.LoggingHelper.jmapAuthContext;
import static org.apache.james.jmap.http.LoggingHelper.jmapContext;
import static org.apache.james.util.ReactorUtils.log;
import static org.apache.james.util.ReactorUtils.logOnError;
import java.io.IOException;
import java.util.Objects;
import javax.inject.Inject;
import org.apache.james.core.Username;
import org.apache.james.jmap.JMAPRoutes;
import org.apache.james.jmap.api.access.AccessToken;
import org.apache.james.jmap.draft.api.AccessTokenManager;
import org.apache.james.jmap.draft.api.SimpleTokenFactory;
import org.apache.james.jmap.draft.api.SimpleTokenManager;
import org.apache.james.jmap.draft.exceptions.BadRequestException;
import org.apache.james.jmap.draft.exceptions.InternalErrorException;
import org.apache.james.jmap.draft.exceptions.UnauthorizedException;
import org.apache.james.jmap.draft.json.MultipleObjectMapperBuilder;
import org.apache.james.jmap.draft.model.AccessTokenRequest;
import org.apache.james.jmap.draft.model.AccessTokenResponse;
import org.apache.james.jmap.draft.model.ContinuationTokenRequest;
import org.apache.james.jmap.draft.model.ContinuationTokenResponse;
import org.apache.james.jmap.draft.model.EndPointsResponse;
import org.apache.james.metrics.api.MetricFactory;
import org.apache.james.user.api.UsersRepository;
import org.apache.james.user.api.UsersRepositoryException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import reactor.netty.http.server.HttpServerRequest;
import reactor.netty.http.server.HttpServerResponse;
import reactor.netty.http.server.HttpServerRoutes;
public class AuthenticationRoutes implements JMAPRoutes {
private static final Logger LOGGER = LoggerFactory.getLogger(AuthenticationRoutes.class);
private final ObjectMapper mapper;
private final UsersRepository usersRepository;
private final SimpleTokenManager simpleTokenManager;
private final AccessTokenManager accessTokenManager;
private final SimpleTokenFactory simpleTokenFactory;
private final MetricFactory metricFactory;
private final Authenticator authenticator;
@Inject
public AuthenticationRoutes(UsersRepository usersRepository, SimpleTokenManager simpleTokenManager, AccessTokenManager accessTokenManager, SimpleTokenFactory simpleTokenFactory, MetricFactory metricFactory, Authenticator authenticator) {
this.mapper = new MultipleObjectMapperBuilder()
.registerClass(ContinuationTokenRequest.UNIQUE_JSON_PATH, ContinuationTokenRequest.class)
.registerClass(AccessTokenRequest.UNIQUE_JSON_PATH, AccessTokenRequest.class)
.build();
this.usersRepository = usersRepository;
this.simpleTokenManager = simpleTokenManager;
this.accessTokenManager = accessTokenManager;
this.simpleTokenFactory = simpleTokenFactory;
this.metricFactory = metricFactory;
this.authenticator = authenticator;
}
@Override
public Logger logger() {
return LOGGER;
}
@Override
public HttpServerRoutes define(HttpServerRoutes builder) {
return builder
.post(AUTHENTICATION, JMAPRoutes.corsHeaders(this::post))
.get(AUTHENTICATION, JMAPRoutes.corsHeaders(this::returnEndPointsResponse))
.delete(AUTHENTICATION, JMAPRoutes.corsHeaders(this::delete))
.options(AUTHENTICATION, CORS_CONTROL);
}
private Mono post(HttpServerRequest request, HttpServerResponse response) {
return Mono.from(metricFactory.runPublishingTimerMetricLogP99("JMAP-authentication-post",
Mono.just(request)
.map(this::assertJsonContentType)
.map(this::assertAcceptJsonOnly)
.flatMap(this::deserialize)
.flatMap(objectRequest -> {
if (objectRequest instanceof ContinuationTokenRequest) {
return handleContinuationTokenRequest((ContinuationTokenRequest) objectRequest, response);
} else if (objectRequest instanceof AccessTokenRequest) {
return handleAccessTokenRequest((AccessTokenRequest) objectRequest, response);
} else {
throw new RuntimeException(objectRequest.getClass() + " " + objectRequest);
}
})))
.onErrorResume(BadRequestException.class, e -> handleBadRequest(response, e))
.doOnEach(logOnError(e -> LOGGER.error("Unexpected error", e)))
.onErrorResume(e -> handleInternalError(response, e))
.subscriberContext(jmapContext(request))
.subscriberContext(jmapAction("auth-post"))
.subscribeOn(Schedulers.elastic());
}
private Mono returnEndPointsResponse(HttpServerRequest req, HttpServerResponse resp) {
return authenticator.authenticate(req)
.flatMap(session -> returnEndPointsResponse(resp)
.subscriberContext(jmapAuthContext(session)))
.onErrorResume(BadRequestException.class, e -> handleBadRequest(resp, e))
.doOnEach(logOnError(e -> LOGGER.error("Unexpected error", e)))
.onErrorResume(InternalErrorException.class, e -> handleInternalError(resp, e))
.onErrorResume(UnauthorizedException.class, e -> handleAuthenticationFailure(resp, e))
.subscriberContext(jmapContext(req))
.subscriberContext(jmapAction("returnEndPoints"))
.subscribeOn(Schedulers.elastic());
}
private Mono returnEndPointsResponse(HttpServerResponse resp) {
try {
return resp.status(OK)
.header(CONTENT_TYPE, JSON_CONTENT_TYPE_UTF8)
.sendString(Mono.just(mapper.writeValueAsString(EndPointsResponse
.builder()
.api(JMAPUrls.JMAP)
.eventSource(JMAPUrls.NOT_IMPLEMENTED)
.upload(JMAPUrls.UPLOAD)
.download(JMAPUrls.DOWNLOAD)
.build())))
.then();
} catch (JsonProcessingException e) {
throw new InternalErrorException("Error serializing endpoint response", e);
}
}
private Mono delete(HttpServerRequest req, HttpServerResponse resp) {
String authorizationHeader = req.requestHeaders().get("Authorization");
return authenticator.authenticate(req)
.flatMap(session -> Mono.from(accessTokenManager.revoke(AccessToken.fromString(authorizationHeader)))
.then(resp.status(NO_CONTENT).send().then())
.subscriberContext(jmapAuthContext(session)))
.onErrorResume(UnauthorizedException.class, e -> handleAuthenticationFailure(resp, e))
.subscriberContext(jmapContext(req))
.subscriberContext(jmapAction("auth-delete"))
.subscribeOn(Schedulers.elastic());
}
private HttpServerRequest assertJsonContentType(HttpServerRequest req) {
if (!Objects.equals(req.requestHeaders().get(CONTENT_TYPE), JSON_CONTENT_TYPE_UTF8)) {
throw new BadRequestException("Request ContentType header must be set to: " + JSON_CONTENT_TYPE_UTF8);
}
return req;
}
private HttpServerRequest assertAcceptJsonOnly(HttpServerRequest req) {
String accept = req.requestHeaders().get(ACCEPT);
if (accept == null || !accept.contains(JSON_CONTENT_TYPE)) {
throw new BadRequestException("Request Accept header must be set to JSON content type");
}
return req;
}
private Mono deserialize(HttpServerRequest req) {
return req.receive().aggregate().asInputStream()
.map(inputStream -> {
try {
return mapper.readValue(inputStream, Object.class);
} catch (IOException e) {
throw new BadRequestException("Request can't be deserialized", e);
}
})
.switchIfEmpty(Mono.error(new BadRequestException("Empty body")));
}
private Mono handleContinuationTokenRequest(ContinuationTokenRequest request, HttpServerResponse resp) {
try {
ContinuationTokenResponse continuationTokenResponse = ContinuationTokenResponse
.builder()
.continuationToken(simpleTokenFactory.generateContinuationToken(request.getUsername()))
.methods(ContinuationTokenResponse.AuthenticationMethod.PASSWORD)
.build();
return resp.header(CONTENT_TYPE, JSON_CONTENT_TYPE_UTF8)
.sendString(Mono.just(mapper.writeValueAsString(continuationTokenResponse)))
.then();
} catch (Exception e) {
throw new InternalErrorException("Error while responding to continuation token", e);
}
}
private Mono handleAccessTokenRequest(AccessTokenRequest request, HttpServerResponse resp) {
SimpleTokenManager.TokenStatus validity = simpleTokenManager.getValidity(request.getToken());
switch (validity) {
case EXPIRED:
return returnForbiddenAuthentication(resp);
case INVALID:
return returnUnauthorizedResponse(resp)
.doOnEach(log(() -> LOGGER.warn("Use of an invalid ContinuationToken : {}", request.getToken().serialize())));
case OK:
return manageAuthenticationResponse(request, resp);
default:
throw new InternalErrorException(String.format("Validity %s is not implemented", validity));
}
}
private Mono manageAuthenticationResponse(AccessTokenRequest request, HttpServerResponse resp) {
Username username = Username.of(request.getToken().getUsername());
return authenticate(request, username)
.flatMap(success -> {
if (success) {
return returnAccessTokenResponse(resp, username);
} else {
return returnUnauthorizedResponse(resp)
.doOnEach(log(() -> LOGGER.info("Authentication failure for {}", username)));
}
});
}
private Mono authenticate(AccessTokenRequest request, Username username) {
return Mono.fromCallable(() -> {
try {
return usersRepository.test(username, request.getPassword());
} catch (UsersRepositoryException e) {
LOGGER.error("Error while trying to validate authentication for user '{}'", username, e);
return false;
}
}).subscribeOn(Schedulers.elastic());
}
private Mono returnAccessTokenResponse(HttpServerResponse resp, Username username) {
return Mono.from(accessTokenManager.grantAccessToken(username))
.map(accessToken -> AccessTokenResponse.builder()
.accessToken(accessToken)
.api(JMAPUrls.JMAP)
.eventSource(JMAPUrls.NOT_IMPLEMENTED)
.upload(JMAPUrls.UPLOAD)
.download(JMAPUrls.DOWNLOAD)
.build())
.flatMap(accessTokenResponse -> {
try {
return resp.status(CREATED)
.header(CONTENT_TYPE, JSON_CONTENT_TYPE_UTF8)
.sendString(Mono.just(mapper.writeValueAsString(accessTokenResponse)))
.then();
} catch (JsonProcessingException e) {
throw new InternalErrorException("Could not serialize access token response", e);
}
});
}
private Mono returnUnauthorizedResponse(HttpServerResponse resp) {
return resp.status(UNAUTHORIZED).send().then();
}
private Mono returnForbiddenAuthentication(HttpServerResponse resp) {
return resp.status(FORBIDDEN).send().then();
}
}