From 04aa2ce517e7f6f30af4a9e62c057dd561ef73ea Mon Sep 17 00:00:00 2001 From: Jonas Lochmann Date: Mon, 12 Sep 2022 02:00:00 +0200 Subject: [PATCH] Add identity tokens --- docs/api/parent.md | 25 +++++ docs/schema/README.md | 2 + .../RequestIdentityTokenRequest.schema.json | 31 +++++ ...requestidentitytokenrequest-definitions.md | 15 +++ ...tokenrequest-properties-deviceauthtoken.md | 15 +++ ...est-properties-parentpasswordsecondhash.md | 15 +++ ...itytokenrequest-properties-parentuserid.md | 15 +++ ...identitytokenrequest-properties-purpose.md | 23 ++++ docs/schema/requestidentitytokenrequest.md | 106 ++++++++++++++++++ docs/usage/configuration.md | 3 + package-lock.json | 14 +++ package.json | 1 + scripts/build-schemas.js | 3 +- src/api/parent.ts | 48 ++++++-- src/api/schema.ts | 7 ++ src/api/validator.ts | 31 ++++- src/config.ts | 6 +- src/util/identity-token.ts | 41 +++++++ 18 files changed, 390 insertions(+), 11 deletions(-) create mode 100644 docs/schema/RequestIdentityTokenRequest.schema.json create mode 100644 docs/schema/requestidentitytokenrequest-definitions.md create mode 100644 docs/schema/requestidentitytokenrequest-properties-deviceauthtoken.md create mode 100644 docs/schema/requestidentitytokenrequest-properties-parentpasswordsecondhash.md create mode 100644 docs/schema/requestidentitytokenrequest-properties-parentuserid.md create mode 100644 docs/schema/requestidentitytokenrequest-properties-purpose.md create mode 100644 docs/schema/requestidentitytokenrequest.md create mode 100644 src/util/identity-token.ts diff --git a/docs/api/parent.md b/docs/api/parent.md index 01033f0..b1c7d1f 100644 --- a/docs/api/parent.md +++ b/docs/api/parent.md @@ -169,3 +169,28 @@ If there is no device with the specified ``deviceId``: HTTP status code 409 Conf If the ``secondPasswordHash`` is invalid: HTTP status code 401 Unauthorized On success: ``{"ok": true}`` + +## POST /parent/create-identity-token + +Use this to get a identity token. +This can be used to inform the server operator about ones user account. + +### request + +see [this JSON schema](../schema/requestidentitytokenrequest.md) + +in case of a device used by a parent with disabled password checks, use ``device`` as ``secondPasswordHash`` + +## response + +On a invalid request body: HTTP status code 400 Bad Request + +If the device auth token is invalid: HTTP status code 401 Unauthorized + +If there is no device with the specified ``deviceId``: HTTP status code 409 Conflict + +If the ``secondPasswordHash`` is invalid: HTTP status code 401 Unauthorized + +If the server does not support this request: HTTP status code 404 + +On success: ``{"token": "some string"}``; you should not make any assumptions about the token string diff --git a/docs/schema/README.md b/docs/schema/README.md index 54684c1..2ae14f4 100644 --- a/docs/schema/README.md +++ b/docs/schema/README.md @@ -26,6 +26,8 @@ * [RemoveDeviceRequest](./removedevicerequest.md) – `https://timelimit.io/RemoveDeviceRequest` +* [RequestIdentityTokenRequest](./requestidentitytokenrequest.md) – `https://timelimit.io/RequestIdentityTokenRequest` + * [RequestWithAuthToken](./requestwithauthtoken.md) – `https://timelimit.io/RequestWithAuthToken` * [SendMailLoginCodeRequest](./sendmaillogincoderequest.md) – `https://timelimit.io/SendMailLoginCodeRequest` diff --git a/docs/schema/RequestIdentityTokenRequest.schema.json b/docs/schema/RequestIdentityTokenRequest.schema.json new file mode 100644 index 0000000..afc8a71 --- /dev/null +++ b/docs/schema/RequestIdentityTokenRequest.schema.json @@ -0,0 +1,31 @@ +{ + "type": "object", + "properties": { + "deviceAuthToken": { + "type": "string" + }, + "parentUserId": { + "type": "string" + }, + "parentPasswordSecondHash": { + "type": "string" + }, + "purpose": { + "type": "string", + "enum": [ + "purchase" + ] + } + }, + "additionalProperties": false, + "required": [ + "deviceAuthToken", + "parentPasswordSecondHash", + "parentUserId", + "purpose" + ], + "definitions": {}, + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "RequestIdentityTokenRequest", + "$id": "https://timelimit.io/RequestIdentityTokenRequest" +} \ No newline at end of file diff --git a/docs/schema/requestidentitytokenrequest-definitions.md b/docs/schema/requestidentitytokenrequest-definitions.md new file mode 100644 index 0000000..5584c93 --- /dev/null +++ b/docs/schema/requestidentitytokenrequest-definitions.md @@ -0,0 +1,15 @@ +# Untitled undefined type in RequestIdentityTokenRequest Schema + +```txt +https://timelimit.io/RequestIdentityTokenRequest#/definitions +``` + + + +| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In | +| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :---------------------------------------------------------------------------------------------------------- | +| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [RequestIdentityTokenRequest.schema.json\*](RequestIdentityTokenRequest.schema.json "open original schema") | + +## definitions Type + +unknown diff --git a/docs/schema/requestidentitytokenrequest-properties-deviceauthtoken.md b/docs/schema/requestidentitytokenrequest-properties-deviceauthtoken.md new file mode 100644 index 0000000..7158bd5 --- /dev/null +++ b/docs/schema/requestidentitytokenrequest-properties-deviceauthtoken.md @@ -0,0 +1,15 @@ +# Untitled string in RequestIdentityTokenRequest Schema + +```txt +https://timelimit.io/RequestIdentityTokenRequest#/properties/deviceAuthToken +``` + + + +| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In | +| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :---------------------------------------------------------------------------------------------------------- | +| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [RequestIdentityTokenRequest.schema.json\*](RequestIdentityTokenRequest.schema.json "open original schema") | + +## deviceAuthToken Type + +`string` diff --git a/docs/schema/requestidentitytokenrequest-properties-parentpasswordsecondhash.md b/docs/schema/requestidentitytokenrequest-properties-parentpasswordsecondhash.md new file mode 100644 index 0000000..3dfb482 --- /dev/null +++ b/docs/schema/requestidentitytokenrequest-properties-parentpasswordsecondhash.md @@ -0,0 +1,15 @@ +# Untitled string in RequestIdentityTokenRequest Schema + +```txt +https://timelimit.io/RequestIdentityTokenRequest#/properties/parentPasswordSecondHash +``` + + + +| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In | +| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :---------------------------------------------------------------------------------------------------------- | +| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [RequestIdentityTokenRequest.schema.json\*](RequestIdentityTokenRequest.schema.json "open original schema") | + +## parentPasswordSecondHash Type + +`string` diff --git a/docs/schema/requestidentitytokenrequest-properties-parentuserid.md b/docs/schema/requestidentitytokenrequest-properties-parentuserid.md new file mode 100644 index 0000000..d7ae895 --- /dev/null +++ b/docs/schema/requestidentitytokenrequest-properties-parentuserid.md @@ -0,0 +1,15 @@ +# Untitled string in RequestIdentityTokenRequest Schema + +```txt +https://timelimit.io/RequestIdentityTokenRequest#/properties/parentUserId +``` + + + +| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In | +| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :---------------------------------------------------------------------------------------------------------- | +| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [RequestIdentityTokenRequest.schema.json\*](RequestIdentityTokenRequest.schema.json "open original schema") | + +## parentUserId Type + +`string` diff --git a/docs/schema/requestidentitytokenrequest-properties-purpose.md b/docs/schema/requestidentitytokenrequest-properties-purpose.md new file mode 100644 index 0000000..7c5d6ec --- /dev/null +++ b/docs/schema/requestidentitytokenrequest-properties-purpose.md @@ -0,0 +1,23 @@ +# Untitled string in RequestIdentityTokenRequest Schema + +```txt +https://timelimit.io/RequestIdentityTokenRequest#/properties/purpose +``` + + + +| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In | +| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :---------------------------------------------------------------------------------------------------------- | +| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [RequestIdentityTokenRequest.schema.json\*](RequestIdentityTokenRequest.schema.json "open original schema") | + +## purpose Type + +`string` + +## purpose Constraints + +**enum**: the value of this property must be equal to one of the following values: + +| Value | Explanation | +| :----------- | :---------- | +| `"purchase"` | | diff --git a/docs/schema/requestidentitytokenrequest.md b/docs/schema/requestidentitytokenrequest.md new file mode 100644 index 0000000..fba3ee5 --- /dev/null +++ b/docs/schema/requestidentitytokenrequest.md @@ -0,0 +1,106 @@ +# RequestIdentityTokenRequest Schema + +```txt +https://timelimit.io/RequestIdentityTokenRequest +``` + + + +| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In | +| :------------------ | :--------- | :------------- | :----------- | :---------------- | :-------------------- | :------------------ | :-------------------------------------------------------------------------------------------------------- | +| Can be instantiated | Yes | Unknown status | No | Forbidden | Forbidden | none | [RequestIdentityTokenRequest.schema.json](RequestIdentityTokenRequest.schema.json "open original schema") | + +## RequestIdentityTokenRequest Type + +`object` ([RequestIdentityTokenRequest](requestidentitytokenrequest.md)) + +# RequestIdentityTokenRequest Properties + +| Property | Type | Required | Nullable | Defined by | +| :---------------------------------------------------- | :------- | :------- | :------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [deviceAuthToken](#deviceauthtoken) | `string` | Required | cannot be null | [RequestIdentityTokenRequest](requestidentitytokenrequest-properties-deviceauthtoken.md "https://timelimit.io/RequestIdentityTokenRequest#/properties/deviceAuthToken") | +| [parentUserId](#parentuserid) | `string` | Required | cannot be null | [RequestIdentityTokenRequest](requestidentitytokenrequest-properties-parentuserid.md "https://timelimit.io/RequestIdentityTokenRequest#/properties/parentUserId") | +| [parentPasswordSecondHash](#parentpasswordsecondhash) | `string` | Required | cannot be null | [RequestIdentityTokenRequest](requestidentitytokenrequest-properties-parentpasswordsecondhash.md "https://timelimit.io/RequestIdentityTokenRequest#/properties/parentPasswordSecondHash") | +| [purpose](#purpose) | `string` | Required | cannot be null | [RequestIdentityTokenRequest](requestidentitytokenrequest-properties-purpose.md "https://timelimit.io/RequestIdentityTokenRequest#/properties/purpose") | + +## deviceAuthToken + + + +`deviceAuthToken` + +* is required + +* Type: `string` + +* cannot be null + +* defined in: [RequestIdentityTokenRequest](requestidentitytokenrequest-properties-deviceauthtoken.md "https://timelimit.io/RequestIdentityTokenRequest#/properties/deviceAuthToken") + +### deviceAuthToken Type + +`string` + +## parentUserId + + + +`parentUserId` + +* is required + +* Type: `string` + +* cannot be null + +* defined in: [RequestIdentityTokenRequest](requestidentitytokenrequest-properties-parentuserid.md "https://timelimit.io/RequestIdentityTokenRequest#/properties/parentUserId") + +### parentUserId Type + +`string` + +## parentPasswordSecondHash + + + +`parentPasswordSecondHash` + +* is required + +* Type: `string` + +* cannot be null + +* defined in: [RequestIdentityTokenRequest](requestidentitytokenrequest-properties-parentpasswordsecondhash.md "https://timelimit.io/RequestIdentityTokenRequest#/properties/parentPasswordSecondHash") + +### parentPasswordSecondHash Type + +`string` + +## purpose + + + +`purpose` + +* is required + +* Type: `string` + +* cannot be null + +* defined in: [RequestIdentityTokenRequest](requestidentitytokenrequest-properties-purpose.md "https://timelimit.io/RequestIdentityTokenRequest#/properties/purpose") + +### purpose Type + +`string` + +### purpose Constraints + +**enum**: the value of this property must be equal to one of the following values: + +| Value | Explanation | +| :----------- | :---------- | +| `"purchase"` | | + +# RequestIdentityTokenRequest Definitions diff --git a/docs/usage/configuration.md b/docs/usage/configuration.md index a924302..a05c22c 100644 --- a/docs/usage/configuration.md +++ b/docs/usage/configuration.md @@ -54,3 +54,6 @@ - PING_INTERVAL_SEC - ping interval at the websocket in seconds - the default value is ``25`` +- SIGN_SECRET + - used for signing tokens + - if not set or set to an empty string, then the features that depend on it are disabled diff --git a/package-lock.json b/package-lock.json index f695bbf..295fa13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "email-addresses": "^3.1.0", "express": "^4.17.1", "http-errors": "^1.8.0", + "jose": "^4.9.3", "lodash": "^4.17.21", "mariadb": "^2.5.2", "nodemailer": "^6.7.2", @@ -2333,6 +2334,14 @@ "node": ">=10" } }, + "node_modules/jose": { + "version": "4.9.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.9.3.tgz", + "integrity": "sha512-f8E/z+T3Q0kA9txzH2DKvH/ds2uggcw0m3vVPSB9HrSkrQ7mojjifvS7aR8cw+lQl2Fcmx9npwaHpM/M3GD8UQ==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -7071,6 +7080,11 @@ "minimatch": "^3.0.4" } }, + "jose": { + "version": "4.9.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.9.3.tgz", + "integrity": "sha512-f8E/z+T3Q0kA9txzH2DKvH/ds2uggcw0m3vVPSB9HrSkrQ7mojjifvS7aR8cw+lQl2Fcmx9npwaHpM/M3GD8UQ==" + }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", diff --git a/package.json b/package.json index 44b5c85..c8ec126 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "email-addresses": "^3.1.0", "express": "^4.17.1", "http-errors": "^1.8.0", + "jose": "^4.9.3", "lodash": "^4.17.21", "mariadb": "^2.5.2", "nodemailer": "^6.7.2", diff --git a/scripts/build-schemas.js b/scripts/build-schemas.js index 5241511..295b3de 100644 --- a/scripts/build-schemas.js +++ b/scripts/build-schemas.js @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 - 2021 Jonas Lochmann + * Copyright (C) 2019 - 2022 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -39,6 +39,7 @@ const types = [ 'LinkParentMailAddressRequest', 'UpdatePrimaryDeviceRequest', 'RemoveDeviceRequest', + 'RequestIdentityTokenRequest', 'RequestWithAuthToken', 'SendMailLoginCodeRequest', 'SignInByMailCodeRequest' diff --git a/src/api/parent.ts b/src/api/parent.ts index 45b1ac8..050d651 100644 --- a/src/api/parent.ts +++ b/src/api/parent.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 - 2021 Jonas Lochmann + * Copyright (C) 2019 - 2022 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -27,12 +27,13 @@ import { getStatusByMailToken } from '../function/parent/get-status-by-mail-addr import { linkMailAddress } from '../function/parent/link-mail-address' import { recoverParentPassword } from '../function/parent/recover-parent-password' import { signInIntoFamily } from '../function/parent/sign-in-into-family' +import { createIdentityToken, MissingSignSecretException } from '../util/identity-token' import { WebsocketApi } from '../websocket' import { isCreateFamilyByMailTokenRequest, isCreateRegisterDeviceTokenRequest, isLinkParentMailAddressRequest, isMailAuthTokenRequestBody, isRecoverParentPasswordRequest, - isRemoveDeviceRequest, isSignIntoFamilyRequest + isRemoveDeviceRequest, isSignIntoFamilyRequest, isRequestIdentityTokenRequest } from './validator' export const createParentRouter = ({ database, websocket }: {database: Database, websocket: WebsocketApi}) => { @@ -131,7 +132,7 @@ export const createParentRouter = ({ database, websocket }: {database: Database, } }) - async function assertAuthValidAndReturnDeviceEntry ({ deviceAuthToken, parentId, secondPasswordHash, transaction }: { + async function assertAuthValidAndReturnDetails ({ deviceAuthToken, parentId, secondPasswordHash, transaction }: { deviceAuthToken: string parentId: string secondPasswordHash: string @@ -165,6 +166,8 @@ export const createParentRouter = ({ database, websocket }: {database: Database, if (!parentEntry) { throw new Unauthorized() } + + return { deviceEntry, parentEntry } } else { const parentEntry = await database.user.findOne({ where: { @@ -179,9 +182,9 @@ export const createParentRouter = ({ database, websocket }: {database: Database, if (!parentEntry) { throw new Unauthorized() } - } - return deviceEntry + return { deviceEntry, parentEntry } + } } router.post('/create-add-device-token', json(), async (req, res, next) => { @@ -191,7 +194,7 @@ export const createParentRouter = ({ database, websocket }: {database: Database, } const { token, deviceId } = await database.transaction(async (transaction) => { - const deviceEntry = await assertAuthValidAndReturnDeviceEntry({ + const { deviceEntry } = await assertAuthValidAndReturnDetails({ deviceAuthToken: req.body.deviceAuthToken, parentId: req.body.parentId, secondPasswordHash: req.body.parentPasswordSecondHash, @@ -235,7 +238,7 @@ export const createParentRouter = ({ database, websocket }: {database: Database, } await database.transaction(async (transaction) => { - const deviceEntry = await assertAuthValidAndReturnDeviceEntry({ + const { deviceEntry } = await assertAuthValidAndReturnDetails({ deviceAuthToken: req.body.deviceAuthToken, parentId: req.body.parentUserId, secondPasswordHash: req.body.parentPasswordSecondHash, @@ -257,5 +260,36 @@ export const createParentRouter = ({ database, websocket }: {database: Database, } }) + router.post('/create-identity-token', json(), async (req, res, next) => { + try { + if (!isRequestIdentityTokenRequest(req.body)) { + throw new BadRequest() + } + + const body = req.body + + await database.transaction(async (transaction) => { + const { deviceEntry, parentEntry } = await assertAuthValidAndReturnDetails({ + deviceAuthToken: body.deviceAuthToken, + parentId: body.parentUserId, + secondPasswordHash: body.parentPasswordSecondHash, + transaction + }) + + const token = await createIdentityToken({ + purpose: body.purpose, + familyId: deviceEntry.familyId, + userId: parentEntry.userId, + mail: parentEntry.mail + }) + + res.json({ token }) + }) + } catch (ex) { + if (ex instanceof MissingSignSecretException) res.sendStatus(404) + else next(ex) + } + }) + return router } diff --git a/src/api/schema.ts b/src/api/schema.ts index 9012f9f..eba769d 100644 --- a/src/api/schema.ts +++ b/src/api/schema.ts @@ -140,6 +140,13 @@ export interface RemoveDeviceRequest { deviceId: string } +export interface RequestIdentityTokenRequest { + deviceAuthToken: string + parentUserId: string + parentPasswordSecondHash: string + purpose: 'purchase' +} + export interface RequestWithAuthToken { deviceAuthToken: string } diff --git a/src/api/validator.ts b/src/api/validator.ts index 5666631..3ff5d5a 100644 --- a/src/api/validator.ts +++ b/src/api/validator.ts @@ -1,5 +1,5 @@ // tslint:disable -import { ClientPushChangesRequest, ClientPullChangesRequest, MailAuthTokenRequestBody, CreateFamilyByMailTokenRequest, SignIntoFamilyRequest, RecoverParentPasswordRequest, RegisterChildDeviceRequest, SerializedParentAction, SerializedAppLogicAction, SerializedChildAction, CreateRegisterDeviceTokenRequest, CanDoPurchaseRequest, FinishPurchaseByGooglePlayRequest, LinkParentMailAddressRequest, UpdatePrimaryDeviceRequest, RemoveDeviceRequest, RequestWithAuthToken, SendMailLoginCodeRequest, SignInByMailCodeRequest } from './schema' +import { ClientPushChangesRequest, ClientPullChangesRequest, MailAuthTokenRequestBody, CreateFamilyByMailTokenRequest, SignIntoFamilyRequest, RecoverParentPasswordRequest, RegisterChildDeviceRequest, SerializedParentAction, SerializedAppLogicAction, SerializedChildAction, CreateRegisterDeviceTokenRequest, CanDoPurchaseRequest, FinishPurchaseByGooglePlayRequest, LinkParentMailAddressRequest, UpdatePrimaryDeviceRequest, RemoveDeviceRequest, RequestIdentityTokenRequest, RequestWithAuthToken, SendMailLoginCodeRequest, SignInByMailCodeRequest } from './schema' import Ajv from 'ajv' const ajv = new Ajv() @@ -3253,6 +3253,35 @@ export const isRemoveDeviceRequest: (value: unknown) => value is RemoveDeviceReq "definitions": definitions, "$schema": "http://json-schema.org/draft-07/schema#" }) +export const isRequestIdentityTokenRequest: (value: unknown) => value is RequestIdentityTokenRequest = ajv.compile({ + "type": "object", + "properties": { + "deviceAuthToken": { + "type": "string" + }, + "parentUserId": { + "type": "string" + }, + "parentPasswordSecondHash": { + "type": "string" + }, + "purpose": { + "type": "string", + "enum": [ + "purchase" + ] + } + }, + "additionalProperties": false, + "required": [ + "deviceAuthToken", + "parentPasswordSecondHash", + "parentUserId", + "purpose" + ], + "definitions": definitions, + "$schema": "http://json-schema.org/draft-07/schema#" +}) export const isRequestWithAuthToken: (value: unknown) => value is RequestWithAuthToken = ajv.compile({ "type": "object", "properties": { diff --git a/src/config.ts b/src/config.ts index 837581a..53b815a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 - 2020 Jonas Lochmann + * Copyright (C) 2019 - 2022 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -21,6 +21,7 @@ interface Config { disableSignup: boolean pingInterval: number alwaysPro: boolean + signSecret: string } function parseYesNo (value: string) { @@ -37,7 +38,8 @@ export const config: Config = { mailWhitelist: (process.env.MAIL_WHITELIST || '').split(',').map((item) => item.trim()).filter((item) => item.length > 0), disableSignup: parseYesNo(process.env.DISABLE_SIGNUP || 'no'), pingInterval: parseInt(process.env.PING_INTERVAL_SEC || '25', 10) * 1000, - alwaysPro: process.env.ALWAYS_PRO ? parseYesNo(process.env.ALWAYS_PRO) : false + alwaysPro: process.env.ALWAYS_PRO ? parseYesNo(process.env.ALWAYS_PRO) : false, + signSecret: process.env.SIGN_SECRET || '' } class ParseYesNoException extends Error {} diff --git a/src/util/identity-token.ts b/src/util/identity-token.ts new file mode 100644 index 0000000..1704ba3 --- /dev/null +++ b/src/util/identity-token.ts @@ -0,0 +1,41 @@ +/* + * server component for the TimeLimit App + * Copyright (C) 2019 - 2022 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { SignJWT } from 'jose' +import { config } from '../config' + +export async function createIdentityToken({ purpose, familyId, userId, mail }: { + purpose: string + familyId: string + userId: string + mail: string +}) { + if (config.signSecret === '') throw new MissingSignSecretException() + + const jwt = await new SignJWT({ purpose, familyId, userId, mail }) + .setExpirationTime('7d') + .setProtectedHeader({ alg: 'HS512' }) + .sign(Buffer.from(config.signSecret, 'utf8')) + + return Buffer.from(jwt, 'ascii') + .toString('base64') + .split(/(.{32})/) + .filter((item) => item.length > 0) + .join('\n') +} + +export class MissingSignSecretException extends Error {}