Add identity tokens

This commit is contained in:
Jonas Lochmann
2022-09-12 02:00:00 +02:00
parent a86a0abb05
commit 04aa2ce517
18 changed files with 390 additions and 11 deletions
+41 -7
View File
@@ -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
}
+7
View File
@@ -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
}
+30 -1
View File
@@ -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": {
+4 -2
View File
@@ -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 {}
+41
View File
@@ -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 <https://www.gnu.org/licenses/>.
*/
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 {}