SEBSERV-139 integration of token generation

This commit is contained in:
anhefti 2020-08-10 11:42:58 +02:00
parent 548d4d132f
commit 7335547341
8 changed files with 209 additions and 57 deletions

View file

@ -0,0 +1,79 @@
/*
* Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.gbl.model.exam;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
@JsonIgnoreProperties(ignoreUnknown = true)
public class SEBClientProctoringConnectionData {
public static final String ATTR_SERVER_URL = "serverURL";
public static final String ATTR_ROOM_NAME = "roomName";
public static final String ATTR_ACCESS_TOKEN = "accessToken";
public static final String ATTR_CONNECTION_URL = "connectionURL";
@JsonProperty(ATTR_SERVER_URL)
public final String serverURL;
@JsonProperty(ATTR_ROOM_NAME)
public final String roomName;
@JsonProperty(ATTR_ACCESS_TOKEN)
public final String accessToken;
@JsonProperty(ATTR_CONNECTION_URL)
public final String connectionURL;
@JsonCreator
public SEBClientProctoringConnectionData(
@JsonProperty(ATTR_SERVER_URL) final String serverURL,
@JsonProperty(ATTR_ROOM_NAME) final String roomName,
@JsonProperty(ATTR_ACCESS_TOKEN) final String accessToken,
@JsonProperty(ATTR_CONNECTION_URL) final String connectionURL) {
this.serverURL = serverURL;
this.roomName = roomName;
this.accessToken = accessToken;
this.connectionURL = connectionURL;
}
public String getAccessToken() {
return this.accessToken;
}
public String getServerURL() {
return this.serverURL;
}
public String getConnectionURL() {
return this.connectionURL;
}
public String getRoomName() {
return this.roomName;
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
builder.append("SEBClientProctoringConnectionData [serverURL=");
builder.append(this.serverURL);
builder.append(", roomName=");
builder.append(this.roomName);
builder.append(", accessToken=");
builder.append(this.accessToken);
builder.append(", connectionURL=");
builder.append(this.connectionURL);
builder.append("]");
return builder.toString();
}
}

View file

@ -25,6 +25,7 @@ import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBClientProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
@ -285,16 +286,17 @@ public class MonitoringClientConnection implements TemplateComposer {
// urlLauncher.openURL(
// "https://seb-jitsi.ethz.ch/TestRoomABC?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb250ZXh0Ijp7InVzZXIiOnsiYXZhdGFyIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9qb2huLWRvZSIsIm5hbWUiOiJEaXNwbGF5IE5hbWUiLCJlbWFpbCI6Im5hbWVAZXhhbXBsZS5jb20ifX0sImF1ZCI6InNlYi1qaXRzaSIsImlzcyI6InNlYi1qaXRzaSIsInN1YiI6Im1lZXQuaml0c2kiLCJyb29tIjoiKiJ9.SD9Zs78mMFqxS1tpalPTykYYaubIYsj_406WAOhcqxQ");
final String proctorURL = this.pageService.getRestService().getBuilder(GetProctorURLForClient.class)
.withURIVariable(API.PARAM_MODEL_ID, action.getEntityKey().modelId)
.withURIVariable(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken)
.call()
.getOrThrow();
final SEBClientProctoringConnectionData proctoringConnectionData =
this.pageService.getRestService().getBuilder(GetProctorURLForClient.class)
.withURIVariable(API.PARAM_MODEL_ID, action.getEntityKey().modelId)
.withURIVariable(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken)
.call()
.getOrThrow();
final JavaScriptExecutor javaScriptExecutor = RWT.getClient().getService(JavaScriptExecutor.class);
javaScriptExecutor.execute(
"window.open("
+ "'https://seb-jitsi.ethz.ch/TestRoomABC?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb250ZXh0Ijp7InVzZXIiOnsiYXZhdGFyIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9qb2huLWRvZSIsIm5hbWUiOiJEaXNwbGF5IE5hbWUiLCJlbWFpbCI6Im5hbWVAZXhhbXBsZS5jb20ifX0sImF1ZCI6InNlYi1qaXRzaSIsImlzcyI6InNlYi1qaXRzaSIsInN1YiI6Im1lZXQuaml0c2kiLCJyb29tIjoiKiJ9.SD9Zs78mMFqxS1tpalPTykYYaubIYsj_406WAOhcqxQ',"
+ "'" + proctoringConnectionData.connectionURL + "',"
+ "'proctoring',"
+ "'height=400,width=800,location=no,scrollbars=yes,status=no,menubar=yes,toolbar=yes,titlebar=yes');");

View file

@ -17,22 +17,23 @@ import com.fasterxml.jackson.core.type.TypeReference;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBClientProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
@Lazy
@Component
@GuiProfile
public class GetProctorURLForClient extends RestCall<String> {
public class GetProctorURLForClient extends RestCall<SEBClientProctoringConnectionData> {
public GetProctorURLForClient() {
super(new TypeKey<>(
CallType.GET_SINGLE,
EntityType.EXAM_PROCTOR_DATA,
new TypeReference<String>() {
new TypeReference<SEBClientProctoringConnectionData>() {
}),
HttpMethod.GET,
MediaType.APPLICATION_JSON_UTF8,
MediaType.APPLICATION_FORM_URLENCODED,
API.EXAM_ADMINISTRATION_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_ADMINISTRATION_PROCTOR_PATH_SEGMENT

View file

@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.exam;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings.ServerType;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBClientProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.util.Result;
@ -19,12 +20,12 @@ public interface ExamProctoringService {
Result<Boolean> testExamProctoring(final ProctoringSettings examProctoring);
public Result<String> createProctoringURL(
public Result<SEBClientProctoringConnectionData> createProctoringConnectionData(
final ProctoringSettings examProctoring,
final String connectionToken,
final boolean server);
Result<String> createProctoringURL(
Result<SEBClientProctoringConnectionData> createProctoringConnectionData(
final ProctoringSettings examProctoring,
ClientConnection clientConnection,
boolean server);

View file

@ -9,6 +9,8 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.exam.impl;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Base64.Encoder;
@ -23,8 +25,10 @@ import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings.ServerType;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBClientProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Cryptor;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamProctoringService;
@ -42,9 +46,14 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
"{\"context\":{\"user\":{\"name\":\"%s\"}},\"iss\":\"%s\",\"aud\":\"%s\",\"sub\":\"%s\",\"room\":\"%s\"%s}";
private final ExamSessionService examSessionService;
private final Cryptor cryptor;
protected ExamJITSIProctoringService(
final ExamSessionService examSessionService,
final Cryptor cryptor) {
protected ExamJITSIProctoringService(final ExamSessionService examSessionService) {
this.examSessionService = examSessionService;
this.cryptor = cryptor;
}
@Override
@ -59,13 +68,14 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
}
@Override
public Result<String> createProctoringURL(
public Result<SEBClientProctoringConnectionData> createProctoringConnectionData(
final ProctoringSettings examProctoring,
final String connectionToken,
final boolean server) {
return Result.tryCatch(() -> {
return createProctoringURL(examProctoring,
return createProctoringConnectionData(
examProctoring,
this.examSessionService
.getConnectionData(connectionToken)
.getOrThrow().clientConnection,
@ -75,7 +85,7 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
}
@Override
public Result<String> createProctoringURL(
public Result<SEBClientProctoringConnectionData> createProctoringConnectionData(
final ProctoringSettings examProctoring,
final ClientConnection clientConnection,
final boolean server) {
@ -90,23 +100,29 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
if (this.examSessionService.isExamRunning(examProctoring.examId)) {
final Exam exam = this.examSessionService.getRunningExam(examProctoring.examId)
.getOrThrow();
expTime = exam.endTime.getMillis();
if (exam.endTime != null) {
expTime = exam.endTime.getMillis();
}
}
return createProctoringURL(
final Encoder urlEncoder = Base64.getUrlEncoder().withoutPadding();
final String roomName = urlEncoder.encodeToString(
Utils.toByteArray(clientConnection.connectionToken));
return createProctoringConnectionData(
examProctoring.serverURL,
examProctoring.appKey,
examProctoring.getAppSecret(),
clientConnection.userSessionId,
(server) ? "seb-server" : "seb-client",
clientConnection.connectionToken,
roomName,
expTime)
.getOrThrow();
});
}
public Result<String> createProctoringURL(
public Result<SEBClientProctoringConnectionData> createProctoringConnectionData(
final String url,
final String appKey,
final CharSequence appSecret,
@ -117,46 +133,84 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
return Result.tryCatch(() -> {
final Encoder urlEncoder = Base64.getUrlEncoder().withoutPadding();
final String roomUrl = createServerConnectionURL(url, roomName);
final String host = UriComponentsBuilder.fromHttpUrl(url)
.build()
.getHost();
final StringBuilder builder = new StringBuilder();
builder.append(url)
.append("/")
.append(roomName)
.append("?jwt=");
final String jwtHeaderPart = urlEncoder
.encodeToString(JITSI_ACCESS_TOKEN_HEADER.getBytes(StandardCharsets.UTF_8));
final String jwtPayload = String.format(
JITSI_ACCESS_TOKEN_PAYLOAD.replaceAll(" ", "").replaceAll("\n", ""),
clientName,
final CharSequence decryptedSecret = this.cryptor.decrypt(appSecret);
final String token = createAccessToken(
appKey,
decryptedSecret,
clientName,
clientKey,
host,
roomName,
(expTime != null)
? String.format(",\"exp\":%s", String.valueOf(expTime))
: "");
final String jwtPayloadPart = urlEncoder
.encodeToString(jwtPayload.getBytes(StandardCharsets.UTF_8));
final String message = jwtHeaderPart + "." + jwtPayloadPart;
expTime,
host);
final Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
final SecretKeySpec secret_key =
new SecretKeySpec(Utils.toByteArray(appSecret), "HmacSHA256");
sha256_HMAC.init(secret_key);
final String hash = urlEncoder.encodeToString(sha256_HMAC.doFinal(Utils.toByteArray(message)));
final StringBuilder builder = new StringBuilder();
final String connectionURL = builder.append(roomUrl)
.append("?jwt=")
.append(token).toString();
builder.append(message)
.append(".")
.append(hash);
return builder.toString();
return new SEBClientProctoringConnectionData(
roomUrl,
roomName,
token,
connectionURL);
});
}
private String createServerConnectionURL(
final String url,
final String roomName) {
final StringBuilder builder = new StringBuilder();
return builder.append(url)
.append("/")
.append(roomName)
.toString();
}
private String createAccessToken(
final String appKey,
final CharSequence appSecret,
final String clientName,
final String clientKey,
final String roomName,
final Long expTime,
final String host) throws NoSuchAlgorithmException, InvalidKeyException {
final StringBuilder builder = new StringBuilder();
final Encoder urlEncoder = Base64.getUrlEncoder().withoutPadding();
final String jwtHeaderPart = urlEncoder
.encodeToString(JITSI_ACCESS_TOKEN_HEADER.getBytes(StandardCharsets.UTF_8));
final String jwtPayload = String.format(
JITSI_ACCESS_TOKEN_PAYLOAD.replaceAll(" ", "").replaceAll("\n", ""),
clientName,
appKey,
clientKey,
host,
roomName,
(expTime != null)
? String.format(",\"exp\":%s", String.valueOf(expTime))
: "");
final String jwtPayloadPart = urlEncoder
.encodeToString(jwtPayload.getBytes(StandardCharsets.UTF_8));
final String message = jwtHeaderPart + "." + jwtPayloadPart;
final Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
final SecretKeySpec secret_key =
new SecretKeySpec(Utils.toByteArray(appSecret), "HmacSHA256");
sha256_HMAC.init(secret_key);
final String hash = urlEncoder.encodeToString(sha256_HMAC.doFinal(Utils.toByteArray(message)));
builder.append(message)
.append(".")
.append(hash);
return builder.toString();
}
}

View file

@ -52,6 +52,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBClientProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.Features;
@ -429,8 +430,8 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
+ API.EXAM_ADMINISTRATION_PROCTOR_PATH_SEGMENT
+ API.EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT,
method = RequestMethod.GET,
produces = MediaType.TEXT_PLAIN_VALUE)
public String getExamProctoringURL(
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public SEBClientProctoringConnectionData getExamProctoringURL(
@RequestParam(
name = API.PARAM_INSTITUTION_ID,
required = true,
@ -444,8 +445,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
.flatMap(this.examAdminService::getExamProctoring)
.flatMap(proc -> this.examAdminService
.getExamProctoringService(proc.serverType)
.map(s -> s.createProctoringURL(proc, connectionToken, true))
.getOrThrow())
.flatMap(s -> s.createProctoringConnectionData(proc, connectionToken, true)))
.getOrThrow();
}

View file

@ -5,5 +5,5 @@ server.port=8080
server.servlet.context-path=/
server.tomcat.uri-encoding=UTF-8
logging.level.ch=DEBUG
logging.level.ch=ERROR

View file

@ -9,15 +9,22 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.exam.impl;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import org.junit.Test;
import org.mockito.Mockito;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBClientProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.util.Cryptor;
public class ExamJITSIProctoringServiceTest {
@Test
public void testCreateProctoringURL() {
final ExamJITSIProctoringService examJITSIProctoringService = new ExamJITSIProctoringService(null);
final String jwt = examJITSIProctoringService.createProctoringURL(
Cryptor cryptorMock = Mockito.mock(Cryptor.class);
Mockito.when(cryptorMock.decrypt(Mockito.any())).thenReturn("fbvgeghergrgrthrehreg123");
final ExamJITSIProctoringService examJITSIProctoringService = new ExamJITSIProctoringService(null, cryptorMock);
final SEBClientProctoringConnectionData data = examJITSIProctoringService.createProctoringConnectionData(
"https://seb-jitsi.example.ch",
"test-app",
"fbvgeghergrgrthrehreg123",
@ -27,9 +34,17 @@ public class ExamJITSIProctoringServiceTest {
1609459200L)
.getOrThrow();
assertNotNull(data);
assertEquals(
"https://seb-jitsi.example.ch/SomeRoom",
data.serverURL);
assertEquals(
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb250ZXh0Ijp7InVzZXIiOnsibmFtZSI6IlRlc3QgTmFtZSJ9fSwiaXNzIjoidGVzdC1hcHAiLCJhdWQiOiJ0ZXN0LWNsaWVudCIsInN1YiI6InNlYi1qaXRzaS5leGFtcGxlLmNoIiwicm9vbSI6IlNvbWVSb29tIiwiZXhwIjoxNjA5NDU5MjAwfQ.4ovqUkG6jrLvkDEZNdhbtFI_DFLDFsM2eBJHhcYq7a4",
data.accessToken);
assertEquals(
"https://seb-jitsi.example.ch/SomeRoom?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb250ZXh0Ijp7InVzZXIiOnsibmFtZSI6IlRlc3QgTmFtZSJ9fSwiaXNzIjoidGVzdC1hcHAiLCJhdWQiOiJ0ZXN0LWNsaWVudCIsInN1YiI6InNlYi1qaXRzaS5leGFtcGxlLmNoIiwicm9vbSI6IlNvbWVSb29tIiwiZXhwIjoxNjA5NDU5MjAwfQ.4ovqUkG6jrLvkDEZNdhbtFI_DFLDFsM2eBJHhcYq7a4",
jwt);
data.connectionURL);
}
}