blob: 70457f904bb1ddc7b288c6b025890713c740bd6c [file] [log] [blame]
/****************************************************************
* 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;
import static io.restassured.RestAssured.given;
import static io.restassured.RestAssured.with;
import static org.apache.james.jmap.JMAPTestingConstants.jmapRequestSpecBuilder;
import static org.hamcrest.Matchers.both;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.isA;
import static org.hamcrest.Matchers.notNullValue;
import java.io.IOException;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
import org.apache.james.GuiceJamesServer;
import org.apache.james.jmap.JmapGuiceProbe;
import org.apache.james.jmap.draft.model.ContinuationToken;
import org.apache.james.junit.categories.BasicFeature;
import org.apache.james.utils.DataProbeImpl;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
public abstract class JMAPAuthenticationTest {
private static final ZonedDateTime oldDate = ZonedDateTime.parse("2011-12-03T10:15:30+01:00", DateTimeFormatter.ISO_OFFSET_DATE_TIME);
private static final ZonedDateTime newDate = ZonedDateTime.parse("2011-12-03T10:16:30+01:00", DateTimeFormatter.ISO_OFFSET_DATE_TIME);
private static final ZonedDateTime afterExpirationDate = ZonedDateTime.parse("2011-12-03T10:30:31+01:00", DateTimeFormatter.ISO_OFFSET_DATE_TIME);
protected abstract GuiceJamesServer createJmapServer(FixedDateZonedDateTimeProvider zonedDateTimeProvider) throws IOException;
private UserCredentials userCredentials;
private FixedDateZonedDateTimeProvider zonedDateTimeProvider;
private GuiceJamesServer jmapServer;
@Before
public void setup() throws Throwable {
zonedDateTimeProvider = new FixedDateZonedDateTimeProvider();
zonedDateTimeProvider.setFixedDateTime(oldDate);
jmapServer = createJmapServer(zonedDateTimeProvider);
jmapServer.start();
RestAssured.requestSpecification = jmapRequestSpecBuilder
.setPort(jmapServer.getProbe(JmapGuiceProbe.class).getJmapPort().getValue())
.build();
userCredentials = UserCredentials.builder()
.username("user@domain.tld")
.password("password")
.build();
String domain = "domain.tld";
jmapServer.getProbe(DataProbeImpl.class)
.fluent()
.addDomain(domain)
.addUser(userCredentials.getUsername(), userCredentials.getPassword());
}
@After
public void teardown() {
jmapServer.stop();
}
@Test
public void mustReturnMalformedRequestWhenContentTypeIsMissing() {
given()
.accept(ContentType.JSON)
.contentType("")
.when()
.post("/authentication")
.then()
.statusCode(400);
}
@Test
public void mustReturnMalformedRequestWhenContentTypeIsNotJson() {
given()
.contentType(ContentType.XML)
.accept(ContentType.JSON)
.when()
.post("/authentication")
.then()
.statusCode(400);
}
@Test
public void mustReturnMalformedRequestWhenAcceptIsMissing() {
given()
.accept("")
.contentType(ContentType.JSON)
.when()
.post("/authentication")
.then()
.statusCode(400);
}
@Test
public void mustReturnMalformedRequestWhenAcceptIsNotJson() {
given()
.contentType(ContentType.JSON)
.accept(ContentType.XML)
.when()
.post("/authentication")
.then()
.statusCode(400);
}
@Test
public void mustReturnMalformedRequestWhenCharsetIsNotUTF8() {
given()
.contentType("application/json; charset=ISO-8859-1")
.accept(ContentType.JSON)
.when()
.post("/authentication")
.then()
.statusCode(400);
}
@Test
public void mustReturnMalformedRequestWhenBodyIsEmpty() {
given()
.contentType(ContentType.JSON)
.accept(ContentType.JSON)
.when()
.post("/authentication")
.then()
.statusCode(400);
}
@Test
public void mustReturnMalformedRequestWhenBodyIsNotAcceptable() {
given()
.contentType(ContentType.JSON)
.accept(ContentType.JSON)
.body("{\"badAttributeName\": \"value\"}")
.when()
.post("/authentication")
.then()
.statusCode(400);
}
@Test
public void mustPositionCorsHeaders() {
given()
.contentType(ContentType.JSON)
.accept(ContentType.JSON)
.body("{\"username\": \"" + userCredentials.getUsername() + "\", \"clientName\": \"Mozilla Thunderbird\", \"clientVersion\": \"42.0\", \"deviceName\": \"Joe Blogg’s iPhone\"}")
.when()
.post("/authentication")
.then()
.header("Access-Control-Allow-Origin", "*")
.header("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT")
.header("Access-Control-Allow-Headers", "Content-Type, Authorization, Accept")
.header("Access-Control-Max-Age", "86400");
}
@Test
public void mustReturnJsonResponse() {
given()
.contentType(ContentType.JSON)
.accept(ContentType.JSON)
.body("{\"username\": \"" + userCredentials.getUsername() + "\", \"clientName\": \"Mozilla Thunderbird\", \"clientVersion\": \"42.0\", \"deviceName\": \"Joe Blogg’s iPhone\"}")
.when()
.post("/authentication")
.then()
.statusCode(200)
.header("Content-Length", "447")
.contentType(ContentType.JSON);
}
@Test
public void methodShouldContainPasswordWhenValidRequest() {
given()
.contentType(ContentType.JSON)
.accept(ContentType.JSON)
.body("{\"username\": \"" + userCredentials.getUsername() + "\", \"clientName\": \"Mozilla Thunderbird\", \"clientVersion\": \"42.0\", \"deviceName\": \"Joe Blogg’s iPhone\"}")
.when()
.post("/authentication")
.then()
.statusCode(200)
.body("methods", hasItem(userCredentials.getPassword()));
}
@Category(BasicFeature.class)
@Test
public void mustReturnContinuationTokenWhenValidRequest() {
given()
.contentType(ContentType.JSON)
.accept(ContentType.JSON)
.body("{\"username\": \"" + userCredentials.getUsername() + "\", \"clientName\": \"Mozilla Thunderbird\", \"clientVersion\": \"42.0\", \"deviceName\": \"Joe Blogg’s iPhone\"}")
.when()
.post("/authentication")
.then()
.statusCode(200)
.body("continuationToken", isA(String.class));
}
@Category(BasicFeature.class)
@Test
public void mustReturnAuthenticationFailedWhenBadPassword() {
String continuationToken = fromGoodContinuationTokenRequest();
given()
.contentType(ContentType.JSON)
.accept(ContentType.JSON)
.body("{\"token\": \"" + continuationToken + "\", \"method\": \"password\", \"password\": \"badpassword\"}")
.when()
.post("/authentication")
.then()
.statusCode(401);
}
@Test
public void mustReturnAuthenticationFailedWhenContinuationTokenIsRejectedByTheContinuationTokenManager() throws Exception {
ContinuationToken badContinuationToken = new ContinuationToken(userCredentials.getUsername(), newDate, "badSignature");
given()
.contentType(ContentType.JSON)
.accept(ContentType.JSON)
.body("{\"token\": \"" + badContinuationToken.serialize() + "\", \"method\": \"password\", \"password\": \"" + userCredentials.getPassword() + "\"}")
.when()
.post("/authentication")
.then()
.statusCode(401);
}
@Test
public void mustReturnRestartAuthenticationWhenContinuationTokenIsExpired() {
String continuationToken = fromGoodContinuationTokenRequest();
zonedDateTimeProvider.setFixedDateTime(afterExpirationDate);
given()
.contentType(ContentType.JSON)
.accept(ContentType.JSON)
.body("{\"token\": \"" + continuationToken + "\", \"method\": \"password\", \"password\": \"" + userCredentials.getPassword() + "\"}")
.when()
.post("/authentication")
.then()
.statusCode(403);
}
@Test
public void mustReturnAuthenticationFailedWhenUsersRepositoryException() {
String continuationToken = fromGoodContinuationTokenRequest();
given()
.contentType(ContentType.JSON)
.accept(ContentType.JSON)
.body("{\"token\": \"" + continuationToken + "\", \"method\": \"password\", \"password\": \"" + "wrong password" + "\"}")
.when()
.post("/authentication")
.then()
.statusCode(401);
}
@Test
public void mustReturnCreatedWhenGoodPassword() {
String continuationToken = fromGoodContinuationTokenRequest();
zonedDateTimeProvider.setFixedDateTime(newDate);
given()
.contentType(ContentType.JSON)
.accept(ContentType.JSON)
.body("{\"token\": \"" + continuationToken + "\", \"method\": \"password\", \"password\": \"" + userCredentials.getPassword() + "\"}")
.when()
.post("/authentication")
.then()
.statusCode(201);
}
@Category(BasicFeature.class)
@Test
public void mustSendJsonContainingAccessTokenAndEndpointsWhenGoodPassword() {
String continuationToken = fromGoodContinuationTokenRequest();
zonedDateTimeProvider.setFixedDateTime(newDate);
given()
.contentType(ContentType.JSON)
.accept(ContentType.JSON)
.body("{\"token\": \"" + continuationToken + "\", \"method\": \"password\", \"password\": \"" + userCredentials.getPassword() + "\"}")
.when()
.post("/authentication")
.then()
.body("accessToken", isA(String.class))
.body("api", equalTo("/jmap"))
.body("eventSource", both(isA(String.class)).and(notNullValue()))
.body("upload", equalTo("/upload"))
.body("download", equalTo("/download"));
}
@Test
public void getMustReturnUnauthorizedWithoutAuthorizationHeader() {
given()
.when()
.get("/authentication")
.then()
.statusCode(401);
}
@Test
public void getMustReturnUnauthorizedWithoutAValidAuthorizationHeader() {
given()
.header("Authorization", UUID.randomUUID())
.when()
.get("/authentication")
.then()
.statusCode(401);
}
@Category(BasicFeature.class)
@Test
public void getMustReturnEndpointsWhenValidAuthorizationHeader() {
String continuationToken = fromGoodContinuationTokenRequest();
String token = fromGoodAccessTokenRequest(continuationToken);
given()
.header("Authorization", token)
.when()
.get("/authentication")
.then()
.statusCode(200)
.body("api", equalTo("/jmap"))
.body("eventSource", both(isA(String.class)).and(notNullValue()))
.body("upload", equalTo("/upload"))
.body("download", equalTo("/download"));
}
@Category(BasicFeature.class)
@Test
public void getMustReturnEndpointsWhenValidJwtAuthorizationHeader() {
String token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyQGRvbWFpbi50bGQifQ.U-dUPv6OU6KO5N7CooHUfMkCd" +
"FJHx2F3H4fm7Q79g1BPfBSkifPj5xyVlZ0JwEGXypC4zBw9ay3l4DxzX7D_6p1Hx_ihXsoLx1Ca-WUo44x-XRSpPfgxiZjHCJkGBLMV3RZlA" +
"jip-d18mxkcX3JGplX_sCQkFisduAOAHuKSUg9wI6VBgUQi_0B35FYv6tP_bD6eFtvaAUN9QyXXh8UQjEp8CO12lRz6enfLx_V6BG_fEMkee" +
"6vRqdEqx_F9OF3eWTe1giMp_JhQ7_l1OXXtbd4TndVvTeuVy4irPbsRc-M8x_-qTDpFp6saRRsyOcFspxPp5n3yIhEK7B3UZiseXw";
given()
.header("Authorization", "Bearer " + token)
.when()
.get("/authentication")
.then()
.statusCode(200);
}
@Test
public void getMustReturnEndpointsWhenValidUnknownUserJwtAuthorizationHeader() {
String token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1bmtub3duQGRvbWFpbi50bGQifQ.hr8AhlNIpiA3Mv_A5ZLyL" +
"f1BHeBSaRDfdR_GLV_hlPdIrWv1xtwjBH86E1YnTPx2tTpr_NWTbHcR1OCkuVCpgloEnUNbE3U2l0WrGOX2Eh9dWCXOCtrNvCeSHQuvx5_8W" +
"nSVENYidk7o2icE8_gz_Giwf0Z3bHJJYXfAxupv__tCkmhqt3E888VZPjs26AsqxQ29YyX0Fjx8UwKbPrH5-tnyftX-kLjjZNtahVIVtbW4v" +
"b8rEEZ4nzqxHqtI2co6yCXjgyoFMdDAKCOU-Bq35Gdo-Qiu8l7a0kQGhuhkjoaVWvw4bcSvunxAnh_0W5g3Lw-rwljNu2JrJS0gAH6NDA";
given()
.header("Authorization", "Bearer " + token)
.when()
.get("/authentication")
.then()
.statusCode(200);
}
@Test
public void getMustReturnBadCredentialsWhenInvalidJwtAuthorizationHeader() {
String token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.T04BTk" +
"LXkJj24coSZkK13RfG25lpvmSl2MJ7N10KpBk9_-95EGYZdog-BDAn3PJzqVw52z-Bwjh4VOj1-j7cURu0cT4jXehhUrlCxS4n7QHZ" +
"EN_bsEYGu7KzjWTpTsUiHe-rN7izXVFxDGG1TGwlmBCBnPW-EFCf9ylUsJi0r2BKNdaaPRfMIrHptH1zJBkkUziWpBN1RNLjmvlAUf" +
"49t1Tbv21ZqYM5Ht2vrhJWczFbuC-TD-8zJkXhjTmA1GVgomIX5dx1cH-dZX1wANNmshUJGHgepWlPU-5VIYxPEhb219RMLJIELMY2" +
"qNOR8Q31ydinyqzXvCSzVJOf6T60-w";
given()
.header("Authorization", "Bearer " + token)
.when()
.get("/authentication")
.then()
.statusCode(401);
}
@Test
public void optionsRequestsShouldNeverRequireAuthentication() {
given()
.when()
.options("/authentication")
.then()
.statusCode(200);
}
@Test
public void getMustReturnEndpointsWhenCorrectAuthentication() {
String continuationToken = fromGoodContinuationTokenRequest();
zonedDateTimeProvider.setFixedDateTime(newDate);
String accessToken = fromGoodAccessTokenRequest(continuationToken);
given()
.header("Authorization", accessToken)
.when()
.get("/authentication")
.then()
.statusCode(200)
.body("api", isA(String.class));
}
@Category(BasicFeature.class)
@Test
public void getShouldReturn400WhenMultipleCredentials() {
String jwtToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyQGRvbWFpbi50bGQifQ.U-dUPv6OU6KO5N7CooHUfMkCd" +
"FJHx2F3H4fm7Q79g1BPfBSkifPj5xyVlZ0JwEGXypC4zBw9ay3l4DxzX7D_6p1Hx_ihXsoLx1Ca-WUo44x-XRSpPfgxiZjHCJkGBLMV3RZlA" +
"jip-d18mxkcX3JGplX_sCQkFisduAOAHuKSUg9wI6VBgUQi_0B35FYv6tP_bD6eFtvaAUN9QyXXh8UQjEp8CO12lRz6enfLx_V6BG_fEMkee" +
"6vRqdEqx_F9OF3eWTe1giMp_JhQ7_l1OXXtbd4TndVvTeuVy4irPbsRc-M8x_-qTDpFp6saRRsyOcFspxPp5n3yIhEK7B3UZiseXw";
String continuationToken = fromGoodContinuationTokenRequest();
String accessToken = fromGoodAccessTokenRequest(continuationToken);
given()
.header("Authorization", "Bearer " + jwtToken)
.header("Authorization", accessToken)
.when()
.get("/authentication")
.then()
.statusCode(400);
}
@Test
public void deleteMustReturnUnauthenticatedWithoutAuthorizationHeader() {
given()
.when()
.delete("/authentication")
.then()
.statusCode(401);
}
@Test
public void deleteMustReturnUnauthenticatedWithoutAValidAuthroizationHeader() {
given()
.header("Authorization", UUID.randomUUID())
.when()
.delete("/authentication")
.then()
.statusCode(401);
}
@Test
public void deleteMustReturnOKNoContentOnValidAuthorizationToken() {
String continuationToken = fromGoodContinuationTokenRequest();
String token = fromGoodAccessTokenRequest(continuationToken);
given()
.header("Authorization", token)
.when()
.delete("/authentication")
.then()
.statusCode(204);
}
@Category(BasicFeature.class)
@Test
public void deleteMustInvalidAuthorizationOnCorrectAuthorization() {
String continuationToken = fromGoodContinuationTokenRequest();
zonedDateTimeProvider.setFixedDateTime(newDate);
String accessToken = fromGoodAccessTokenRequest(continuationToken);
goodDeleteAccessTokenRequest(accessToken);
given()
.header("Authorization", accessToken)
.when()
.get("/authentication")
.then()
.statusCode(401);
}
private String fromGoodContinuationTokenRequest() {
return with()
.contentType(ContentType.JSON)
.accept(ContentType.JSON)
.body("{\"username\": \"" + userCredentials.getUsername() + "\", \"clientName\": \"Mozilla Thunderbird\", \"clientVersion\": \"42.0\", \"deviceName\": \"Joe Blogg’s iPhone\"}")
.post("/authentication")
.body()
.path("continuationToken")
.toString();
}
private String fromGoodAccessTokenRequest(String continuationToken) {
return with()
.contentType(ContentType.JSON)
.accept(ContentType.JSON)
.body("{\"token\": \"" + continuationToken + "\", \"method\": \"password\", \"password\": \"" + userCredentials.getPassword() + "\"}")
.post("/authentication")
.path("accessToken")
.toString();
}
private void goodDeleteAccessTokenRequest(String accessToken) {
with()
.header("Authorization", accessToken)
.delete("/authentication");
}
}