diff --git a/schema.sql b/schema.sql index 9795f34..663952d 100644 --- a/schema.sql +++ b/schema.sql @@ -599,7 +599,8 @@ DELIMITER ; CREATE TABLE `tm_platforms` ( `ID` bigint NOT NULL, `name` varchar(255) NOT NULL, - `public_key` varchar(500) NOT NULL + `public_key` varchar(500) NOT NULL, + `api_url` varchar(200) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3; -- -------------------------------------------------------- diff --git a/src/crypto/jwes_decoder.ts b/src/crypto/jwes_decoder.ts index 5ee98bb..79e9576 100644 --- a/src/crypto/jwes_decoder.ts +++ b/src/crypto/jwes_decoder.ts @@ -12,12 +12,23 @@ export class JwesDecoder { public algorithm = 'ES256'; // Recipient public key to check signature for JWS, own private key to decrypt for JWE - public async setKeys(jwsKey: string|undefined, jweKey: string|undefined): Promise { - if (!jwsKey || !jweKey) { + public async setKeys(jwsKey: string, jweKey: string|undefined = undefined): Promise { + await this.setJwsKey(jwsKey); + + if (jweKey) { + await this.setJweKey(jweKey); + } + } + + public async setJwsKey(jwsKey: string): Promise { + if (!jwsKey) { throw new Error('A valid JWS key and a valid JWE key must be fulfilled'); } this.jwsKey = await jose.importSPKI(jwsKey, this.algorithm); + } + + public async setJweKey(jweKey: string): Promise { this.jweKey = await jose.importPKCS8(jweKey, this.algorithm); } diff --git a/src/db_models.ts b/src/db_models.ts index 32fc695..9f75fe0 100644 --- a/src/db_models.ts +++ b/src/db_models.ts @@ -82,6 +82,7 @@ export interface Platform { ID: string, name: string, public_key: string, + api_url: string, } export interface Submission { diff --git a/src/error_handler.ts b/src/error_handler.ts index 8bc34e2..062a75c 100644 --- a/src/error_handler.ts +++ b/src/error_handler.ts @@ -9,6 +9,9 @@ export class NotFoundError extends Error { export class InvalidInputError extends Error { } +export class PlatformInteractionError extends Error { +} + export function isResponseBoom(response: any): response is Boom { return (response as Boom).isBoom && (response as Boom).isServer; } diff --git a/src/grader_interface.ts b/src/grader_interface.ts index 4bb485d..a18ba84 100644 --- a/src/grader_interface.ts +++ b/src/grader_interface.ts @@ -1,5 +1,5 @@ -import {PlatformTokenParameters, TokenGenerator} from './tokenization'; -import {findSourceCodeById, getPlatformTokenParams, SubmissionParameters} from './submissions'; +import {TokenGenerator} from './tokenization'; +import {findSourceCodeById, SubmissionParameters} from './submissions'; import * as Db from './db'; import {findTaskById} from './tasks'; import {Submission, TaskLimit, TaskLimitModel, TaskTest} from './db_models'; @@ -8,6 +8,10 @@ import got from 'got'; import log from 'loglevel'; import {getRandomId} from './util'; import appConfig from './config'; +import { + PlatformAnswerTokenData, + PlatformTaskTokenData, +} from './platform_interface'; function baseLangToJSONLang(baseLang: string): string { baseLang = baseLang.toLocaleLowerCase(); @@ -85,8 +89,8 @@ export function setQueueRequestSender(queueRequestSender: (queueRequest: QueueRe sendQueueRequestToGraderQueue = queueRequestSender; } -export async function sendSubmissionToTaskGrader(submissionId: string, submissionData: SubmissionParameters): Promise { - const queueRequest = await generateQueueRequest(submissionId, submissionData); +export async function sendSubmissionToTaskGrader(submissionId: string, submissionData: SubmissionParameters, taskTokenData: PlatformTaskTokenData, answerTokenData: PlatformAnswerTokenData|null): Promise { + const queueRequest = await generateQueueRequest(submissionId, submissionData, taskTokenData, answerTokenData); let queueAnswer: string; @@ -117,40 +121,19 @@ export interface QueueRequest { sPlatform?: string, } -export async function generateQueueRequest(submissionId: string, submissionData: SubmissionParameters): Promise { - const params = await getPlatformTokenParams(submissionData.taskId, submissionData.token, submissionData.platform); +export async function generateQueueRequest(submissionId: string, submissionData: SubmissionParameters, taskTokenData: PlatformTaskTokenData, answerTokenData: PlatformAnswerTokenData|null): Promise { let idUserAnswer = null; - let answerTokenParams: PlatformTokenParameters|null = null; - if (submissionData.answerToken) { - answerTokenParams = await getPlatformTokenParams(submissionData.taskId, submissionData.answerToken, submissionData.platform); - if (answerTokenParams.idUserAnswer) { - idUserAnswer = answerTokenParams.idUserAnswer; - } - } - - if (answerTokenParams && !appConfig.testMode.enabled) { - if (answerTokenParams.idUser !== params.idUser || answerTokenParams.itemUrl !== params.itemUrl) { - throw new InvalidInputError(`Mismatching tokens idUser or itemUrl, token = ${JSON.stringify(params)}, answerToken = ${JSON.stringify(answerTokenParams)}`); - } - if (!answerTokenParams.sAnswer) { - throw new InvalidInputError('Missing answer in answerToken'); - } - const decodedAnswer = JSON.parse(answerTokenParams.sAnswer) as {idSubmission?: string}; - if (!('idSubmission' in decodedAnswer) || decodedAnswer['idSubmission'] !== submissionId) { - throw new InvalidInputError('Impossible to read submission associated with answer token or submission ID mismatching'); - } - if (false === params.bSubmissionPossible || false === params.bAllowGrading) { - throw new InvalidInputError('Token indicates read-only task'); - } + if (answerTokenData?.payload.idUserAnswer) { + idUserAnswer = answerTokenData.payload.idUserAnswer; } - const returnUrl = submissionData?.taskParams?.returnUrl ?? params.returnUrl; + const returnUrl = submissionData?.taskParams?.returnUrl; if (returnUrl || idUserAnswer) { await Db.execute('update tm_submissions set sReturnUrl = :returnUrl, idUserAnswer = :idUserAnswer WHERE tm_submissions.`ID` = :idSubmission and tm_submissions.idUser = :idUser and tm_submissions.idPlatform = :idPlatform and tm_submissions.idTask = :idTask;', { - idUser: params.idUser, - idTask: params.idTaskLocal, - idPlatform: params.idPlatform, + idUser: taskTokenData.payload.idUser, + idTask: taskTokenData.idTaskLocal, + idPlatform: taskTokenData.platform.ID, idSubmission: submissionId, returnUrl, idUserAnswer, @@ -165,18 +148,18 @@ WHERE tm_submissions.ID = :idSubmission and tm_submissions.idUser = :idUser and tm_submissions.idPlatform = :idPlatform and tm_submissions.idTask = :idTask;`, { - idUser: params.idUser, - idTask: params.idTaskLocal, - idPlatform: params.idPlatform, + idUser: taskTokenData.payload.idUser, + idTask: taskTokenData.idTaskLocal, + idPlatform: taskTokenData.platform.ID, idSubmission: submissionId, }); if (null === submission) { throw new InvalidInputError('Cannot find submission ' + submissionId); } - const task = await findTaskById(params.idTaskLocal); + const task = await findTaskById(taskTokenData.idTaskLocal); if (null === task) { - throw new InvalidInputError(`Cannot find task with id ${params.idTaskLocal}`); + throw new InvalidInputError(`Cannot find task with id ${taskTokenData.idTaskLocal}`); } const sourceCode = await findSourceCodeById(submission.idSourceCode); if (null === sourceCode) { @@ -190,9 +173,9 @@ WHERE tm_submissions.ID = :idSubmission let tests: TaskTest[] = []; if ('UserTest' === submission.sMode) { tests = await Db.execute('SELECT tm_tasks_tests.* FROM tm_tasks_tests WHERE idUser = :idUser and idPlatform = :idPlatform and idTask = :idTask and idSubmission = :idSubmission ORDER BY iRank ASC', { - idUser: params.idUser, - idTask: params.idTaskLocal, - idPlatform: params.idPlatform, + idUser: taskTokenData.payload.idUser, + idTask: taskTokenData.idTaskLocal, + idPlatform: taskTokenData.platform.ID, idSubmission: submissionId, }); } diff --git a/src/platform_interface.ts b/src/platform_interface.ts new file mode 100644 index 0000000..b461909 --- /dev/null +++ b/src/platform_interface.ts @@ -0,0 +1,143 @@ +import got from 'got'; +import { + decodePlatformTaskToken, PlatformAnswerTokenPayload, + PlatformGenericTokenPayload, + PlatformTaskTokenPayload, +} from './tokenization'; +import appConfig from './config'; +import * as Db from './db'; +import {Platform} from './db_models'; +import {InvalidInputError, PlatformInteractionError} from './error_handler'; + +export interface PlatformTaskTokenData { + payload: PlatformTaskTokenPayload, + idTaskLocal: string, + platform: Platform, +} + +export interface PlatformAnswerTokenData { + payload: PlatformAnswerTokenPayload, + idTaskLocal: string, + platform: Platform, +} + +export async function extractPlatformTaskTokenData(token: string|null|undefined, platformName: string|null|undefined, taskId: string|null = null): Promise { + const platformEntity = await getPlatformByName(platformName); + let payload: PlatformTaskTokenPayload; + try { + payload = await decodePlatformTaskToken(token, platformEntity); + } catch (e) { + if (appConfig.testMode.enabled && null !== taskId) { + payload = getTestTokenParameters(taskId); + } else { + throw e; + } + } + + if (!payload.idUser || (!payload.idItem && !payload.itemUrl)) { + throw new InvalidInputError('Missing idUser or idItem in token'); + } + + return { + payload: payload, + idTaskLocal: await getLocalIdTask(payload), + platform: platformEntity, + }; +} + +export async function extractPlatformAnswerTaskTokenData(token: string|null|undefined, platformName: string|null|undefined, taskId: string|null = null): Promise { + const platformEntity = await getPlatformByName(platformName); + let payload: PlatformAnswerTokenPayload; + try { + payload = await decodePlatformTaskToken(token, platformEntity); + } catch (e) { + if (appConfig.testMode.enabled && null !== taskId) { + payload = getTestTokenParameters(taskId); + } else { + throw e; + } + } + + if (!payload.idUser || (!payload.idItem && !payload.itemUrl)) { + throw new InvalidInputError('Missing idUser or idItem in token'); + } + + return { + payload: payload, + idTaskLocal: await getLocalIdTask(payload), + platform: platformEntity, + }; +} + +function getIdFromUrl(itemUrl: string): string|null { + const urlSearchParams = (new URL(itemUrl)).searchParams; + const params = Object.fromEntries(urlSearchParams.entries()); + + return params['taskId'] ? params['taskId'] : null; +} + +async function getLocalIdTask(params: PlatformGenericTokenPayload): Promise { + const idItem = params.idItem || null; + const itemUrl = params.itemUrl || null; + if (itemUrl) { + const id = getIdFromUrl(itemUrl); + if (!id) { + throw new InvalidInputError('Cannot find ID in url ' + itemUrl); + } + + return id; + } + + const id = await Db.querySingleScalarResult('SELECT ID FROM tm_tasks WHERE sTextId = ?', [idItem]); + if (!id) { + throw new InvalidInputError(`Cannot find task ${idItem || ''}`); + } + + return id; +} + +function getTestTokenParameters(taskId: string): PlatformTaskTokenPayload { + return { + idUser: appConfig.testMode.userId ? appConfig.testMode.userId : null, + bSubmissionPossible: true, + itemUrl: appConfig.baseUrl ? appConfig.baseUrl + '?taskId=' + taskId : null, + bAccessSolutions: appConfig.testMode.accessSolutions, + nbHintsGiven: appConfig.testMode.nbHintsGiven, + }; +} + +export async function askPlatformAnswerToken(taskToken: string, answer: string, platform: Platform): Promise { + const platformUrl = `${platform.api_url}/answers`; + + const askAnswerTokenRequest = { + answer, + task_token: taskToken, + }; + + const gotResponse = await got.post(platformUrl, { + headers: { + 'content-type': 'application/json', + Cookie: 'access_token=3!6iebvzan0hf3g5w8h34ang33c09pzrkm!beta.opentezos.com!/api/', + }, + json: askAnswerTokenRequest, + }).json<{success: boolean, data?: {answer_token: string}}>(); + + if (!gotResponse.success) { + throw new PlatformInteractionError('Impossible to fetch answer token from platform'); + } + + return gotResponse.data?.answer_token!; +} + +export async function getPlatformByName(platformName?: string|null): Promise { + if (!platformName && appConfig.testMode.enabled && appConfig.testMode.platformName) { + platformName = appConfig.testMode.platformName; + } + + const platforms = await Db.execute('SELECT * FROM tm_platforms WHERE name = ?', [platformName]); + if (!platforms.length) { + throw new InvalidInputError(`Cannot find platform ${platformName || ''}`); + } + + return platforms[0]; +} diff --git a/src/server.ts b/src/server.ts index 7346b5f..a4f3e3d 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,7 +1,7 @@ import Hapi, {Lifecycle} from '@hapi/hapi'; import {Server} from '@hapi/hapi'; import {getTask} from './tasks'; -import {createSubmission, getSubmission} from './submissions'; +import {createOfflineSubmission, createSubmission, getSubmission} from './submissions'; import ReturnValue = Lifecycle.ReturnValue; import {ErrorHandler, isResponseBoom, NotFoundError} from './error_handler'; import {receiveSubmissionResultsFromTaskGrader} from './grader_webhook'; @@ -52,6 +52,21 @@ export async function init(): Promise { } }); + server.route({ + method: 'POST', + path: '/submissions-offline', + options: { + handler: async (request, h) => { + const submissionId = await createOfflineSubmission(request.payload); + + return h.response({ + success: true, + submissionId, + }); + } + } + }); + server.route({ method: 'POST', path: '/task-grader-webhook', diff --git a/src/submissions.ts b/src/submissions.ts index 8be7692..a6018d6 100644 --- a/src/submissions.ts +++ b/src/submissions.ts @@ -1,12 +1,10 @@ import * as Db from './db'; import { - Platform, SourceCode, Submission, SubmissionSubtask, SubmissionTest, TaskTest, } from './db_models'; -import {decodePlatformToken, PlatformTokenParameters} from './tokenization'; import {decode, getRandomId} from './util'; import * as D from 'io-ts/Decoder'; import {pipe} from 'fp-ts/function'; @@ -15,6 +13,13 @@ import {sendSubmissionToTaskGrader} from './grader_interface'; import {findTaskById, normalizeTaskTest} from './tasks'; import {ProgramExecutionResultMetadata} from './grader_webhook'; import appConfig from './config'; +import { + askPlatformAnswerToken, + extractPlatformAnswerTaskTokenData, + extractPlatformTaskTokenData, + PlatformAnswerTokenData, + PlatformTaskTokenData, +} from './platform_interface'; export const submissionDataDecoder = pipe( D.struct({ @@ -43,6 +48,16 @@ export const submissionDataDecoder = pipe( ); export type SubmissionParameters = D.TypeOf; +export const offlineSubmissionDataDecoder = pipe( + D.struct({ + token: D.string, + platform: D.string, + answer: D.string, + // sLocale: D.string, + }), +); +export type OfflineSubmissionParameters = D.TypeOf; + export enum SubmissionMode { Submitted = 'Submitted', LimitedTime = 'LimitedTime', @@ -111,59 +126,22 @@ export interface SubmissionOutput extends SubmissionNormalized { tests?: SubmissionTestNormalized[], } -export async function getPlatformTokenParams(taskId: string, token?: string|null, platform?: string|null): Promise { - if (!platform && appConfig.testMode.enabled && appConfig.testMode.platformName) { - platform = appConfig.testMode.platformName; - } - - const platforms = await Db.execute('SELECT ID, public_key FROM tm_platforms WHERE name = ?', [platform]); - if (!platforms.length) { - throw new InvalidInputError(`Cannot find platform ${platform || ''}`); - } - - const platformEntity = platforms[0]; - const platformKey = platformEntity.public_key; - - const params = decodePlatformToken(token, platformKey, platform as string, taskId, platformEntity); - - if (!params.idUser || (!params.idItem && !params.itemUrl)) { - // console.error('Missing idUser or idItem in token', params); - throw new InvalidInputError('Missing idUser or idItem in token'); - } - - params.idPlatform = platformEntity.ID; - params.idTaskLocal = await getLocalIdTask(params); +export async function createOfflineSubmission(submissionDataPayload: unknown): Promise { + const submissionData: OfflineSubmissionParameters = decode(offlineSubmissionDataDecoder)(submissionDataPayload); + const tokenData = await extractPlatformTaskTokenData(submissionData.token, submissionData.platform); - return params; -} + // Get answer token from platform + const answerToken = await askPlatformAnswerToken(submissionData.token, submissionData.answer, tokenData.platform); -function getIdFromUrl(itemUrl: string): string|null { - const urlSearchParams = (new URL(itemUrl)).searchParams; - const params = Object.fromEntries(urlSearchParams.entries()); + // console.log('answer token', answerToken); + // Create submission with answer token - return params['taskId'] ? params['taskId'] : null; -} - -async function getLocalIdTask(params: PlatformTokenParameters): Promise { - const idItem = params.idItem || null; - const itemUrl = params.itemUrl || null; - if (itemUrl) { - const id = getIdFromUrl(itemUrl); - if (!id) { - throw new InvalidInputError('Cannot find ID in url ' + itemUrl); - } - - return id; - } - - const id = await Db.querySingleScalarResult('SELECT ID FROM tm_tasks WHERE sTextId = ?', [idItem]); - if (!id) { - throw new InvalidInputError(`Cannot find task ${idItem || ''}`); - } - - return id; + return answerToken; } +// This requires a token and an answer token, except if: +// - we execute user tests (it's not a submission) +// - or the test mode is enabled export async function createSubmission(submissionDataPayload: unknown): Promise { const submissionData: SubmissionParameters = decode(submissionDataDecoder)(submissionDataPayload); @@ -171,12 +149,19 @@ export async function createSubmission(submissionDataPayload: unknown): Promise< throw new InvalidInputError('Missing token or platform POST variable'); } - const params = await getPlatformTokenParams(submissionData.taskId, submissionData.token, submissionData.platform); - const task = await findTaskById(params.idTaskLocal); + const taskTokenData = await extractPlatformTaskTokenData(submissionData.token, submissionData.platform, submissionData.taskId); + const task = await findTaskById(taskTokenData.idTaskLocal); if (null === task) { - throw new InvalidInputError(`Invalid task id: ${params.idTaskLocal}`); + throw new InvalidInputError(`Invalid task id: ${taskTokenData.idTaskLocal}`); + } + + let answerTokenData: PlatformAnswerTokenData|null = null; + if (submissionData.answerToken) { + answerTokenData = await extractPlatformAnswerTaskTokenData(submissionData.answerToken, submissionData.platform, submissionData.taskId); } + checkAnswerTokenValid(answerTokenData, taskTokenData); + const mode = submissionData.userTests && submissionData.userTests.length ? SubmissionMode.UserTest : SubmissionMode.Submitted; // save source code (with bSubmission = 1) @@ -189,9 +174,9 @@ export async function createSubmission(submissionDataPayload: unknown): Promise< await Db.transactional(async connection => { await Db.executeInConnection(connection, "insert into tm_source_codes (ID, idUser, idPlatform, idTask, sDate, sParams, sName, sSource, bEditable, bSubmission) values(:idNewSC, :idUser, :idPlatform, :idTask, NOW(), :sParams, :sName, :sSource, '0', '1');", { idNewSC: idNewSourceCode, - idUser: params.idUser, - idPlatform: params.idPlatform, - idTask: params.idTaskLocal, + idUser: taskTokenData.payload.idUser, + idPlatform: taskTokenData.platform.ID, + idTask: taskTokenData.idTaskLocal, sParams: sourceCodeParams, sName: submissionData.answer.fileName ?? idSubmission, sSource: submissionData.answer.sourceCode @@ -199,9 +184,9 @@ export async function createSubmission(submissionDataPayload: unknown): Promise< await Db.executeInConnection(connection, 'insert into tm_submissions (ID, idUser, idPlatform, idTask, sDate, idSourceCode, sMode) values(:idSubmission, :idUser, :idPlatform, :idTask, NOW(), :idSourceCode, :sMode);', { idSubmission, - idUser: params.idUser, - idPlatform: params.idPlatform, - idTask: params.idTaskLocal, + idUser: taskTokenData.payload.idUser, + idPlatform: taskTokenData.platform.ID, + idTask: taskTokenData.idTaskLocal, idSourceCode: idNewSourceCode, sMode: mode, }); @@ -210,9 +195,9 @@ export async function createSubmission(submissionDataPayload: unknown): Promise< if ('UserTest' === mode && submissionData.userTests && submissionData.userTests.length) { const valuesToInsert = submissionData.userTests.map((test, index) => ([ testPrefixId + '' + String(index), - params.idUser, - params.idPlatform, - params.idTaskLocal, + taskTokenData.payload.idUser, + taskTokenData.platform.ID, + taskTokenData.idTaskLocal, 'User', test.input, test.output, @@ -226,11 +211,25 @@ export async function createSubmission(submissionDataPayload: unknown): Promise< } }); - await sendSubmissionToTaskGrader(idSubmission, submissionData); + await sendSubmissionToTaskGrader(idSubmission, submissionData, taskTokenData, answerTokenData); return idSubmission; } +export function checkAnswerTokenValid(answerTokenData: PlatformAnswerTokenData|null = null, taskTokenData: PlatformTaskTokenData): void { + if (answerTokenData && !appConfig.testMode.enabled) { + if (answerTokenData.payload.idUser !== taskTokenData.payload.idUser || answerTokenData.payload.itemUrl !== taskTokenData.payload.itemUrl) { + throw new InvalidInputError(`Mismatching tokens idUser or itemUrl, token = ${JSON.stringify(taskTokenData)}, answerToken = ${JSON.stringify(answerTokenData)}`); + } + if (!answerTokenData.payload.sAnswer) { + throw new InvalidInputError('Missing answer in answerToken'); + } + if (false === taskTokenData.payload.bSubmissionPossible || false === taskTokenData.payload.bAllowGrading) { + throw new InvalidInputError('Token indicates read-only task'); + } + } +} + export async function findSourceCodeById(sourceCodeId: string): Promise { return await Db.querySingleResult('SELECT * FROM tm_source_codes WHERE ID = ?', [sourceCodeId]); } diff --git a/src/tokenization.ts b/src/tokenization.ts index 20c59e9..fe47add 100644 --- a/src/tokenization.ts +++ b/src/tokenization.ts @@ -2,75 +2,52 @@ import {Platform} from './db_models'; import * as jose from 'jose'; import {KeyLike} from 'jose/dist/types/types'; import moment from 'moment'; -import appConfig from './config'; - -export interface PlatformTokenParameters { - idUser: string|null, - bSubmissionPossible?: boolean, - bAllowGrading?: boolean, - idTaskLocal: string, - itemUrl: string|null, - bAccessSolutions: boolean, - nbHintsGiven: number, - idPlatform?: string, - idItem?: string, - idUserAnswer?: string, - sAnswer?: string, - returnUrl?: string, +import {JwesDecoder} from './crypto/jwes_decoder'; +import {InvalidInputError} from './error_handler'; +import * as Db from './db'; + +export interface PlatformGenericTokenPayload { + idUser: string|null, + itemUrl: string|null, + nbHintsGiven: number, + idItem?: string, + platformName?: string, + type?: string, + secret?: string, } -function getTestTokenParameters(taskId: string): PlatformTokenParameters { - return { - idUser: appConfig.testMode.userId ? appConfig.testMode.userId : null, - bSubmissionPossible: true, - idTaskLocal: taskId, - itemUrl: appConfig.baseUrl ? appConfig.baseUrl + '?taskId=' + taskId : null, - bAccessSolutions: appConfig.testMode.accessSolutions, - nbHintsGiven: appConfig.testMode.nbHintsGiven, - }; +export interface PlatformTaskTokenPayload extends PlatformGenericTokenPayload { + bSubmissionPossible?: boolean, + bAllowGrading?: boolean, + bAccessSolutions: boolean, } -// eslint-disable-next-line -export function decodePlatformToken(token: string|null|undefined, platformKey: string, keyName: string, askedTaskId: string, platform: Platform): PlatformTokenParameters { - // const JWKS = jose.createLocalJWKSet({ - // keys: [ - // { - // kty: 'RSA', - // e: 'AQAB', - // n: '12oBZRhCiZFJLcPg59LkZZ9mdhSMTKAQZYq32k_ti5SBB6jerkh-WzOMAO664r_qyLkqHUSp3u5SbXtseZEpN3XPWGKSxjsy-1JyEFTdLSYe6f9gfrmxkUF_7DTpq0gn6rntP05g2-wFW50YO7mosfdslfrTJYWHFhJALabAeYirYD7-9kqq9ebfFMF4sRRELbv9oi36As6Q9B3Qb5_C1rAzqfao_PCsf9EPsTZsVVVkA5qoIAr47lo1ipfiBPxUCCNSdvkmDTYgvvRm6ZoMjFbvOtgyts55fXKdMWv7I9HMD5HwE9uW839PWA514qhbcIsXEYSFMPMV6fnlsiZvQQ', - // alg: 'PS256', - // }, - // { - // crv: 'P-256', - // kty: 'EC', - // x: 'ySK38C1jBdLwDsNWKzzBHqKYEE5Cgv-qjWvorUXk9fw', - // y: '_LeQBw07cf5t57Iavn4j-BqJsAD1dpoz8gokd3sBsOo', - // alg: 'ES256', - // }, - // ], - // }) - - // const { payload, protectedHeader } = await jose.jwtVerify(token, JWKS, { - // issuer: 'urn:example:issuer', - // audience: 'urn:example:audience', - // }) - // console.log(protectedHeader) - // console.log(payload) - - try { - // TODO: implement token parsing - // $params = $tokenParser->decodeJWS($sToken); - // if (isset($params['type']) && $params['type'] == 'long') { - // checkLongToken($params, $sPlatform['ID']); - // } - - throw new Error('Reading token is not implemented yet'); - } catch (e) { - if (appConfig.testMode.enabled) { - return getTestTokenParameters(askedTaskId); - } else { - throw e; - } +export interface PlatformAnswerTokenPayload extends PlatformGenericTokenPayload { + idUserAnswer?: string, + sAnswer?: string, +} + +export async function decodePlatformTaskToken(token: string|null|undefined, platform: Platform): Promise { + const jwesDecoder = new JwesDecoder(); + await jwesDecoder.setKeys(platform.public_key); + const tokenParams = await jwesDecoder.checkJwsSignature(token!) as T; + if ('long' === tokenParams?.type) { + await checkLongToken(tokenParams, platform); + } + + return tokenParams; +} + +async function checkLongToken(params: PlatformGenericTokenPayload, platform: Platform): Promise { + const remoteSecret = await Db.querySingleScalarResult( + 'SELECT sRemoteSecret from tm_remote_secret WHERE idUser = ? and idPlatform = ?', + [params.idUser, platform.ID] + ); + if (!remoteSecret) { + throw new InvalidInputError(`Cannot find secret for user ${String(params.idUser)} and platform ${platform.ID}`); + } + if (remoteSecret !== params.secret) { + throw new InvalidInputError(`Remote secret does not match for user ${String(params.idUser)} and platform ${platform.ID}`); } }