From 7335547341f91c4271780ec0aa65f56df15069ed Mon Sep 17 00:00:00 2001 From: anhefti Date: Mon, 10 Aug 2020 11:42:58 +0200 Subject: [PATCH] SEBSERV-139 integration of token generation --- .../SEBClientProctoringConnectionData.java | 79 +++++++++++ .../content/MonitoringClientConnection.java | 14 +- .../api/session/GetProctorURLForClient.java | 7 +- .../exam/ExamProctoringService.java | 5 +- .../exam/impl/ExamJITSIProctoringService.java | 130 +++++++++++++----- .../api/ExamAdministrationController.java | 8 +- .../config/application-dev.properties | 2 +- .../impl/ExamJITSIProctoringServiceTest.java | 21 ++- 8 files changed, 209 insertions(+), 57 deletions(-) create mode 100644 src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/SEBClientProctoringConnectionData.java diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/SEBClientProctoringConnectionData.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/SEBClientProctoringConnectionData.java new file mode 100644 index 00000000..8666adff --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/SEBClientProctoringConnectionData.java @@ -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(); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringClientConnection.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringClientConnection.java index a2c4018f..a41455af 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringClientConnection.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringClientConnection.java @@ -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');"); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetProctorURLForClient.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetProctorURLForClient.java index 15a929bb..5d3ca0c3 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetProctorURLForClient.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetProctorURLForClient.java @@ -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 { +public class GetProctorURLForClient extends RestCall { public GetProctorURLForClient() { super(new TypeKey<>( CallType.GET_SINGLE, EntityType.EXAM_PROCTOR_DATA, - new TypeReference() { + new TypeReference() { }), 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 diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamProctoringService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamProctoringService.java index e3d3c6de..6428f429 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamProctoringService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamProctoringService.java @@ -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 testExamProctoring(final ProctoringSettings examProctoring); - public Result createProctoringURL( + public Result createProctoringConnectionData( final ProctoringSettings examProctoring, final String connectionToken, final boolean server); - Result createProctoringURL( + Result createProctoringConnectionData( final ProctoringSettings examProctoring, ClientConnection clientConnection, boolean server); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamJITSIProctoringService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamJITSIProctoringService.java index 57415104..4b706070 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamJITSIProctoringService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamJITSIProctoringService.java @@ -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 createProctoringURL( + public Result 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 createProctoringURL( + public Result 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 createProctoringURL( + public Result 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(); + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java index 45f8a46e..e92dbc40 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java @@ -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 { + 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 { .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(); } diff --git a/src/main/resources/config/application-dev.properties b/src/main/resources/config/application-dev.properties index 1a7f85fe..a0ee0b69 100644 --- a/src/main/resources/config/application-dev.properties +++ b/src/main/resources/config/application-dev.properties @@ -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 diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamJITSIProctoringServiceTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamJITSIProctoringServiceTest.java index 5f550583..f176677b 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamJITSIProctoringServiceTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamJITSIProctoringServiceTest.java @@ -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); + } }