mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
Merge pull request #3555 from gradido/federation_handshake_own_table
feat(federation): use own table for handshake state
This commit is contained in:
commit
974fe032e8
@ -1,78 +1,110 @@
|
||||
import { CommunityLoggingView, Community as DbCommunity, FederatedCommunity as DbFederatedCommunity, FederatedCommunityLoggingView, getHomeCommunity } from 'database'
|
||||
import { validate as validateUUID, version as versionUUID } from 'uuid'
|
||||
import {
|
||||
CommunityHandshakeState as DbCommunityHandshakeState,
|
||||
CommunityHandshakeStateLoggingView,
|
||||
FederatedCommunity as DbFederatedCommunity,
|
||||
findPendingCommunityHandshake,
|
||||
getHomeCommunityWithFederatedCommunityOrFail,
|
||||
CommunityHandshakeStateType,
|
||||
getCommunityByPublicKeyOrFail,
|
||||
} from 'database'
|
||||
import { randombytes_random } from 'sodium-native'
|
||||
import { CONFIG as CONFIG_CORE } from 'core'
|
||||
|
||||
import { AuthenticationClient as V1_0_AuthenticationClient } from '@/federation/client/1_0/AuthenticationClient'
|
||||
import { ensureUrlEndsWithSlash } from 'core'
|
||||
import { ensureUrlEndsWithSlash, getFederatedCommunityWithApiOrFail } from 'core'
|
||||
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import { encryptAndSign, OpenConnectionJwtPayloadType } from 'shared'
|
||||
import { communityAuthenticatedSchema, encryptAndSign, OpenConnectionJwtPayloadType } from 'shared'
|
||||
import { getLogger } from 'log4js'
|
||||
import { AuthenticationClientFactory } from './client/AuthenticationClientFactory'
|
||||
import { EncryptedTransferArgs } from 'core'
|
||||
import { CommunityHandshakeStateLogic } from 'core'
|
||||
import { Ed25519PublicKey } from 'shared'
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.authenticateCommunities`)
|
||||
const createLogger = (functionName: string) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.authenticateCommunities.${functionName}`)
|
||||
|
||||
export enum StartCommunityAuthenticationResult {
|
||||
ALREADY_AUTHENTICATED = 'already authenticated',
|
||||
ALREADY_IN_PROGRESS = 'already in progress',
|
||||
SUCCESSFULLY_STARTED = 'successfully started',
|
||||
}
|
||||
|
||||
export async function startCommunityAuthentication(
|
||||
fedComB: DbFederatedCommunity,
|
||||
): Promise<void> {
|
||||
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.authenticateCommunities.startCommunityAuthentication`)
|
||||
): Promise<StartCommunityAuthenticationResult> {
|
||||
const methodLogger = createLogger('startCommunityAuthentication')
|
||||
const handshakeID = randombytes_random().toString()
|
||||
const fedComBPublicKey = new Ed25519PublicKey(fedComB.publicKey)
|
||||
methodLogger.addContext('handshakeID', handshakeID)
|
||||
methodLogger.debug(`startCommunityAuthentication()...`, {
|
||||
fedComB: new FederatedCommunityLoggingView(fedComB),
|
||||
})
|
||||
const homeComA = await getHomeCommunity()
|
||||
methodLogger.debug('homeComA', new CommunityLoggingView(homeComA!))
|
||||
const homeFedComA = await DbFederatedCommunity.findOneByOrFail({
|
||||
foreign: false,
|
||||
apiVersion: CONFIG_CORE.FEDERATION_BACKEND_SEND_ON_API,
|
||||
})
|
||||
methodLogger.debug('homeFedComA', new FederatedCommunityLoggingView(homeFedComA))
|
||||
const comB = await DbCommunity.findOneByOrFail({ publicKey: fedComB.publicKey })
|
||||
methodLogger.debug('started with comB:', new CommunityLoggingView(comB))
|
||||
methodLogger.debug(`start with public key ${fedComBPublicKey.asHex()}`)
|
||||
const homeComA = await getHomeCommunityWithFederatedCommunityOrFail(fedComB.apiVersion)
|
||||
// methodLogger.debug('homeComA', new CommunityLoggingView(homeComA))
|
||||
const homeFedComA = getFederatedCommunityWithApiOrFail(homeComA, fedComB.apiVersion)
|
||||
|
||||
const comB = await getCommunityByPublicKeyOrFail(fedComBPublicKey)
|
||||
// methodLogger.debug('started with comB:', new CommunityLoggingView(comB))
|
||||
// check if communityUuid is not a valid v4Uuid
|
||||
try {
|
||||
if (
|
||||
comB &&
|
||||
((comB.communityUuid === null && comB.authenticatedAt === null) ||
|
||||
(comB.communityUuid !== null &&
|
||||
(!validateUUID(comB.communityUuid) ||
|
||||
versionUUID(comB.communityUuid!) !== 4)))
|
||||
) {
|
||||
methodLogger.debug('comB.uuid is null or is a not valid v4Uuid...', comB.communityUuid || 'null', comB.authenticatedAt || 'null')
|
||||
const client = AuthenticationClientFactory.getInstance(fedComB)
|
||||
|
||||
if (client instanceof V1_0_AuthenticationClient) {
|
||||
if (!comB.publicJwtKey) {
|
||||
throw new Error('Public JWT key still not exist for comB ' + comB.name)
|
||||
}
|
||||
//create JWT with url in payload encrypted by foreignCom.publicJwtKey and signed with homeCom.privateJwtKey
|
||||
const payload = new OpenConnectionJwtPayloadType(handshakeID,
|
||||
ensureUrlEndsWithSlash(homeFedComA.endPoint).concat(homeFedComA.apiVersion),
|
||||
)
|
||||
methodLogger.debug('payload', payload)
|
||||
const jws = await encryptAndSign(payload, homeComA!.privateJwtKey!, comB.publicJwtKey!)
|
||||
methodLogger.debug('jws', jws)
|
||||
// prepare the args for the client invocation
|
||||
const args = new EncryptedTransferArgs()
|
||||
args.publicKey = homeComA!.publicKey.toString('hex')
|
||||
args.jwt = jws
|
||||
args.handshakeID = handshakeID
|
||||
methodLogger.debug('before client.openConnection() args:', args)
|
||||
const result = await client.openConnection(args)
|
||||
if (result) {
|
||||
methodLogger.debug(`successful initiated at community:`, fedComB.endPoint)
|
||||
} else {
|
||||
methodLogger.error(`can't initiate at community:`, fedComB.endPoint)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
methodLogger.debug(`comB.communityUuid is already a valid v4Uuid ${ comB.communityUuid || 'null' } and was authenticated at ${ comB.authenticatedAt || 'null'}`)
|
||||
}
|
||||
} catch (err) {
|
||||
methodLogger.error(`Error:`, err)
|
||||
|
||||
// communityAuthenticatedSchema.safeParse return true
|
||||
// - if communityUuid is a valid v4Uuid and
|
||||
// - if authenticatedAt is a valid date
|
||||
if (communityAuthenticatedSchema.safeParse(comB).success) {
|
||||
methodLogger.debug(`comB.communityUuid is already a valid v4Uuid ${ comB.communityUuid || 'null' } and was authenticated at ${ comB.authenticatedAt || 'null'}`)
|
||||
return StartCommunityAuthenticationResult.ALREADY_AUTHENTICATED
|
||||
}
|
||||
methodLogger.removeContext('handshakeID')
|
||||
/*methodLogger.debug('comB.uuid is null or is a not valid v4Uuid...',
|
||||
comB.communityUuid || 'null', comB.authenticatedAt || 'null'
|
||||
)*/
|
||||
|
||||
// check if a authentication is already in progress
|
||||
const existingState = await findPendingCommunityHandshake(fedComBPublicKey, fedComB.apiVersion)
|
||||
if (existingState) {
|
||||
const stateLogic = new CommunityHandshakeStateLogic(existingState)
|
||||
// retry on timeout or failure
|
||||
if (!(await stateLogic.isTimeoutUpdate())) {
|
||||
// authentication with community and api version is still in progress and it is not timeout yet
|
||||
methodLogger.debug('existingState, so we exit here', new CommunityHandshakeStateLoggingView(existingState))
|
||||
return StartCommunityAuthenticationResult.ALREADY_IN_PROGRESS
|
||||
}
|
||||
}
|
||||
|
||||
const client = AuthenticationClientFactory.getInstance(fedComB)
|
||||
|
||||
if (client instanceof V1_0_AuthenticationClient) {
|
||||
if (!comB.publicJwtKey) {
|
||||
throw new Error(`Public JWT key still not exist for comB ${comB.name}`)
|
||||
}
|
||||
const state = new DbCommunityHandshakeState()
|
||||
state.publicKey = fedComBPublicKey.asBuffer()
|
||||
state.apiVersion = fedComB.apiVersion
|
||||
state.status = CommunityHandshakeStateType.START_COMMUNITY_AUTHENTICATION
|
||||
state.handshakeId = parseInt(handshakeID)
|
||||
await state.save()
|
||||
methodLogger.debug('[START_COMMUNITY_AUTHENTICATION] community handshake state created')
|
||||
|
||||
//create JWT with url in payload encrypted by foreignCom.publicJwtKey and signed with homeCom.privateJwtKey
|
||||
const payload = new OpenConnectionJwtPayloadType(handshakeID,
|
||||
ensureUrlEndsWithSlash(homeFedComA.endPoint).concat(homeFedComA.apiVersion),
|
||||
)
|
||||
// methodLogger.debug('payload', payload)
|
||||
const jws = await encryptAndSign(payload, homeComA!.privateJwtKey!, comB.publicJwtKey!)
|
||||
// methodLogger.debug('jws', jws)
|
||||
// prepare the args for the client invocation
|
||||
const args = new EncryptedTransferArgs()
|
||||
const homeComAPublicKey = new Ed25519PublicKey(homeComA!.publicKey)
|
||||
args.publicKey = homeComAPublicKey.asHex()
|
||||
args.jwt = jws
|
||||
args.handshakeID = handshakeID
|
||||
// methodLogger.debug('before client.openConnection() args:', args)
|
||||
const result = await client.openConnection(args)
|
||||
if (result) {
|
||||
methodLogger.debug(`successful initiated at community:`, fedComB.endPoint)
|
||||
} else {
|
||||
const errorMsg = `can't initiate at community: ${fedComB.endPoint}`
|
||||
methodLogger.error(errorMsg)
|
||||
state.status = CommunityHandshakeStateType.FAILED
|
||||
state.lastError = errorMsg
|
||||
}
|
||||
await state.save()
|
||||
}
|
||||
return StartCommunityAuthenticationResult.SUCCESSFULLY_STARTED
|
||||
}
|
||||
|
||||
@ -103,7 +103,7 @@ describe('validate Communities', () => {
|
||||
return {
|
||||
data: {
|
||||
getPublicKey: {
|
||||
publicKey: 'somePubKey',
|
||||
publicKey: '2222222222222222222222222222222222222222222222222222222222222222',
|
||||
},
|
||||
},
|
||||
} as Response<unknown>
|
||||
@ -170,8 +170,8 @@ describe('validate Communities', () => {
|
||||
it('logs not matching publicKeys', () => {
|
||||
expect(logger.debug).toBeCalledWith(
|
||||
'received not matching publicKey:',
|
||||
'somePubKey',
|
||||
expect.stringMatching('11111111111111111111111111111111'),
|
||||
'2222222222222222222222222222222222222222222222222222222222222222',
|
||||
expect.stringMatching('1111111111111111111111111111111100000000000000000000000000000000'),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import {
|
||||
Community as DbCommunity,
|
||||
FederatedCommunity as DbFederatedCommunity,
|
||||
FederatedCommunityLoggingView,
|
||||
getHomeCommunity,
|
||||
} from 'database'
|
||||
import { IsNull } from 'typeorm'
|
||||
@ -11,7 +10,7 @@ import { FederationClient as V1_0_FederationClient } from '@/federation/client/1
|
||||
import { PublicCommunityInfo } from '@/federation/client/1_0/model/PublicCommunityInfo'
|
||||
import { FederationClientFactory } from '@/federation/client/FederationClientFactory'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { createKeyPair } from 'shared'
|
||||
import { createKeyPair, Ed25519PublicKey } from 'shared'
|
||||
import { getLogger } from 'log4js'
|
||||
import { startCommunityAuthentication } from './authenticateCommunities'
|
||||
import { PublicCommunityInfoLoggingView } from './client/1_0/logging/PublicCommunityInfoLogging.view'
|
||||
@ -45,27 +44,31 @@ export async function validateCommunities(): Promise<void> {
|
||||
|
||||
logger.debug(`found ${dbFederatedCommunities.length} dbCommunities`)
|
||||
for (const dbFedComB of dbFederatedCommunities) {
|
||||
logger.debug('dbFedComB', new FederatedCommunityLoggingView(dbFedComB))
|
||||
logger.debug(`verify federation community: ${dbFedComB.endPoint}${dbFedComB.apiVersion}`)
|
||||
const apiValueStrings: string[] = Object.values(ApiVersionType)
|
||||
logger.debug(`suppported ApiVersions=`, apiValueStrings)
|
||||
if (!apiValueStrings.includes(dbFedComB.apiVersion)) {
|
||||
logger.debug('dbFedComB with unsupported apiVersion', dbFedComB.endPoint, dbFedComB.apiVersion)
|
||||
logger.debug(`supported ApiVersions=`, apiValueStrings)
|
||||
continue
|
||||
}
|
||||
try {
|
||||
const client = FederationClientFactory.getInstance(dbFedComB)
|
||||
|
||||
if (client instanceof V1_0_FederationClient) {
|
||||
const pubKey = await client.getPublicKey()
|
||||
if (pubKey && pubKey === dbFedComB.publicKey.toString('hex')) {
|
||||
// throw if key isn't valid hex with length 64
|
||||
const clientPublicKey = new Ed25519PublicKey(await client.getPublicKey())
|
||||
// throw if key isn't valid hex with length 64
|
||||
const fedComBPublicKey = new Ed25519PublicKey(dbFedComB.publicKey)
|
||||
if (clientPublicKey.isSame(fedComBPublicKey)) {
|
||||
await DbFederatedCommunity.update({ id: dbFedComB.id }, { verifiedAt: new Date() })
|
||||
logger.debug(`verified dbFedComB with:`, dbFedComB.endPoint)
|
||||
// logger.debug(`verified dbFedComB with:`, dbFedComB.endPoint)
|
||||
const pubComInfo = await client.getPublicCommunityInfo()
|
||||
if (pubComInfo) {
|
||||
await writeForeignCommunity(dbFedComB, pubComInfo)
|
||||
logger.debug(`wrote response of getPublicCommunityInfo in dbFedComB ${dbFedComB.endPoint}`)
|
||||
try {
|
||||
await startCommunityAuthentication(dbFedComB)
|
||||
const result = await startCommunityAuthentication(dbFedComB)
|
||||
logger.info(`${dbFedComB.endPoint}${dbFedComB.apiVersion} verified, authentication state: ${result}`)
|
||||
} catch (err) {
|
||||
logger.warn(`Warning: Authentication of community ${dbFedComB.endPoint} still ongoing:`, err)
|
||||
}
|
||||
@ -73,7 +76,7 @@ export async function validateCommunities(): Promise<void> {
|
||||
logger.debug('missing result of getPublicCommunityInfo')
|
||||
}
|
||||
} else {
|
||||
logger.debug('received not matching publicKey:', pubKey, dbFedComB.publicKey.toString('hex'))
|
||||
logger.debug('received not matching publicKey:', clientPublicKey.asHex(), fedComBPublicKey.asHex())
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
17
bun.lock
17
bun.lock
@ -449,6 +449,7 @@
|
||||
"esbuild": "^0.25.2",
|
||||
"jose": "^4.14.4",
|
||||
"log4js": "^6.9.1",
|
||||
"yoctocolors-cjs": "^2.1.2",
|
||||
"zod": "^3.25.61",
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -1780,7 +1781,7 @@
|
||||
|
||||
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="],
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="],
|
||||
|
||||
@ -2816,7 +2817,7 @@
|
||||
|
||||
"object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="],
|
||||
|
||||
"ohash": ["ohash@1.1.6", "", {}, "sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg=="],
|
||||
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
|
||||
|
||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||
|
||||
@ -3636,8 +3637,6 @@
|
||||
|
||||
"@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"@babel/core/convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
@ -3724,20 +3723,22 @@
|
||||
|
||||
"@jest/source-map/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"@jest/transform/convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="],
|
||||
|
||||
"@jest/transform/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"@jest/transform/write-file-atomic": ["write-file-atomic@3.0.3", "", { "dependencies": { "imurmurhash": "^0.1.4", "is-typedarray": "^1.0.0", "signal-exit": "^3.0.2", "typedarray-to-buffer": "^3.1.5" } }, "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q=="],
|
||||
|
||||
"@jest/types/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
|
||||
|
||||
"@morev/utils/ohash": ["ohash@1.1.6", "", {}, "sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg=="],
|
||||
|
||||
"@morev/utils/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
|
||||
"@nuxt/kit/consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
|
||||
|
||||
"@nuxt/kit/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||
|
||||
"@nuxt/kit/ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
|
||||
|
||||
"@nuxt/kit/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"@nuxt/kit/pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="],
|
||||
@ -3886,8 +3887,6 @@
|
||||
|
||||
"c12/dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
|
||||
|
||||
"c12/ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
|
||||
|
||||
"c12/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"c12/pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="],
|
||||
@ -4248,6 +4247,8 @@
|
||||
|
||||
"unplugin-vue-components/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"v8-to-istanbul/convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="],
|
||||
|
||||
"vee-validate/@vue/devtools-api": ["@vue/devtools-api@7.7.7", "", { "dependencies": { "@vue/devtools-kit": "^7.7.7" } }, "sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg=="],
|
||||
|
||||
"vee-validate/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
|
||||
@ -1,42 +1,41 @@
|
||||
import { EncryptedTransferArgs } from '../model/EncryptedTransferArgs'
|
||||
import { JwtPayloadType } from 'shared'
|
||||
import { Ed25519PublicKey, JwtPayloadType } from 'shared'
|
||||
import { Community as DbCommunity } from 'database'
|
||||
import { getLogger } from 'log4js'
|
||||
import { CommunityLoggingView, getHomeCommunity } from 'database'
|
||||
import { verifyAndDecrypt } from 'shared'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '../../config/const'
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.logic.interpretEncryptedTransferArgs`)
|
||||
const createLogger = (functionName: string) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.logic.interpretEncryptedTransferArgs.${functionName}`)
|
||||
|
||||
export const interpretEncryptedTransferArgs = async (args: EncryptedTransferArgs): Promise<JwtPayloadType | null> => {
|
||||
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.logic.interpretEncryptedTransferArgs-method`)
|
||||
const methodLogger = createLogger('interpretEncryptedTransferArgs')
|
||||
methodLogger.addContext('handshakeID', args.handshakeID)
|
||||
methodLogger.debug('interpretEncryptedTransferArgs()... args:', args)
|
||||
const argsPublicKey = new Ed25519PublicKey(args.publicKey)
|
||||
// first find with args.publicKey the community 'requestingCom', which starts the request
|
||||
const requestingCom = await DbCommunity.findOneBy({ publicKey: Buffer.from(args.publicKey, 'hex') })
|
||||
// TODO: maybe use community from caller instead of loading it separately
|
||||
const requestingCom = await DbCommunity.findOneBy({ publicKey: argsPublicKey.asBuffer() })
|
||||
if (!requestingCom) {
|
||||
const errmsg = `unknown requesting community with publicKey ${Buffer.from(args.publicKey, 'hex')}`
|
||||
const errmsg = `unknown requesting community with publicKey ${argsPublicKey.asHex()}`
|
||||
methodLogger.error(errmsg)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
throw new Error(errmsg)
|
||||
}
|
||||
if (!requestingCom.publicJwtKey) {
|
||||
const errmsg = `missing publicJwtKey of requesting community with publicKey ${Buffer.from(args.publicKey, 'hex')}`
|
||||
const errmsg = `missing publicJwtKey of requesting community with publicKey ${argsPublicKey.asHex()}`
|
||||
methodLogger.error(errmsg)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
throw new Error(errmsg)
|
||||
}
|
||||
methodLogger.debug(`found requestingCom:`, new CommunityLoggingView(requestingCom))
|
||||
// verify the signing of args.jwt with homeCom.privateJwtKey and decrypt args.jwt with requestingCom.publicJwtKey
|
||||
// TODO: maybe use community from caller instead of loading it separately
|
||||
const homeCom = await getHomeCommunity()
|
||||
const jwtPayload = await verifyAndDecrypt(args.handshakeID, args.jwt, homeCom!.privateJwtKey!, requestingCom.publicJwtKey) as JwtPayloadType
|
||||
if (!jwtPayload) {
|
||||
const errmsg = `invalid payload of community with publicKey ${Buffer.from(args.publicKey, 'hex')}`
|
||||
const errmsg = `invalid payload of community with publicKey ${argsPublicKey.asHex()}`
|
||||
methodLogger.error(errmsg)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
throw new Error(errmsg)
|
||||
}
|
||||
methodLogger.debug('jwtPayload', jwtPayload)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
return jwtPayload
|
||||
}
|
||||
|
||||
@ -22,4 +22,5 @@ export * from './util/calculateSenderBalance'
|
||||
export * from './util/utilities'
|
||||
export * from './validation/user'
|
||||
export * from './config/index'
|
||||
export * from './logic'
|
||||
|
||||
|
||||
33
core/src/logic/CommunityHandshakeState.logic.ts
Normal file
33
core/src/logic/CommunityHandshakeState.logic.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { CommunityHandshakeState, CommunityHandshakeStateType } from 'database'
|
||||
import { FEDERATION_AUTHENTICATION_TIMEOUT_MS } from 'shared'
|
||||
|
||||
export class CommunityHandshakeStateLogic {
|
||||
public constructor(private self: CommunityHandshakeState) {}
|
||||
|
||||
/**
|
||||
* Check for expired state and if not, check timeout and update (write into db) to expired state
|
||||
* @returns true if the community handshake state is expired
|
||||
*/
|
||||
public async isTimeoutUpdate(): Promise<boolean> {
|
||||
const timeout = this.isTimeout()
|
||||
if (timeout && this.self.status !== CommunityHandshakeStateType.EXPIRED) {
|
||||
this.self.status = CommunityHandshakeStateType.EXPIRED
|
||||
await this.self.save()
|
||||
}
|
||||
return timeout
|
||||
}
|
||||
|
||||
public isTimeout(): boolean {
|
||||
if (this.self.status === CommunityHandshakeStateType.EXPIRED) {
|
||||
return true
|
||||
}
|
||||
if ((Date.now() - this.self.updatedAt.getTime()) > FEDERATION_AUTHENTICATION_TIMEOUT_MS) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public isFailed(): boolean {
|
||||
return this.self.status === CommunityHandshakeStateType.FAILED
|
||||
}
|
||||
}
|
||||
22
core/src/logic/CommunityHandshakeStateLogic.test.ts
Normal file
22
core/src/logic/CommunityHandshakeStateLogic.test.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { CommunityHandshakeState } from 'database'
|
||||
import { CommunityHandshakeStateLogic } from './CommunityHandshakeState.logic'
|
||||
import { CommunityHandshakeStateType } from 'database'
|
||||
import { FEDERATION_AUTHENTICATION_TIMEOUT_MS } from 'shared'
|
||||
|
||||
describe('CommunityHandshakeStateLogic', () => {
|
||||
it('isTimeout', () => {
|
||||
const state = new CommunityHandshakeState()
|
||||
state.updatedAt = new Date(Date.now() - FEDERATION_AUTHENTICATION_TIMEOUT_MS * 2)
|
||||
state.status = CommunityHandshakeStateType.START_AUTHENTICATION
|
||||
const logic = new CommunityHandshakeStateLogic(state)
|
||||
expect(logic.isTimeout()).toEqual(true)
|
||||
})
|
||||
|
||||
it('isTimeout return false', () => {
|
||||
const state = new CommunityHandshakeState()
|
||||
state.updatedAt = new Date(Date.now())
|
||||
state.status = CommunityHandshakeStateType.START_AUTHENTICATION
|
||||
const logic = new CommunityHandshakeStateLogic(state)
|
||||
expect(logic.isTimeout()).toEqual(false)
|
||||
})
|
||||
})
|
||||
12
core/src/logic/community.logic.ts
Normal file
12
core/src/logic/community.logic.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Community as DbCommunity, FederatedCommunity as DbFederatedCommunity } from 'database'
|
||||
|
||||
export function getFederatedCommunityWithApiOrFail(
|
||||
community: DbCommunity,
|
||||
apiVersion: string
|
||||
): DbFederatedCommunity {
|
||||
const fedCom = community.federatedCommunities?.find((fedCom) => fedCom.apiVersion === apiVersion)
|
||||
if (!fedCom) {
|
||||
throw new Error(`Missing federated community with api version ${apiVersion}`)
|
||||
}
|
||||
return fedCom
|
||||
}
|
||||
2
core/src/logic/index.ts
Normal file
2
core/src/logic/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './CommunityHandshakeState.logic'
|
||||
export * from './community.logic'
|
||||
@ -36,6 +36,11 @@ export const delay = promisify(setTimeout)
|
||||
export const ensureUrlEndsWithSlash = (url: string): string => {
|
||||
return url.endsWith('/') ? url : url.concat('/')
|
||||
}
|
||||
export function splitUrlInEndPointAndApiVersion(url: string): { endPoint: string, apiVersion: string } {
|
||||
const endPoint = url.slice(0, url.lastIndexOf('/') + 1)
|
||||
const apiVersion = url.slice(url.lastIndexOf('/') + 1, url.length)
|
||||
return { endPoint, apiVersion }
|
||||
}
|
||||
/**
|
||||
* Calculates the date representing the first day of the month, a specified number of months prior to a given date.
|
||||
*
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn(`
|
||||
CREATE TABLE community_handshake_states (
|
||||
id int unsigned NOT NULL AUTO_INCREMENT,
|
||||
handshake_id int unsigned NOT NULL,
|
||||
one_time_code int unsigned NULL DEFAULT NULL,
|
||||
public_key binary(32) NOT NULL,
|
||||
api_version varchar(255) NOT NULL,
|
||||
status varchar(255) NOT NULL DEFAULT 'OPEN_CONNECTION',
|
||||
last_error text,
|
||||
created_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_public_key (public_key)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`)
|
||||
}
|
||||
|
||||
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn(`DROP TABLE community_handshake_states;`)
|
||||
}
|
||||
37
database/src/entity/CommunityHandshakeState.ts
Normal file
37
database/src/entity/CommunityHandshakeState.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { CommunityHandshakeStateType } from '../enum'
|
||||
import { BaseEntity, Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'
|
||||
|
||||
@Entity('community_handshake_states')
|
||||
export class CommunityHandshakeState extends BaseEntity {
|
||||
@PrimaryGeneratedColumn({ unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ name: 'handshake_id', type: 'int', unsigned: true })
|
||||
handshakeId: number
|
||||
|
||||
@Column({ name: 'one_time_code', type: 'int', unsigned: true, default: null, nullable: true })
|
||||
oneTimeCode?: number
|
||||
|
||||
@Column({ name: 'public_key', type: 'binary', length: 32 })
|
||||
publicKey: Buffer
|
||||
|
||||
@Column({ name: 'api_version', type: 'varchar', length: 255 })
|
||||
apiVersion: string
|
||||
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 255,
|
||||
default: CommunityHandshakeStateType.START_COMMUNITY_AUTHENTICATION,
|
||||
nullable: false,
|
||||
})
|
||||
status: CommunityHandshakeStateType
|
||||
|
||||
@Column({ name: 'last_error', type: 'text', nullable: true })
|
||||
lastError?: string
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'datetime', precision: 3 })
|
||||
createdAt: Date
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'datetime', precision: 3 })
|
||||
updatedAt: Date
|
||||
}
|
||||
@ -7,6 +7,7 @@ import { Event } from './Event'
|
||||
import { FederatedCommunity } from './FederatedCommunity'
|
||||
import { LoginElopageBuys } from './LoginElopageBuys'
|
||||
import { Migration } from './Migration'
|
||||
import { CommunityHandshakeState } from './CommunityHandshakeState'
|
||||
import { OpenaiThreads } from './OpenaiThreads'
|
||||
import { PendingTransaction } from './PendingTransaction'
|
||||
import { ProjectBranding } from './ProjectBranding'
|
||||
@ -18,6 +19,7 @@ import { UserRole } from './UserRole'
|
||||
|
||||
export {
|
||||
Community,
|
||||
CommunityHandshakeState,
|
||||
Contribution,
|
||||
ContributionLink,
|
||||
ContributionMessage,
|
||||
@ -25,7 +27,7 @@ export {
|
||||
Event,
|
||||
FederatedCommunity,
|
||||
LoginElopageBuys,
|
||||
Migration,
|
||||
Migration,
|
||||
ProjectBranding,
|
||||
OpenaiThreads,
|
||||
PendingTransaction,
|
||||
@ -38,6 +40,7 @@ export {
|
||||
|
||||
export const entities = [
|
||||
Community,
|
||||
CommunityHandshakeState,
|
||||
Contribution,
|
||||
ContributionLink,
|
||||
ContributionMessage,
|
||||
|
||||
9
database/src/enum/CommunityHandshakeStateType.ts
Normal file
9
database/src/enum/CommunityHandshakeStateType.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export enum CommunityHandshakeStateType {
|
||||
START_COMMUNITY_AUTHENTICATION = 'START_COMMUNITY_AUTHENTICATION',
|
||||
START_OPEN_CONNECTION_CALLBACK = 'START_OPEN_CONNECTION_CALLBACK',
|
||||
START_AUTHENTICATION = 'START_AUTHENTICATION',
|
||||
|
||||
SUCCESS = 'SUCCESS',
|
||||
FAILED = 'FAILED',
|
||||
EXPIRED = 'EXPIRED'
|
||||
}
|
||||
1
database/src/enum/index.ts
Normal file
1
database/src/enum/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './CommunityHandshakeStateType'
|
||||
@ -1,64 +1,9 @@
|
||||
import { latestDbVersion } from './detectLastDBVersion'
|
||||
import { Community } from './entity/Community'
|
||||
import { Contribution } from './entity/Contribution'
|
||||
import { ContributionLink } from './entity/ContributionLink'
|
||||
import { ContributionMessage } from './entity/ContributionMessage'
|
||||
import { DltTransaction } from './entity/DltTransaction'
|
||||
import { Event } from './entity/Event'
|
||||
import { FederatedCommunity } from './entity/FederatedCommunity'
|
||||
import { LoginElopageBuys } from './entity/LoginElopageBuys'
|
||||
import { Migration } from './entity/Migration'
|
||||
import { OpenaiThreads } from './entity/OpenaiThreads'
|
||||
import { PendingTransaction } from './entity/PendingTransaction'
|
||||
import { ProjectBranding } from './entity/ProjectBranding'
|
||||
import { Transaction } from './entity/Transaction'
|
||||
import { TransactionLink } from './entity/TransactionLink'
|
||||
import { User } from './entity/User'
|
||||
import { UserContact } from './entity/UserContact'
|
||||
import { UserRole } from './entity/UserRole'
|
||||
|
||||
export {
|
||||
Community,
|
||||
Contribution,
|
||||
ContributionLink,
|
||||
ContributionMessage,
|
||||
DltTransaction,
|
||||
Event,
|
||||
FederatedCommunity,
|
||||
LoginElopageBuys,
|
||||
Migration,
|
||||
ProjectBranding,
|
||||
OpenaiThreads,
|
||||
PendingTransaction,
|
||||
Transaction,
|
||||
TransactionLink,
|
||||
User,
|
||||
UserContact,
|
||||
UserRole,
|
||||
}
|
||||
|
||||
export const entities = [
|
||||
Community,
|
||||
Contribution,
|
||||
ContributionLink,
|
||||
ContributionMessage,
|
||||
DltTransaction,
|
||||
Event,
|
||||
FederatedCommunity,
|
||||
LoginElopageBuys,
|
||||
Migration,
|
||||
ProjectBranding,
|
||||
OpenaiThreads,
|
||||
PendingTransaction,
|
||||
Transaction,
|
||||
TransactionLink,
|
||||
User,
|
||||
UserContact,
|
||||
UserRole,
|
||||
]
|
||||
|
||||
export { latestDbVersion }
|
||||
|
||||
export * from './entity'
|
||||
export * from './logging'
|
||||
export * from './queries'
|
||||
export * from './util'
|
||||
export * from './util'
|
||||
export * from './enum'
|
||||
export { AppDatabase } from './AppDatabase'
|
||||
|
||||
21
database/src/logging/CommunityHandshakeStateLogging.view.ts
Normal file
21
database/src/logging/CommunityHandshakeStateLogging.view.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { CommunityHandshakeState } from '..'
|
||||
import { AbstractLoggingView } from './AbstractLogging.view'
|
||||
|
||||
export class CommunityHandshakeStateLoggingView extends AbstractLoggingView {
|
||||
public constructor(private self: CommunityHandshakeState) {
|
||||
super()
|
||||
}
|
||||
|
||||
public toJSON(): any {
|
||||
return {
|
||||
id: this.self.id,
|
||||
handshakeId: this.self.handshakeId,
|
||||
oneTimeCode: this.self.oneTimeCode,
|
||||
publicKey: this.self.publicKey.toString(this.bufferStringFormat),
|
||||
status: this.self.status,
|
||||
lastError: this.self.lastError,
|
||||
createdAt: this.dateToString(this.self.createdAt),
|
||||
updatedAt: this.dateToString(this.self.updatedAt),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import { Community } from '../entity'
|
||||
|
||||
import { FederatedCommunityLoggingView } from './FederatedCommunityLogging.view'
|
||||
import { AbstractLoggingView } from './AbstractLogging.view'
|
||||
|
||||
export class CommunityLoggingView extends AbstractLoggingView {
|
||||
@ -21,6 +21,9 @@ export class CommunityLoggingView extends AbstractLoggingView {
|
||||
creationDate: this.dateToString(this.self.creationDate),
|
||||
createdAt: this.dateToString(this.self.createdAt),
|
||||
updatedAt: this.dateToString(this.self.updatedAt),
|
||||
federatedCommunities: this.self.federatedCommunities?.map(
|
||||
(federatedCommunity) => new FederatedCommunityLoggingView(federatedCommunity)
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import { TransactionLoggingView } from './TransactionLogging.view'
|
||||
import { UserContactLoggingView } from './UserContactLogging.view'
|
||||
import { UserLoggingView } from './UserLogging.view'
|
||||
import { UserRoleLoggingView } from './UserRoleLogging.view'
|
||||
import { CommunityHandshakeStateLoggingView } from './CommunityHandshakeStateLogging.view'
|
||||
|
||||
export {
|
||||
AbstractLoggingView,
|
||||
@ -24,6 +25,7 @@ export {
|
||||
UserContactLoggingView,
|
||||
UserLoggingView,
|
||||
UserRoleLoggingView,
|
||||
CommunityHandshakeStateLoggingView,
|
||||
}
|
||||
|
||||
export const logger = getLogger(LOG4JS_BASE_CATEGORY_NAME)
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { Community as DbCommunity, FederatedCommunity as DbFederatedCommunity } from '..'
|
||||
import { AppDatabase } from '../AppDatabase'
|
||||
import { getHomeCommunity, getReachableCommunities } from './communities'
|
||||
import { getCommunityByPublicKeyOrFail, getHomeCommunity, getHomeCommunityWithFederatedCommunityOrFail, getReachableCommunities } from './communities'
|
||||
import { describe, expect, it, beforeEach, beforeAll, afterAll } from 'vitest'
|
||||
import { createCommunity, createVerifiedFederatedCommunity } from '../seeds/community'
|
||||
import { Ed25519PublicKey } from 'shared'
|
||||
|
||||
const db = AppDatabase.getInstance()
|
||||
|
||||
@ -39,6 +40,36 @@ describe('community.queries', () => {
|
||||
expect(community?.privateKey).toStrictEqual(homeCom.privateKey)
|
||||
})
|
||||
})
|
||||
describe('getHomeCommunityWithFederatedCommunityOrFail', () => {
|
||||
it('should return the home community with federated communities', async () => {
|
||||
const homeCom = await createCommunity(false)
|
||||
await createVerifiedFederatedCommunity('1_0', 100, homeCom)
|
||||
const community = await getHomeCommunityWithFederatedCommunityOrFail('1_0')
|
||||
expect(community).toBeDefined()
|
||||
expect(community?.federatedCommunities).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should throw if no home community exists', async () => {
|
||||
expect(() => getHomeCommunityWithFederatedCommunityOrFail('1_0')).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('should throw if no federated community exists', async () => {
|
||||
await createCommunity(false)
|
||||
expect(() => getHomeCommunityWithFederatedCommunityOrFail('1_0')).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('load community by public key returned from getHomeCommunityWithFederatedCommunityOrFail', async () => {
|
||||
const homeCom = await createCommunity(false)
|
||||
await createVerifiedFederatedCommunity('1_0', 100, homeCom)
|
||||
const community = await getHomeCommunityWithFederatedCommunityOrFail('1_0')
|
||||
expect(community).toBeDefined()
|
||||
expect(community?.federatedCommunities).toHaveLength(1)
|
||||
const ed25519PublicKey = new Ed25519PublicKey(community.federatedCommunities![0].publicKey)
|
||||
const communityByPublicKey = await getCommunityByPublicKeyOrFail(ed25519PublicKey)
|
||||
expect(communityByPublicKey).toBeDefined()
|
||||
expect(communityByPublicKey?.communityUuid).toBe(homeCom.communityUuid)
|
||||
})
|
||||
})
|
||||
describe('getReachableCommunities', () => {
|
||||
it('home community counts also to reachable communities', async () => {
|
||||
await createCommunity(false)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { FindOptionsOrder, FindOptionsWhere, IsNull, MoreThanOrEqual, Not } from 'typeorm'
|
||||
import { Community as DbCommunity } from '../entity'
|
||||
import { urlSchema, uuidv4Schema } from 'shared'
|
||||
import { Ed25519PublicKey, urlSchema, uuidv4Schema } from 'shared'
|
||||
|
||||
/**
|
||||
* Retrieves the home community, i.e., a community that is not foreign.
|
||||
@ -10,7 +10,14 @@ export async function getHomeCommunity(): Promise<DbCommunity | null> {
|
||||
// TODO: Put in Cache, it is needed nearly always
|
||||
// TODO: return only DbCommunity or throw to reduce unnecessary checks, because there should be always a home community
|
||||
return await DbCommunity.findOne({
|
||||
where: { foreign: false },
|
||||
where: { foreign: false }
|
||||
})
|
||||
}
|
||||
|
||||
export async function getHomeCommunityWithFederatedCommunityOrFail(apiVersion: string): Promise<DbCommunity> {
|
||||
return await DbCommunity.findOneOrFail({
|
||||
where: { foreign: false, federatedCommunities: { apiVersion } },
|
||||
relations: { federatedCommunities: true },
|
||||
})
|
||||
}
|
||||
|
||||
@ -42,6 +49,22 @@ export async function getCommunityWithFederatedCommunityByIdentifier(
|
||||
})
|
||||
}
|
||||
|
||||
export async function getCommunityWithFederatedCommunityWithApiOrFail(
|
||||
publicKey: Ed25519PublicKey,
|
||||
apiVersion: string
|
||||
): Promise<DbCommunity> {
|
||||
return await DbCommunity.findOneOrFail({
|
||||
where: { foreign: true, publicKey: publicKey.asBuffer(), federatedCommunities: { apiVersion } },
|
||||
relations: { federatedCommunities: true },
|
||||
})
|
||||
}
|
||||
|
||||
export async function getCommunityByPublicKeyOrFail(publicKey: Ed25519PublicKey): Promise<DbCommunity> {
|
||||
return await DbCommunity.findOneOrFail({
|
||||
where: { publicKey: publicKey.asBuffer() },
|
||||
})
|
||||
}
|
||||
|
||||
// returns all reachable communities
|
||||
// home community and all federated communities which have been verified within the last authenticationTimeoutMs
|
||||
export async function getReachableCommunities(
|
||||
@ -60,4 +83,13 @@ export async function getReachableCommunities(
|
||||
],
|
||||
order,
|
||||
})
|
||||
}
|
||||
|
||||
export async function getNotReachableCommunities(
|
||||
order?: FindOptionsOrder<DbCommunity>
|
||||
): Promise<DbCommunity[]> {
|
||||
return await DbCommunity.find({
|
||||
where: { authenticatedAt: IsNull(), foreign: true },
|
||||
order,
|
||||
})
|
||||
}
|
||||
71
database/src/queries/communityHandshakes.test.ts
Normal file
71
database/src/queries/communityHandshakes.test.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { AppDatabase } from '../AppDatabase'
|
||||
import {
|
||||
CommunityHandshakeState as DbCommunityHandshakeState,
|
||||
Community as DbCommunity,
|
||||
FederatedCommunity as DbFederatedCommunity,
|
||||
findPendingCommunityHandshake,
|
||||
CommunityHandshakeStateType
|
||||
} from '..'
|
||||
import { describe, expect, it, beforeEach, beforeAll, afterAll } from 'vitest'
|
||||
import { createCommunity, createVerifiedFederatedCommunity } from '../seeds/community'
|
||||
import { Ed25519PublicKey } from 'shared'
|
||||
import { randomBytes } from 'node:crypto'
|
||||
|
||||
const db = AppDatabase.getInstance()
|
||||
|
||||
beforeAll(async () => {
|
||||
await db.init()
|
||||
})
|
||||
afterAll(async () => {
|
||||
await db.destroy()
|
||||
})
|
||||
|
||||
async function createCommunityHandshakeState(publicKey: Buffer) {
|
||||
const state = new DbCommunityHandshakeState()
|
||||
state.publicKey = publicKey
|
||||
state.apiVersion = '1_0'
|
||||
state.status = CommunityHandshakeStateType.START_COMMUNITY_AUTHENTICATION
|
||||
state.handshakeId = 1
|
||||
await state.save()
|
||||
}
|
||||
|
||||
describe('communityHandshakes', () => {
|
||||
// clean db for every test case
|
||||
beforeEach(async () => {
|
||||
await DbCommunity.clear()
|
||||
await DbFederatedCommunity.clear()
|
||||
await DbCommunityHandshakeState.clear()
|
||||
})
|
||||
|
||||
it('should find pending community handshake by public key', async () => {
|
||||
const com1 = await createCommunity(false)
|
||||
await createVerifiedFederatedCommunity('1_0', 100, com1)
|
||||
await createCommunityHandshakeState(com1.publicKey)
|
||||
const communityHandshakeState = await findPendingCommunityHandshake(new Ed25519PublicKey(com1.publicKey), '1_0')
|
||||
expect(communityHandshakeState).toBeDefined()
|
||||
expect(communityHandshakeState).toMatchObject({
|
||||
publicKey: com1.publicKey,
|
||||
apiVersion: '1_0',
|
||||
status: CommunityHandshakeStateType.START_COMMUNITY_AUTHENTICATION,
|
||||
handshakeId: 1
|
||||
})
|
||||
})
|
||||
|
||||
it('update state', async () => {
|
||||
const publicKey = new Ed25519PublicKey(randomBytes(32))
|
||||
await createCommunityHandshakeState(publicKey.asBuffer())
|
||||
const communityHandshakeState = await findPendingCommunityHandshake(publicKey, '1_0')
|
||||
expect(communityHandshakeState).toBeDefined()
|
||||
communityHandshakeState!.status = CommunityHandshakeStateType.START_OPEN_CONNECTION_CALLBACK
|
||||
await communityHandshakeState!.save()
|
||||
const communityHandshakeState2 = await findPendingCommunityHandshake(publicKey, '1_0')
|
||||
const states = await DbCommunityHandshakeState.find()
|
||||
expect(communityHandshakeState2).toBeDefined()
|
||||
expect(communityHandshakeState2).toMatchObject({
|
||||
publicKey: publicKey.asBuffer(),
|
||||
apiVersion: '1_0',
|
||||
status: CommunityHandshakeStateType.START_OPEN_CONNECTION_CALLBACK,
|
||||
handshakeId: 1
|
||||
})
|
||||
})
|
||||
})
|
||||
35
database/src/queries/communityHandshakes.ts
Normal file
35
database/src/queries/communityHandshakes.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { Not, In } from 'typeorm'
|
||||
import { CommunityHandshakeState, CommunityHandshakeStateType} from '..'
|
||||
import { Ed25519PublicKey } from 'shared'
|
||||
|
||||
/**
|
||||
* Find a pending community handshake by public key.
|
||||
* @param publicKey The public key of the community.
|
||||
* @param apiVersion The API version of the community.
|
||||
* @param status The status of the community handshake. Optional, if not set, it will find a pending community handshake.
|
||||
* @returns The CommunityHandshakeState with associated federated community and community.
|
||||
*/
|
||||
export function findPendingCommunityHandshake(
|
||||
publicKey: Ed25519PublicKey, apiVersion: string, status?: CommunityHandshakeStateType
|
||||
): Promise<CommunityHandshakeState | null> {
|
||||
return CommunityHandshakeState.findOne({
|
||||
where: {
|
||||
publicKey: publicKey.asBuffer(),
|
||||
apiVersion,
|
||||
status: status || Not(In([
|
||||
CommunityHandshakeStateType.EXPIRED,
|
||||
CommunityHandshakeStateType.FAILED,
|
||||
CommunityHandshakeStateType.SUCCESS
|
||||
]))
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function findPendingCommunityHandshakeOrFailByOneTimeCode(
|
||||
oneTimeCode: number
|
||||
): Promise<CommunityHandshakeState> {
|
||||
return CommunityHandshakeState.findOneOrFail({
|
||||
where: { oneTimeCode },
|
||||
})
|
||||
}
|
||||
|
||||
@ -5,5 +5,6 @@ export * from './communities'
|
||||
export * from './pendingTransactions'
|
||||
export * from './transactions'
|
||||
export * from './transactionLinks'
|
||||
export * from './communityHandshakes'
|
||||
|
||||
export const LOG4JS_QUERIES_CATEGORY_NAME = `${LOG4JS_BASE_CATEGORY_NAME}.queries`
|
||||
|
||||
@ -2,7 +2,13 @@ import { Community, FederatedCommunity } from '../entity'
|
||||
import { randomBytes } from 'node:crypto'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
export async function createCommunity(foreign: boolean, save: boolean = true): Promise<Community> {
|
||||
/**
|
||||
* Creates a community.
|
||||
* @param foreign
|
||||
* @param store if true, write to db, default: true
|
||||
* @returns
|
||||
*/
|
||||
export async function createCommunity(foreign: boolean, store: boolean = true): Promise<Community> {
|
||||
const community = new Community()
|
||||
community.publicKey = randomBytes(32)
|
||||
community.communityUuid = uuidv4()
|
||||
@ -23,14 +29,22 @@ export async function createCommunity(foreign: boolean, save: boolean = true): P
|
||||
community.description = 'HomeCommunity-description'
|
||||
community.url = 'http://localhost/api'
|
||||
}
|
||||
return save ? await community.save() : community
|
||||
return store ? await community.save() : community
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a verified federated community.
|
||||
* @param apiVersion
|
||||
* @param verifiedBeforeMs time in ms before the current time
|
||||
* @param community
|
||||
* @param store if true, write to db, default: true
|
||||
* @returns
|
||||
*/
|
||||
export async function createVerifiedFederatedCommunity(
|
||||
apiVersion: string,
|
||||
verifiedBeforeMs: number,
|
||||
community: Community,
|
||||
save: boolean = true
|
||||
store: boolean = true
|
||||
): Promise<FederatedCommunity> {
|
||||
const federatedCommunity = new FederatedCommunity()
|
||||
federatedCommunity.apiVersion = apiVersion
|
||||
@ -38,5 +52,5 @@ export async function createVerifiedFederatedCommunity(
|
||||
federatedCommunity.publicKey = community.publicKey
|
||||
federatedCommunity.community = community
|
||||
federatedCommunity.verifiedAt = new Date(Date.now() - verifiedBeforeMs)
|
||||
return save ? await federatedCommunity.save() : federatedCommunity
|
||||
return store ? await federatedCommunity.save() : federatedCommunity
|
||||
}
|
||||
|
||||
@ -1,19 +1,31 @@
|
||||
import { CONFIG } from '@/config'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import { EncryptedTransferArgs, interpretEncryptedTransferArgs } from 'core'
|
||||
import { CommunityHandshakeStateLogic, EncryptedTransferArgs, interpretEncryptedTransferArgs, splitUrlInEndPointAndApiVersion } from 'core'
|
||||
import {
|
||||
CommunityLoggingView,
|
||||
Community as DbCommunity,
|
||||
CommunityHandshakeStateLoggingView,
|
||||
CommunityHandshakeState as DbCommunityHandshakeState,
|
||||
CommunityHandshakeStateType,
|
||||
FederatedCommunity as DbFedCommunity,
|
||||
FederatedCommunityLoggingView,
|
||||
getHomeCommunity,
|
||||
findPendingCommunityHandshakeOrFailByOneTimeCode,
|
||||
getCommunityByPublicKeyOrFail,
|
||||
} from 'database'
|
||||
import { getLogger } from 'log4js'
|
||||
import { AuthenticationJwtPayloadType, AuthenticationResponseJwtPayloadType, encryptAndSign, OpenConnectionCallbackJwtPayloadType, OpenConnectionJwtPayloadType, uint32Schema, uuidv4Schema } from 'shared'
|
||||
import {
|
||||
AuthenticationJwtPayloadType,
|
||||
AuthenticationResponseJwtPayloadType,
|
||||
Ed25519PublicKey,
|
||||
encryptAndSign,
|
||||
OpenConnectionCallbackJwtPayloadType,
|
||||
OpenConnectionJwtPayloadType,
|
||||
uint32Schema,
|
||||
uuidv4Schema
|
||||
} from 'shared'
|
||||
import { Arg, Mutation, Resolver } from 'type-graphql'
|
||||
import { startAuthentication, startOpenConnectionCallback } from '../util/authenticateCommunity'
|
||||
|
||||
const createLogger = (method: string ) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.AuthenticationResolver.${method}`)
|
||||
// TODO: think about the case, when we have a higher api version, which still use this resolver
|
||||
const apiVersion = '1_0'
|
||||
const createLogger = (method: string ) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.${apiVersion}.resolver.AuthenticationResolver.${method}`)
|
||||
|
||||
@Resolver()
|
||||
export class AuthenticationResolver {
|
||||
@ -24,45 +36,38 @@ export class AuthenticationResolver {
|
||||
): Promise<boolean> {
|
||||
const methodLogger = createLogger('openConnection')
|
||||
methodLogger.addContext('handshakeID', args.handshakeID)
|
||||
methodLogger.debug(`openConnection() via apiVersion=1_0:`, args)
|
||||
const argsPublicKey = new Ed25519PublicKey(args.publicKey)
|
||||
methodLogger.debug(`start via apiVersion=${apiVersion}, public key: ${argsPublicKey.asHex()}`)
|
||||
try {
|
||||
const openConnectionJwtPayload = await interpretEncryptedTransferArgs(args) as OpenConnectionJwtPayloadType
|
||||
methodLogger.debug('openConnectionJwtPayload', openConnectionJwtPayload)
|
||||
methodLogger.debug(`openConnectionJwtPayload url: ${openConnectionJwtPayload.url}`)
|
||||
if (!openConnectionJwtPayload) {
|
||||
const errmsg = `invalid OpenConnection payload of requesting community with publicKey` + args.publicKey
|
||||
methodLogger.error(errmsg)
|
||||
// no infos to the caller
|
||||
return true
|
||||
throw new Error(`invalid OpenConnection payload of requesting community with publicKey ${argsPublicKey.asHex()}`)
|
||||
}
|
||||
if (openConnectionJwtPayload.tokentype !== OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE) {
|
||||
const errmsg = `invalid tokentype of community with publicKey` + args.publicKey
|
||||
methodLogger.error(errmsg)
|
||||
// no infos to the caller
|
||||
return true
|
||||
throw new Error(`invalid tokentype: ${openConnectionJwtPayload.tokentype} of community with publicKey ${argsPublicKey.asHex()}`)
|
||||
}
|
||||
if (!openConnectionJwtPayload.url) {
|
||||
const errmsg = `invalid url of community with publicKey` + args.publicKey
|
||||
methodLogger.error(errmsg)
|
||||
// no infos to the caller
|
||||
return true
|
||||
throw new Error(`invalid url of community with publicKey ${argsPublicKey.asHex()}`)
|
||||
}
|
||||
methodLogger.debug(`vor DbFedCommunity.findOneByOrFail()...`, { publicKey: args.publicKey })
|
||||
const fedComA = await DbFedCommunity.findOneByOrFail({ publicKey: Buffer.from(args.publicKey, 'hex') })
|
||||
methodLogger.debug(`nach DbFedCommunity.findOneByOrFail()...`, fedComA)
|
||||
methodLogger.debug('fedComA', new FederatedCommunityLoggingView(fedComA))
|
||||
// methodLogger.debug(`before DbFedCommunity.findOneByOrFail()...`, { publicKey: argsPublicKey.asHex() })
|
||||
const fedComA = await DbFedCommunity.findOneByOrFail({ publicKey: argsPublicKey.asBuffer() })
|
||||
// methodLogger.debug(`after DbFedCommunity.findOneByOrFail()...`, new FederatedCommunityLoggingView(fedComA))
|
||||
if (!openConnectionJwtPayload.url.startsWith(fedComA.endPoint)) {
|
||||
const errmsg = `invalid url of community with publicKey` + args.publicKey
|
||||
methodLogger.error(errmsg)
|
||||
// no infos to the caller
|
||||
return true
|
||||
throw new Error(`invalid url of community with publicKey ${argsPublicKey.asHex()}`)
|
||||
}
|
||||
if (fedComA.apiVersion !== apiVersion) {
|
||||
throw new Error(`invalid apiVersion: ${fedComA.apiVersion} of community with publicKey ${argsPublicKey.asHex()}`)
|
||||
}
|
||||
|
||||
// no await to respond immediately and invoke callback-request asynchronously
|
||||
void startOpenConnectionCallback(args.handshakeID, args.publicKey, CONFIG.FEDERATION_API)
|
||||
// important: startOpenConnectionCallback must catch all exceptions them self, or server will crash!
|
||||
void startOpenConnectionCallback(args.handshakeID, argsPublicKey, fedComA)
|
||||
methodLogger.debug('openConnection() successfully initiated callback and returns true immediately...')
|
||||
return true
|
||||
} catch (err) {
|
||||
methodLogger.error('invalid jwt token:', err)
|
||||
// no infos to the caller
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -74,37 +79,29 @@ export class AuthenticationResolver {
|
||||
): Promise<boolean> {
|
||||
const methodLogger = createLogger('openConnectionCallback')
|
||||
methodLogger.addContext('handshakeID', args.handshakeID)
|
||||
methodLogger.debug(`openConnectionCallback() via apiVersion=1_0 ...`, args)
|
||||
methodLogger.debug(`start via apiVersion=${apiVersion}, public key: ${args.publicKey}`)
|
||||
try {
|
||||
// decrypt args.url with homeCom.privateJwtKey and verify signing with callbackFedCom.publicKey
|
||||
const openConnectionCallbackJwtPayload = await interpretEncryptedTransferArgs(args) as OpenConnectionCallbackJwtPayloadType
|
||||
if (!openConnectionCallbackJwtPayload) {
|
||||
const errmsg = `invalid OpenConnectionCallback payload of requesting community with publicKey` + args.publicKey
|
||||
methodLogger.error(errmsg)
|
||||
// no infos to the caller
|
||||
return true
|
||||
throw new Error(`invalid OpenConnectionCallback payload of requesting community with publicKey ${args.publicKey}`)
|
||||
}
|
||||
|
||||
const endPoint = openConnectionCallbackJwtPayload.url.slice(0, openConnectionCallbackJwtPayload.url.lastIndexOf('/') + 1)
|
||||
const apiVersion = openConnectionCallbackJwtPayload.url.slice(openConnectionCallbackJwtPayload.url.lastIndexOf('/') + 1, openConnectionCallbackJwtPayload.url.length)
|
||||
methodLogger.debug(`search fedComB per:`, endPoint, apiVersion)
|
||||
const { endPoint, apiVersion } = splitUrlInEndPointAndApiVersion(openConnectionCallbackJwtPayload.url)
|
||||
// methodLogger.debug(`search fedComB per:`, endPoint, apiVersion)
|
||||
const fedComB = await DbFedCommunity.findOneBy({ endPoint, apiVersion })
|
||||
if (!fedComB) {
|
||||
const errmsg = `unknown callback community with url` + openConnectionCallbackJwtPayload.url
|
||||
methodLogger.error(errmsg)
|
||||
// no infos to the caller
|
||||
return true
|
||||
throw new Error(`unknown callback community for ${endPoint}${apiVersion}`)
|
||||
}
|
||||
methodLogger.debug(
|
||||
`found fedComB and start authentication:`,
|
||||
new FederatedCommunityLoggingView(fedComB),
|
||||
`found fedComB and start authentication: ${fedComB.endPoint}${fedComB.apiVersion}`,
|
||||
)
|
||||
// no await to respond immediately and invoke authenticate-request asynchronously
|
||||
void startAuthentication(args.handshakeID, openConnectionCallbackJwtPayload.oneTimeCode, fedComB)
|
||||
methodLogger.debug('openConnectionCallback() successfully initiated authentication and returns true immediately...')
|
||||
// methodLogger.debug('openConnectionCallback() successfully initiated authentication and returns true immediately...')
|
||||
return true
|
||||
} catch (err) {
|
||||
methodLogger.error('invalid jwt token:', err)
|
||||
// no infos to the caller
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -116,51 +113,80 @@ export class AuthenticationResolver {
|
||||
): Promise<string | null> {
|
||||
const methodLogger = createLogger('authenticate')
|
||||
methodLogger.addContext('handshakeID', args.handshakeID)
|
||||
methodLogger.debug(`authenticate() via apiVersion=1_0 ...`, args)
|
||||
methodLogger.debug(`start via apiVersion=${apiVersion}, public key: ${args.publicKey}`)
|
||||
let state: DbCommunityHandshakeState | null = null
|
||||
const argsPublicKey = new Ed25519PublicKey(args.publicKey)
|
||||
try {
|
||||
const authArgs = await interpretEncryptedTransferArgs(args) as AuthenticationJwtPayloadType
|
||||
// methodLogger.debug(`interpreted authentication payload...authArgs:`, authArgs)
|
||||
if (!authArgs) {
|
||||
const errmsg = `invalid authentication payload of requesting community with publicKey` + args.publicKey
|
||||
methodLogger.error(errmsg)
|
||||
// no infos to the caller
|
||||
return null
|
||||
methodLogger.debug(`interpretEncryptedTransferArgs was called with`, args)
|
||||
throw new Error(`invalid authentication payload of requesting community with publicKey ${argsPublicKey.asHex()}`)
|
||||
}
|
||||
if (!uint32Schema.safeParse(Number(authArgs.oneTimeCode)).success) {
|
||||
const errmsg = `invalid oneTimeCode: ${authArgs.oneTimeCode} for community with publicKey ${authArgs.publicKey}, expect uint32`
|
||||
methodLogger.error(errmsg)
|
||||
// no infos to the caller
|
||||
return null
|
||||
const validOneTimeCode = uint32Schema.safeParse(Number(authArgs.oneTimeCode))
|
||||
if (!validOneTimeCode.success) {
|
||||
throw new Error(
|
||||
`invalid oneTimeCode: ${authArgs.oneTimeCode} for community with publicKey ${argsPublicKey.asHex()}, expect uint32`
|
||||
)
|
||||
}
|
||||
const authCom = await DbCommunity.findOneByOrFail({ communityUuid: authArgs.oneTimeCode })
|
||||
|
||||
state = await findPendingCommunityHandshakeOrFailByOneTimeCode(validOneTimeCode.data)
|
||||
const stateLogic = new CommunityHandshakeStateLogic(state)
|
||||
if (
|
||||
(await stateLogic.isTimeoutUpdate()) ||
|
||||
state.status !== CommunityHandshakeStateType.START_OPEN_CONNECTION_CALLBACK
|
||||
) {
|
||||
throw new Error('No valid pending community handshake found')
|
||||
}
|
||||
state.status = CommunityHandshakeStateType.SUCCESS
|
||||
await state.save()
|
||||
methodLogger.debug('[SUCCESS] community handshake state updated')
|
||||
|
||||
// methodLogger.debug(`search community per oneTimeCode:`, authArgs.oneTimeCode)
|
||||
const authCom = await getCommunityByPublicKeyOrFail(argsPublicKey)
|
||||
if (authCom) {
|
||||
methodLogger.debug('found authCom:', new CommunityLoggingView(authCom))
|
||||
if (authCom.publicKey !== authArgs.publicKey) {
|
||||
const errmsg = `corrupt authentication call detected, oneTimeCode: ${authArgs.oneTimeCode} doesn't belong to caller: ${authArgs.publicKey}`
|
||||
methodLogger.error(errmsg)
|
||||
// no infos to the caller
|
||||
return null
|
||||
methodLogger.debug(`found authCom ${authCom.name}`)
|
||||
const authComPublicKey = new Ed25519PublicKey(authCom.publicKey)
|
||||
// methodLogger.debug('authCom.publicKey', authComPublicKey.asHex())
|
||||
// methodLogger.debug('args.publicKey', argsPublicKey.asHex())
|
||||
if (!authComPublicKey.isSame(argsPublicKey)) {
|
||||
throw new Error(
|
||||
`corrupt authentication call detected, oneTimeCode: ${authArgs.oneTimeCode} doesn't belong to caller: ${argsPublicKey.asHex()}`
|
||||
)
|
||||
}
|
||||
const communityUuid = uuidv4Schema.safeParse(authArgs.uuid)
|
||||
if (!communityUuid.success) {
|
||||
const errmsg = `invalid uuid: ${authArgs.uuid} for community with publicKey ${authArgs.publicKey}`
|
||||
methodLogger.error(errmsg)
|
||||
// no infos to the caller
|
||||
return null
|
||||
throw new Error(
|
||||
`invalid uuid: ${authArgs.uuid} for community with publicKey ${authComPublicKey.asHex()}`
|
||||
)
|
||||
}
|
||||
authCom.communityUuid = communityUuid.data
|
||||
authCom.authenticatedAt = new Date()
|
||||
await DbCommunity.save(authCom)
|
||||
methodLogger.debug('store authCom.uuid successfully:', new CommunityLoggingView(authCom))
|
||||
await authCom.save()
|
||||
methodLogger.debug(`update authCom.uuid successfully with ${authCom.communityUuid} at ${authCom.authenticatedAt}`)
|
||||
|
||||
const homeComB = await getHomeCommunity()
|
||||
if (homeComB?.communityUuid) {
|
||||
const responseArgs = new AuthenticationResponseJwtPayloadType(args.handshakeID,homeComB.communityUuid)
|
||||
const responseJwt = await encryptAndSign(responseArgs, homeComB.privateJwtKey!, authCom.publicJwtKey!)
|
||||
return responseJwt
|
||||
}
|
||||
} else {
|
||||
throw new Error(`community with publicKey ${argsPublicKey.asHex()} not found`)
|
||||
}
|
||||
return null
|
||||
} catch (err) {
|
||||
methodLogger.error('invalid jwt token:', err)
|
||||
if (state) {
|
||||
try {
|
||||
state.status = CommunityHandshakeStateType.FAILED
|
||||
state.lastError = String(err)
|
||||
await state.save()
|
||||
} catch (err) {
|
||||
methodLogger.error(`failed to save state`, new CommunityHandshakeStateLoggingView(state), err)
|
||||
}
|
||||
}
|
||||
methodLogger.error(`failed`, err)
|
||||
// no infos to the caller
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,85 +1,118 @@
|
||||
import { EncryptedTransferArgs } from 'core'
|
||||
import { CommunityHandshakeStateLogic, EncryptedTransferArgs, ensureUrlEndsWithSlash } from 'core'
|
||||
import {
|
||||
CommunityLoggingView,
|
||||
CommunityHandshakeStateLoggingView,
|
||||
Community as DbCommunity,
|
||||
FederatedCommunity as DbFedCommunity,
|
||||
FederatedCommunityLoggingView,
|
||||
findPendingCommunityHandshake,
|
||||
getCommunityByPublicKeyOrFail,
|
||||
getHomeCommunity,
|
||||
getHomeCommunityWithFederatedCommunityOrFail,
|
||||
} from 'database'
|
||||
import { getLogger } from 'log4js'
|
||||
import { validate as validateUUID, version as versionUUID } from 'uuid'
|
||||
|
||||
import { AuthenticationClientFactory } from '@/client/AuthenticationClientFactory'
|
||||
import { randombytes_random } from 'sodium-native'
|
||||
|
||||
import { AuthenticationClient as V1_0_AuthenticationClient } from '@/client/1_0/AuthenticationClient'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import { AuthenticationJwtPayloadType, AuthenticationResponseJwtPayloadType, encryptAndSign, OpenConnectionCallbackJwtPayloadType, uuidv4Schema, verifyAndDecrypt } from 'shared'
|
||||
import {
|
||||
AuthenticationJwtPayloadType,
|
||||
AuthenticationResponseJwtPayloadType,
|
||||
Ed25519PublicKey,
|
||||
encryptAndSign,
|
||||
OpenConnectionCallbackJwtPayloadType,
|
||||
uuidv4Schema,
|
||||
verifyAndDecrypt
|
||||
} from 'shared'
|
||||
import { CommunityHandshakeState as DbCommunityHandshakeState, CommunityHandshakeStateType } from 'database'
|
||||
import { getFederatedCommunityWithApiOrFail } from 'core'
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.util.authenticateCommunity`)
|
||||
const createLogger = (method: string ) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.util.authenticateCommunity.${method}`)
|
||||
|
||||
export async function startOpenConnectionCallback(
|
||||
handshakeID: string,
|
||||
publicKey: string,
|
||||
api: string,
|
||||
publicKey: Ed25519PublicKey,
|
||||
fedComA: DbFedCommunity,
|
||||
): Promise<void> {
|
||||
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.util.authenticateCommunity.startOpenConnectionCallback`)
|
||||
const methodLogger = createLogger('startOpenConnectionCallback')
|
||||
methodLogger.addContext('handshakeID', handshakeID)
|
||||
methodLogger.debug(`Authentication: startOpenConnectionCallback() with:`, {
|
||||
publicKey,
|
||||
})
|
||||
methodLogger.debug(`start`)
|
||||
const api = fedComA.apiVersion
|
||||
|
||||
let state: DbCommunityHandshakeState | null = null
|
||||
try {
|
||||
const homeComB = await getHomeCommunity()
|
||||
const homeFedComB = await DbFedCommunity.findOneByOrFail({
|
||||
foreign: false,
|
||||
apiVersion: api,
|
||||
})
|
||||
const comA = await DbCommunity.findOneByOrFail({ publicKey: Buffer.from(publicKey, 'hex') })
|
||||
const fedComA = await DbFedCommunity.findOneByOrFail({
|
||||
foreign: true,
|
||||
apiVersion: api,
|
||||
publicKey: comA.publicKey,
|
||||
})
|
||||
// store oneTimeCode in requestedCom.community_uuid as authenticate-request-identifier
|
||||
// prevent overwriting valid UUID with oneTimeCode, because this request could be initiated at any time from federated community
|
||||
if (uuidv4Schema.safeParse(comA.communityUuid).success) {
|
||||
throw new Error('Community UUID is already a valid UUID')
|
||||
const pendingState = await findPendingCommunityHandshake(publicKey, api, CommunityHandshakeStateType.START_OPEN_CONNECTION_CALLBACK)
|
||||
if (pendingState) {
|
||||
const stateLogic = new CommunityHandshakeStateLogic(pendingState)
|
||||
// retry on timeout or failure
|
||||
if (!(await stateLogic.isTimeoutUpdate())) {
|
||||
// authentication with community and api version is still in progress and it is not timeout yet
|
||||
methodLogger.debug('existingState, so we exit here', new CommunityHandshakeStateLoggingView(pendingState))
|
||||
return
|
||||
}
|
||||
}
|
||||
// load comA and comB parallel
|
||||
// load with joined federated community of given api version
|
||||
const [homeComB, comA] = await Promise.all([
|
||||
getHomeCommunityWithFederatedCommunityOrFail(api),
|
||||
getCommunityByPublicKeyOrFail(publicKey),
|
||||
])
|
||||
// get federated communities with correct api version
|
||||
// simply check and extract federated community from community of given api version or throw error if not found
|
||||
const homeFedComB = getFederatedCommunityWithApiOrFail(homeComB, api)
|
||||
|
||||
// TODO: make sure it is unique
|
||||
const oneTimeCode = randombytes_random().toString()
|
||||
comA.communityUuid = oneTimeCode
|
||||
await DbCommunity.save(comA)
|
||||
methodLogger.debug(
|
||||
`Authentication: stored oneTimeCode in requestedCom:`,
|
||||
new CommunityLoggingView(comA),
|
||||
)
|
||||
const oneTimeCode = randombytes_random()
|
||||
const oneTimeCodeString = oneTimeCode.toString()
|
||||
|
||||
// Create new community handshake state
|
||||
state = new DbCommunityHandshakeState()
|
||||
state.publicKey = publicKey.asBuffer()
|
||||
state.apiVersion = api
|
||||
state.status = CommunityHandshakeStateType.START_OPEN_CONNECTION_CALLBACK
|
||||
state.handshakeId = parseInt(handshakeID)
|
||||
state.oneTimeCode = oneTimeCode
|
||||
state = await state.save()
|
||||
methodLogger.debug('[START_OPEN_CONNECTION_CALLBACK] community handshake state created')
|
||||
|
||||
const client = AuthenticationClientFactory.getInstance(fedComA)
|
||||
|
||||
if (client instanceof V1_0_AuthenticationClient) {
|
||||
const url = homeFedComB.endPoint.endsWith('/')
|
||||
? homeFedComB.endPoint + homeFedComB.apiVersion
|
||||
: homeFedComB.endPoint + '/' + homeFedComB.apiVersion
|
||||
const url = ensureUrlEndsWithSlash(homeFedComB.endPoint) + homeFedComB.apiVersion
|
||||
|
||||
const callbackArgs = new OpenConnectionCallbackJwtPayloadType(handshakeID, oneTimeCode, url)
|
||||
methodLogger.debug(`Authentication: start openConnectionCallback with args:`, callbackArgs)
|
||||
const callbackArgs = new OpenConnectionCallbackJwtPayloadType(handshakeID, oneTimeCodeString, url)
|
||||
// methodLogger.debug(`Authentication: start openConnectionCallback with args:`, callbackArgs)
|
||||
// encrypt callbackArgs with requestedCom.publicJwtKey and sign it with homeCom.privateJwtKey
|
||||
const jwt = await encryptAndSign(callbackArgs, homeComB!.privateJwtKey!, comA.publicJwtKey!)
|
||||
const jwt = await encryptAndSign(callbackArgs, homeComB.privateJwtKey!, comA.publicJwtKey!)
|
||||
const args = new EncryptedTransferArgs()
|
||||
args.publicKey = homeComB!.publicKey.toString('hex')
|
||||
args.publicKey = new Ed25519PublicKey(homeComB.publicKey).asHex()
|
||||
args.jwt = jwt
|
||||
args.handshakeID = handshakeID
|
||||
methodLogger.debug(`invoke openConnectionCallback(), oneTimeCode: ${oneTimeCodeString}`)
|
||||
const result = await client.openConnectionCallback(args)
|
||||
if (result) {
|
||||
methodLogger.debug('startOpenConnectionCallback() successful:', jwt)
|
||||
methodLogger.debug(`startOpenConnectionCallback() successful`)
|
||||
} else {
|
||||
methodLogger.error('startOpenConnectionCallback() failed:', jwt)
|
||||
methodLogger.debug(`jwt: ${jwt}`)
|
||||
const errorString = 'startOpenConnectionCallback() failed'
|
||||
methodLogger.error(errorString)
|
||||
state.status = CommunityHandshakeStateType.FAILED
|
||||
state.lastError = errorString
|
||||
state = await state.save()
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
methodLogger.error('error in startOpenConnectionCallback:', err)
|
||||
methodLogger.error('error in startOpenConnectionCallback', err)
|
||||
if (state) {
|
||||
try {
|
||||
state.status = CommunityHandshakeStateType.FAILED
|
||||
state.lastError = String(err)
|
||||
state = await state.save()
|
||||
} catch(e) {
|
||||
methodLogger.error('error on saving CommunityHandshakeState', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
methodLogger.removeContext('handshakeID')
|
||||
}
|
||||
|
||||
export async function startAuthentication(
|
||||
@ -87,21 +120,31 @@ export async function startAuthentication(
|
||||
oneTimeCode: string,
|
||||
fedComB: DbFedCommunity,
|
||||
): Promise<void> {
|
||||
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.util.authenticateCommunity.startAuthentication`)
|
||||
const methodLogger = createLogger('startAuthentication')
|
||||
methodLogger.addContext('handshakeID', handshakeID)
|
||||
methodLogger.debug(`startAuthentication()...`, {
|
||||
oneTimeCode,
|
||||
fedComB: new FederatedCommunityLoggingView(fedComB),
|
||||
})
|
||||
methodLogger.debug(`startAuthentication()... oneTimeCode: ${oneTimeCode}`)
|
||||
let state: DbCommunityHandshakeState | null = null
|
||||
try {
|
||||
const fedComBPublicKey = new Ed25519PublicKey(fedComB.publicKey)
|
||||
const homeComA = await getHomeCommunity()
|
||||
const comB = await DbCommunity.findOneByOrFail({
|
||||
foreign: true,
|
||||
publicKey: fedComB.publicKey,
|
||||
publicKey: fedComBPublicKey.asBuffer(),
|
||||
})
|
||||
if (!comB.publicJwtKey) {
|
||||
throw new Error('Public JWT key still not exist for foreign community')
|
||||
}
|
||||
state = await findPendingCommunityHandshake(fedComBPublicKey, fedComB.apiVersion, CommunityHandshakeStateType.START_COMMUNITY_AUTHENTICATION)
|
||||
if (!state) {
|
||||
throw new Error('No pending community handshake found')
|
||||
}
|
||||
const stateLogic = new CommunityHandshakeStateLogic(state)
|
||||
if ((await stateLogic.isTimeoutUpdate())) {
|
||||
methodLogger.debug('invalid state', new CommunityHandshakeStateLoggingView(state))
|
||||
throw new Error('No valid pending community handshake found')
|
||||
}
|
||||
state.status = CommunityHandshakeStateType.START_AUTHENTICATION
|
||||
await state.save()
|
||||
|
||||
const client = AuthenticationClientFactory.getInstance(fedComB)
|
||||
|
||||
@ -110,41 +153,55 @@ export async function startAuthentication(
|
||||
// encrypt authenticationArgs.uuid with fedComB.publicJwtKey and sign it with homeCom.privateJwtKey
|
||||
const jwt = await encryptAndSign(authenticationArgs, homeComA!.privateJwtKey!, comB.publicJwtKey!)
|
||||
const args = new EncryptedTransferArgs()
|
||||
args.publicKey = homeComA!.publicKey.toString('hex')
|
||||
args.publicKey = new Ed25519PublicKey(homeComA!.publicKey).asHex()
|
||||
args.jwt = jwt
|
||||
args.handshakeID = handshakeID
|
||||
methodLogger.debug(`invoke authenticate() with:`, args)
|
||||
methodLogger.debug(`invoke authenticate(), publicKey: ${args.publicKey}`)
|
||||
const responseJwt = await client.authenticate(args)
|
||||
methodLogger.debug(`response of authenticate():`, responseJwt)
|
||||
// methodLogger.debug(`response of authenticate():`, responseJwt)
|
||||
|
||||
if (responseJwt !== null) {
|
||||
const payload = await verifyAndDecrypt(handshakeID, responseJwt, homeComA!.privateJwtKey!, comB.publicJwtKey!) as AuthenticationResponseJwtPayloadType
|
||||
methodLogger.debug(
|
||||
/*methodLogger.debug(
|
||||
`received payload from authenticate ComB:`,
|
||||
payload,
|
||||
new FederatedCommunityLoggingView(fedComB),
|
||||
)
|
||||
)*/
|
||||
if (payload.tokentype !== AuthenticationResponseJwtPayloadType.AUTHENTICATION_RESPONSE_TYPE) {
|
||||
const errmsg = `Invalid tokentype in authenticate-response of community with publicKey` + comB.publicKey
|
||||
methodLogger.error(errmsg)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
throw new Error(errmsg)
|
||||
throw new Error(`Invalid tokentype in authenticate-response of community with publicKey ${fedComBPublicKey.asHex()}`)
|
||||
}
|
||||
if (!payload.uuid || !validateUUID(payload.uuid) || versionUUID(payload.uuid) !== 4) {
|
||||
const errmsg = `Invalid uuid in authenticate-response of community with publicKey` + comB.publicKey
|
||||
methodLogger.error(errmsg)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
throw new Error(errmsg)
|
||||
const parsedUuidv4 = uuidv4Schema.safeParse(payload.uuid)
|
||||
if (!parsedUuidv4.success) {
|
||||
throw new Error(`Invalid uuid in authenticate-response of community with publicKey ${fedComBPublicKey.asHex()}`)
|
||||
}
|
||||
comB.communityUuid = payload.uuid
|
||||
methodLogger.debug('received uuid from authenticate ComB:', parsedUuidv4.data)
|
||||
comB.communityUuid = parsedUuidv4.data
|
||||
comB.authenticatedAt = new Date()
|
||||
await DbCommunity.save(comB)
|
||||
methodLogger.debug('Community Authentication successful:', new CommunityLoggingView(comB))
|
||||
await DbCommunity.save(comB)
|
||||
state.status = CommunityHandshakeStateType.SUCCESS
|
||||
await state.save()
|
||||
methodLogger.debug('[SUCCESS] community handshake state updated')
|
||||
const endTime = new Date()
|
||||
const duration = endTime.getTime() - state.createdAt.getTime()
|
||||
methodLogger.debug(`Community Authentication successful in ${duration} ms`)
|
||||
} else {
|
||||
state.status = CommunityHandshakeStateType.FAILED
|
||||
state.lastError = 'Community Authentication failed, empty response'
|
||||
await state.save()
|
||||
methodLogger.error('Community Authentication failed:', authenticationArgs)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
methodLogger.error('error in startAuthentication:', err)
|
||||
if (state) {
|
||||
try {
|
||||
state.status = CommunityHandshakeStateType.FAILED
|
||||
state.lastError = String(err)
|
||||
await state.save()
|
||||
} catch(e) {
|
||||
methodLogger.error('error on saving CommunityHandshakeState', e)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
methodLogger.removeContext('handshakeID')
|
||||
}
|
||||
|
||||
@ -6,8 +6,10 @@ import { getLogger } from 'log4js'
|
||||
// config
|
||||
import { CONFIG } from './config'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from './config/const'
|
||||
import { onShutdown, printServerCrashAsciiArt, ShutdownReason } from 'shared'
|
||||
|
||||
async function main() {
|
||||
const startTime = new Date()
|
||||
// init logger
|
||||
const log4jsConfigFileName = CONFIG.LOG4JS_CONFIG_PLACEHOLDER.replace('%v', CONFIG.FEDERATION_API)
|
||||
initLogger(
|
||||
@ -27,6 +29,16 @@ async function main() {
|
||||
`GraphIQL available at ${CONFIG.FEDERATION_COMMUNITY_URL}/api/${CONFIG.FEDERATION_API}`,
|
||||
)
|
||||
}
|
||||
onShutdown(async (reason, error) => {
|
||||
if (ShutdownReason.SIGINT === reason || ShutdownReason.SIGTERM === reason) {
|
||||
logger.info(`graceful shutdown: ${reason}`)
|
||||
} else {
|
||||
const endTime = new Date()
|
||||
const duration = endTime.getTime() - startTime.getTime()
|
||||
printServerCrashAsciiArt('Server Crash', `reason: ${reason}`, `server was ${duration}ms online`)
|
||||
logger.error(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -37,6 +37,7 @@
|
||||
"esbuild": "^0.25.2",
|
||||
"jose": "^4.14.4",
|
||||
"log4js": "^6.9.1",
|
||||
"yoctocolors-cjs": "^2.1.2",
|
||||
"zod": "^3.25.61"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
export const DECAY_START_TIME = new Date('2021-05-13T17:46:31Z')
|
||||
export const SECONDS_PER_YEAR_GREGORIAN_CALENDER = 31556952.0
|
||||
export const LOG4JS_BASE_CATEGORY_NAME = 'shared'
|
||||
export const REDEEM_JWT_TOKEN_EXPIRATION = '10m'
|
||||
export const REDEEM_JWT_TOKEN_EXPIRATION = '10m'
|
||||
// 10 minutes
|
||||
export const FEDERATION_AUTHENTICATION_TIMEOUT_MS = 60 * 1000 * 10
|
||||
51
shared/src/helper/BinaryData.ts
Normal file
51
shared/src/helper/BinaryData.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { getLogger } from 'log4js'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '../const'
|
||||
|
||||
const logging = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.helper.BinaryData`)
|
||||
|
||||
/**
|
||||
* Class mainly for handling ed25519 public keys,
|
||||
* to make sure we have always the correct Format (Buffer or Hex String)
|
||||
*/
|
||||
export class BinaryData {
|
||||
private buf: Buffer
|
||||
private hex: string
|
||||
|
||||
constructor(input: Buffer | string | undefined) {
|
||||
if (typeof input === 'string') {
|
||||
this.buf = Buffer.from(input, 'hex')
|
||||
this.hex = input
|
||||
} else if (Buffer.isBuffer(input)) {
|
||||
this.buf = input
|
||||
this.hex = input.toString('hex')
|
||||
} else {
|
||||
this.buf = Buffer.from('')
|
||||
this.hex = ''
|
||||
}
|
||||
}
|
||||
|
||||
asBuffer(): Buffer {
|
||||
return this.buf
|
||||
}
|
||||
|
||||
asHex(): string {
|
||||
return this.hex
|
||||
}
|
||||
|
||||
isSame(other: BinaryData): boolean {
|
||||
if (other === undefined || !(other instanceof BinaryData)) {
|
||||
logging.error('other is invalid', other)
|
||||
return false
|
||||
}
|
||||
return this.buf.compare(other.asBuffer()) === 0
|
||||
}
|
||||
}
|
||||
|
||||
export class Ed25519PublicKey extends BinaryData {
|
||||
constructor(input: Buffer | string | undefined) {
|
||||
super(input)
|
||||
if (this.asBuffer().length !== 32) {
|
||||
throw new Error('Invalid ed25519 public key length')
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1 +1,3 @@
|
||||
export * from './updateField'
|
||||
export * from './updateField'
|
||||
export * from './BinaryData'
|
||||
export * from './onShutdown'
|
||||
51
shared/src/helper/onShutdown.ts
Normal file
51
shared/src/helper/onShutdown.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { Logger } from 'log4js'
|
||||
import colors from 'yoctocolors-cjs'
|
||||
|
||||
export enum ShutdownReason {
|
||||
SIGINT = 'SIGINT',
|
||||
SIGTERM = 'SIGTERM',
|
||||
UNCAUGHT_EXCEPTION = 'UNCAUGHT_EXCEPTION',
|
||||
UNCAUGHT_REJECTION = 'UNCAUGHT_REJECTION',
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Setup graceful shutdown for the process
|
||||
* @param gracefulShutdown will be called if process is terminated
|
||||
*/
|
||||
export function onShutdown(shutdownHandler: (reason: ShutdownReason, error?: Error | any) => Promise<void>) {
|
||||
const signals: NodeJS.Signals[] = ['SIGINT', 'SIGTERM']
|
||||
signals.forEach(sig => {
|
||||
process.on(sig, async () => {
|
||||
await shutdownHandler(sig as ShutdownReason)
|
||||
process.exit(0)
|
||||
})
|
||||
})
|
||||
|
||||
process.on('uncaughtException', async (err) => {
|
||||
await shutdownHandler(ShutdownReason.UNCAUGHT_EXCEPTION, err)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
process.on('unhandledRejection', async (err) => {
|
||||
await shutdownHandler(ShutdownReason.UNCAUGHT_REJECTION, err)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
if (process.platform === "win32") {
|
||||
const rl = require("readline").createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
})
|
||||
rl.on("SIGINT", () => {
|
||||
process.emit("SIGINT" as any)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function printServerCrashAsciiArt(msg1: string, msg2: string, msg3: string) {
|
||||
console.error(colors.redBright(` /\\_/\\ ${msg1}`))
|
||||
console.error(colors.redBright(`( x.x ) ${msg2}`))
|
||||
console.error(colors.redBright(` > < ${msg3}`))
|
||||
console.error(colors.redBright(''))
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
export * from './schema'
|
||||
export * from './enum'
|
||||
export * from './const'
|
||||
export * from './helper'
|
||||
export * from './logic/decay'
|
||||
export * from './jwt/JWT'
|
||||
|
||||
@ -43,11 +43,9 @@ export const verify = async (handshakeID: string, token: string, publicKey: stri
|
||||
})
|
||||
payload.handshakeID = handshakeID
|
||||
methodLogger.debug('verify after jwtVerify... payload=', payload)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
return payload as JwtPayloadType
|
||||
} catch (err) {
|
||||
methodLogger.error('verify after jwtVerify... error=', err)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
return null
|
||||
}
|
||||
}
|
||||
@ -74,11 +72,9 @@ export const encode = async (payload: JwtPayloadType, privatekey: string): Promi
|
||||
.setExpirationTime(payload.expiration)
|
||||
.sign(secret)
|
||||
methodLogger.debug('encode... token=', token)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
return token
|
||||
} catch (e) {
|
||||
methodLogger.error('Failed to sign JWT:', e)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
throw e
|
||||
}
|
||||
}
|
||||
@ -111,11 +107,9 @@ export const encrypt = async (payload: JwtPayloadType, publicKey: string): Promi
|
||||
.setProtectedHeader({ alg: 'RSA-OAEP-256', enc: 'A256GCM' })
|
||||
.encrypt(recipientKey)
|
||||
methodLogger.debug('encrypt... jwe=', jwe)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
return jwe.toString()
|
||||
} catch (e) {
|
||||
methodLogger.error('Failed to encrypt JWT:', e)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
throw e
|
||||
}
|
||||
}
|
||||
@ -131,11 +125,9 @@ export const decrypt = async(handshakeID: string, jwe: string, privateKey: strin
|
||||
await compactDecrypt(jwe, decryptKey)
|
||||
methodLogger.debug('decrypt... plaintext=', plaintext)
|
||||
methodLogger.debug('decrypt... protectedHeader=', protectedHeader)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
return new TextDecoder().decode(plaintext)
|
||||
} catch (e) {
|
||||
methodLogger.error('Failed to decrypt JWT:', e)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
throw e
|
||||
}
|
||||
}
|
||||
@ -147,7 +139,6 @@ export const encryptAndSign = async (payload: JwtPayloadType, privateKey: string
|
||||
methodLogger.debug('encryptAndSign... jwe=', jwe)
|
||||
const jws = await encode(new EncryptedJWEJwtPayloadType(payload.handshakeID, jwe), privateKey)
|
||||
methodLogger.debug('encryptAndSign... jws=', jws)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
return jws
|
||||
}
|
||||
|
||||
@ -171,6 +162,5 @@ export const verifyAndDecrypt = async (handshakeID: string, token: string, priva
|
||||
methodLogger.debug('verifyAndDecrypt... jwe=', jwe)
|
||||
const payload = await decrypt(handshakeID, jwe as string, privateKey)
|
||||
methodLogger.debug('verifyAndDecrypt... payload=', payload)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
return JSON.parse(payload) as JwtPayloadType
|
||||
}
|
||||
|
||||
@ -4,4 +4,4 @@ import { validate, version } from 'uuid'
|
||||
export const uuidv4Schema = string().refine((val: string) => validate(val) && version(val) === 4, 'Invalid uuid')
|
||||
export const emailSchema = string().email()
|
||||
export const urlSchema = string().url()
|
||||
export const uint32Schema = number().positive().lte(4294967295)
|
||||
export const uint32Schema = number().positive().lte(4294967295)
|
||||
|
||||
35
shared/src/schema/community.schema.test.ts
Normal file
35
shared/src/schema/community.schema.test.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { communityAuthenticatedSchema } from './community.schema'
|
||||
import { describe, it, expect } from 'bun:test'
|
||||
|
||||
|
||||
describe('communityAuthenticatedSchema', () => {
|
||||
it('should return an error if communityUuid is not a uuidv4', () => {
|
||||
const data = communityAuthenticatedSchema.safeParse({
|
||||
communityUuid: '1234567890',
|
||||
authenticatedAt: new Date(),
|
||||
})
|
||||
|
||||
expect(data.success).toBe(false)
|
||||
expect(data.error?.issues[0].path).toEqual(['communityUuid'])
|
||||
})
|
||||
|
||||
it('should return an error if authenticatedAt is not a date', () => {
|
||||
const data = communityAuthenticatedSchema.safeParse({
|
||||
communityUuid: uuidv4(),
|
||||
authenticatedAt: '2022-01-01',
|
||||
})
|
||||
|
||||
expect(data.success).toBe(false)
|
||||
expect(data.error?.issues[0].path).toEqual(['authenticatedAt'])
|
||||
})
|
||||
|
||||
it('should return no error for valid data and valid uuid4', () => {
|
||||
const data = communityAuthenticatedSchema.safeParse({
|
||||
communityUuid: uuidv4(),
|
||||
authenticatedAt: new Date(),
|
||||
})
|
||||
|
||||
expect(data.success).toBe(true)
|
||||
})
|
||||
})
|
||||
7
shared/src/schema/community.schema.ts
Normal file
7
shared/src/schema/community.schema.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { object, date, array, string } from 'zod'
|
||||
import { uuidv4Schema } from './base.schema'
|
||||
|
||||
export const communityAuthenticatedSchema = object({
|
||||
communityUuid: uuidv4Schema,
|
||||
authenticatedAt: date(),
|
||||
})
|
||||
@ -1,2 +1,3 @@
|
||||
export * from './user.schema'
|
||||
export * from './base.schema'
|
||||
export * from './base.schema'
|
||||
export * from './community.schema'
|
||||
Loading…
x
Reference in New Issue
Block a user