From ced6f42fa0c9214764ca85d3592bc3474bb28544 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Mon, 13 Oct 2025 10:11:27 +0200 Subject: [PATCH] refactor startOpenConnectionCallback --- .../src/federation/authenticateCommunities.ts | 15 +-- core/src/logic/Community.logic.ts | 13 ++ .../logic/CommunityHandshakeState.logic.ts | 14 +- core/src/logic/index.ts | 3 +- .../src/enum/CommunityHandshakeStateType.ts | 2 +- database/src/queries/communities.ts | 10 ++ database/src/queries/communityHandshakes.ts | 10 +- .../1_0/resolver/AuthenticationResolver.ts | 10 +- .../api/1_0/util/authenticateCommunity.ts | 123 ++++++++++++------ shared/src/schema/community.schema.ts | 4 +- 10 files changed, 126 insertions(+), 78 deletions(-) create mode 100644 core/src/logic/Community.logic.ts diff --git a/backend/src/federation/authenticateCommunities.ts b/backend/src/federation/authenticateCommunities.ts index 4b2e5b77b..d71314efa 100644 --- a/backend/src/federation/authenticateCommunities.ts +++ b/backend/src/federation/authenticateCommunities.ts @@ -21,6 +21,7 @@ import { getLogger } from 'log4js' import { AuthenticationClientFactory } from './client/AuthenticationClientFactory' import { EncryptedTransferArgs } from 'core' import { CommunityHandshakeStateLogic } from 'core' +import { CommunityLogic } from 'core' const createLogger = (functionName: string) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.authenticateCommunities.${functionName}`) @@ -35,16 +36,8 @@ export async function startCommunityAuthentication( }) const homeComA = await getHomeCommunityWithFederatedCommunityOrFail(fedComB.apiVersion) methodLogger.debug('homeComA', new CommunityLoggingView(homeComA)) - // check if result is like expected - // TODO: use zod/valibot - if ( - !homeComA.federatedCommunities || - homeComA.federatedCommunities.length === 0 || - homeComA.federatedCommunities[0].apiVersion !== fedComB.apiVersion - ) { - throw new Error(`Missing home community or federated community with api version ${fedComB.apiVersion}`) - } - const homeFedComA = homeComA.federatedCommunities[0] + const homeComALogic = new CommunityLogic(homeComA) + const homeFedComA = homeComALogic.getFederatedCommunityWithApiOrFail(fedComB.apiVersion) const comB = await DbCommunity.findOneByOrFail({ publicKey: fedComB.publicKey }) methodLogger.debug('started with comB:', new CommunityLoggingView(comB)) // check if communityUuid is not a valid v4Uuid @@ -61,7 +54,7 @@ export async function startCommunityAuthentication( ) // check if a authentication is already in progress - const existingState = await findPendingCommunityHandshake(fedComB, false) + const existingState = await findPendingCommunityHandshake(fedComB.publicKey, fedComB.apiVersion, false) if (existingState) { const stateLogic = new CommunityHandshakeStateLogic(existingState) // retry on timeout or failure diff --git a/core/src/logic/Community.logic.ts b/core/src/logic/Community.logic.ts new file mode 100644 index 000000000..40779c703 --- /dev/null +++ b/core/src/logic/Community.logic.ts @@ -0,0 +1,13 @@ +import { Community as DbCommunity, FederatedCommunity as DbFederatedCommunity } from 'database' + +export class CommunityLogic { + public constructor(private self: DbCommunity) {} + + public getFederatedCommunityWithApiOrFail(apiVersion: string): DbFederatedCommunity { + const fedCom = this.self.federatedCommunities?.find((fedCom) => fedCom.apiVersion === apiVersion) + if (!fedCom) { + throw new Error(`Missing federated community with api version ${apiVersion}`) + } + return fedCom + } +} \ No newline at end of file diff --git a/core/src/logic/CommunityHandshakeState.logic.ts b/core/src/logic/CommunityHandshakeState.logic.ts index 03bf9a762..e9cb23d5c 100644 --- a/core/src/logic/CommunityHandshakeState.logic.ts +++ b/core/src/logic/CommunityHandshakeState.logic.ts @@ -2,7 +2,7 @@ import { CommunityHandshakeState, CommunityHandshakeStateType } from 'database' import { FEDERATION_AUTHENTICATION_TIMEOUT_MS } from 'shared' export class CommunityHandshakeStateLogic { - public constructor(private communityHandshakeStateEntity: CommunityHandshakeState) {} + public constructor(private self: CommunityHandshakeState) {} /** * Check for expired state and if not, check timeout and update (write into db) to expired state @@ -10,24 +10,24 @@ export class CommunityHandshakeStateLogic { */ public async isTimeoutUpdate(): Promise { const timeout = this.isTimeout() - if (timeout && this.communityHandshakeStateEntity.status !== CommunityHandshakeStateType.EXPIRED) { - this.communityHandshakeStateEntity.status = CommunityHandshakeStateType.EXPIRED - await this.communityHandshakeStateEntity.save() + if (timeout && this.self.status !== CommunityHandshakeStateType.EXPIRED) { + this.self.status = CommunityHandshakeStateType.EXPIRED + await this.self.save() } return timeout } public isTimeout(): boolean { - if (this.communityHandshakeStateEntity.status === CommunityHandshakeStateType.EXPIRED) { + if (this.self.status === CommunityHandshakeStateType.EXPIRED) { return true } - if (Date.now() - this.communityHandshakeStateEntity.updatedAt.getTime() > FEDERATION_AUTHENTICATION_TIMEOUT_MS) { + if (Date.now() - this.self.updatedAt.getTime() > FEDERATION_AUTHENTICATION_TIMEOUT_MS) { return true } return false } public isFailed(): boolean { - return this.communityHandshakeStateEntity.status === CommunityHandshakeStateType.FAILED + return this.self.status === CommunityHandshakeStateType.FAILED } } diff --git a/core/src/logic/index.ts b/core/src/logic/index.ts index 3a5c48f58..809465850 100644 --- a/core/src/logic/index.ts +++ b/core/src/logic/index.ts @@ -1 +1,2 @@ -export { CommunityHandshakeStateLogic } from './CommunityHandshakeState.logic' \ No newline at end of file +export { CommunityHandshakeStateLogic } from './CommunityHandshakeState.logic' +export { CommunityLogic } from './Community.logic' \ No newline at end of file diff --git a/database/src/enum/CommunityHandshakeStateType.ts b/database/src/enum/CommunityHandshakeStateType.ts index a047e740d..5378974aa 100644 --- a/database/src/enum/CommunityHandshakeStateType.ts +++ b/database/src/enum/CommunityHandshakeStateType.ts @@ -1,6 +1,6 @@ export enum CommunityHandshakeStateType { START_COMMUNITY_AUTHENTICATION = 'START_COMMUNITY_AUTHENTICATION', - OPEN_CONNECTION = 'OPEN_CONNECTION', + START_OPEN_CONNECTION_CALLBACK = 'START_OPEN_CONNECTION_CALLBACK', OPEN_CONNECTION_CALLBACK = 'OPEN_CONNECTION_CALLBACK', SUCCESS = 'SUCCESS', diff --git a/database/src/queries/communities.ts b/database/src/queries/communities.ts index 265be41d4..22bfdb39f 100644 --- a/database/src/queries/communities.ts +++ b/database/src/queries/communities.ts @@ -49,6 +49,16 @@ export async function getCommunityWithFederatedCommunityByIdentifier( }) } +export async function getCommunityWithFederatedCommunityWithApiOrFail( + publicKey: Buffer, + apiVersion: string +): Promise { + return await DbCommunity.findOneOrFail({ + where: { foreign: true, publicKey, federatedCommunities: { apiVersion } }, + relations: { federatedCommunities: true }, + }) +} + // returns all reachable communities // home community and all federated communities which have been verified within the last authenticationTimeoutMs export async function getReachableCommunities( diff --git a/database/src/queries/communityHandshakes.ts b/database/src/queries/communityHandshakes.ts index 3248b311e..3902fb26b 100644 --- a/database/src/queries/communityHandshakes.ts +++ b/database/src/queries/communityHandshakes.ts @@ -1,5 +1,5 @@ import { Not, In } from 'typeorm' -import { CommunityHandshakeState, CommunityHandshakeStateType, FederatedCommunity} from '..' +import { CommunityHandshakeState, CommunityHandshakeStateType} from '..' /** * Find a pending community handshake by public key. @@ -7,11 +7,13 @@ import { CommunityHandshakeState, CommunityHandshakeStateType, FederatedCommunit * @param withRelations Whether to include the federated community and community in the result, default true. * @returns The CommunityHandshakeState with associated federated community and community. */ -export function findPendingCommunityHandshake(federatedCommunity: FederatedCommunity, withRelations = true): Promise { +export function findPendingCommunityHandshake( + publicKey: Buffer, apiVersion: string, withRelations = true +): Promise { return CommunityHandshakeState.findOne({ where: { - publicKey: federatedCommunity.publicKey, - apiVersion: federatedCommunity.apiVersion, + publicKey, + apiVersion, status: Not(In([ CommunityHandshakeStateType.EXPIRED, CommunityHandshakeStateType.FAILED, diff --git a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts index dc6e1347b..74f75239e 100644 --- a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts @@ -6,11 +6,9 @@ import { Community as DbCommunity, FederatedCommunity as DbFedCommunity, FederatedCommunityLoggingView, - CommunityHandshakeStateType, - CommunityHandshakeState as DbCommunityHandshakeState, getHomeCommunity, } from 'database' -import { getLogger, Logger } from 'log4js' +import { getLogger } from 'log4js' import { AuthenticationJwtPayloadType, AuthenticationResponseJwtPayloadType, @@ -28,12 +26,6 @@ const createLogger = (method: string ) => getLogger(`${LOG4JS_BASE_CATEGORY_NAM @Resolver() export class AuthenticationResolver { - private async errorState(errmsg: string, methodLogger: Logger, state: DbCommunityHandshakeState) { - methodLogger.error(errmsg) - state.lastError = errmsg - await state.save() - } - @Mutation(() => Boolean) async openConnection( @Arg('data') diff --git a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts index 4d8af9187..3ffd4b3d5 100644 --- a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts +++ b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts @@ -1,12 +1,16 @@ -import { EncryptedTransferArgs } from 'core' +import { CommunityHandshakeStateLogic, CommunityLogic, EncryptedTransferArgs, ensureUrlEndsWithSlash } from 'core' import { + CommunityHandshakeStateLoggingView, CommunityLoggingView, Community as DbCommunity, FederatedCommunity as DbFedCommunity, FederatedCommunityLoggingView, + findPendingCommunityHandshake, + getCommunityWithFederatedCommunityWithApiOrFail, getHomeCommunity, + getHomeCommunityWithFederatedCommunityOrFail, } from 'database' -import { getLogger } from 'log4js' +import { getLogger, Logger } from 'log4js' import { validate as validateUUID, version as versionUUID } from 'uuid' import { AuthenticationClientFactory } from '@/client/AuthenticationClientFactory' @@ -14,80 +18,113 @@ 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, uint32Schema, uuidv4Schema, verifyAndDecrypt } from 'shared' -import { FEDERATION_AUTHENTICATION_TIMEOUT_MS } from 'shared' +import { + AuthenticationJwtPayloadType, + AuthenticationResponseJwtPayloadType, + encryptAndSign, + OpenConnectionCallbackJwtPayloadType, + verifyAndDecrypt +} from 'shared' +import { CommunityHandshakeState as DbCommunityHandshakeState, CommunityHandshakeStateType } from 'database' -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}`) + +async function errorState( + error: string, + methodLogger: Logger, + state: DbCommunityHandshakeState, +): Promise { + methodLogger.error(error) + state.status = CommunityHandshakeStateType.FAILED + state.lastError = error + return state.save() +} export async function startOpenConnectionCallback( handshakeID: string, publicKey: string, api: string, ): Promise { - 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, }) - 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) { - methodLogger.debug('Community UUID is already a valid UUID') + const publicKeyBuffer = Buffer.from(publicKey, 'hex') + const pendingState = await findPendingCommunityHandshake(publicKeyBuffer, api, false) + 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', new CommunityHandshakeStateLoggingView(pendingState)) return - // check for still ongoing authentication, but with timeout - } else if (uint32Schema.safeParse(Number(comA.communityUuid)).success) { - if (comA.updatedAt && (Date.now() - comA.updatedAt.getTime()) < FEDERATION_AUTHENTICATION_TIMEOUT_MS) { - methodLogger.debug('Community UUID is still in authentication...oneTimeCode=', comA.communityUuid) - return - } } + } + let stateSaveResolver: Promise | undefined = undefined + const state = new DbCommunityHandshakeState() + try { + const [homeComB, comA] = await Promise.all([ + getHomeCommunityWithFederatedCommunityOrFail(api), + getCommunityWithFederatedCommunityWithApiOrFail(publicKeyBuffer, api), + ]) + // load helpers + const homeComBLogic = new CommunityLogic(homeComB) + const comALogic = new CommunityLogic(comA) + // get federated communities with correct api version + const homeFedComB = homeComBLogic.getFederatedCommunityWithApiOrFail(api) + const fedComA = comALogic.getFederatedCommunityWithApiOrFail(api) + // TODO: make sure it is unique - const oneTimeCode = randombytes_random().toString() - comA.communityUuid = oneTimeCode - await DbCommunity.save(comA) + const oneTimeCode = randombytes_random() + const oneTimeCodeString = oneTimeCode.toString() + + state.publicKey = publicKeyBuffer + state.apiVersion = api + state.status = CommunityHandshakeStateType.START_OPEN_CONNECTION_CALLBACK + state.handshakeId = parseInt(handshakeID) + state.oneTimeCode = oneTimeCode + stateSaveResolver = state.save() methodLogger.debug( - `Authentication: stored oneTimeCode in requestedCom:`, - new CommunityLoggingView(comA), + `Authentication: store oneTimeCode in CommunityHandshakeState:`, + new CommunityHandshakeStateLoggingView(state), ) 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) + 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 = homeComB.publicKey.toString('hex') args.jwt = jwt args.handshakeID = handshakeID + await stateSaveResolver const result = await client.openConnectionCallback(args) if (result) { - methodLogger.debug('startOpenConnectionCallback() successful:', jwt) + methodLogger.debug(`startOpenConnectionCallback() successful: ${jwt}`) } else { - methodLogger.error('startOpenConnectionCallback() failed:', jwt) + methodLogger.debug(`jwt: ${jwt}`) + stateSaveResolver = errorState('startOpenConnectionCallback() failed', methodLogger, state) } } } catch (err) { - methodLogger.error('error in startOpenConnectionCallback:', err) + let errorString: string = '' + if (err instanceof Error) { + errorString = err.message + } else { + errorString = String(err) + } + stateSaveResolver = errorState(`error in startOpenConnectionCallback: ${errorString}`, methodLogger, state) + } finally { + if (stateSaveResolver) { + await stateSaveResolver + } } - methodLogger.removeContext('handshakeID') } export async function startAuthentication( diff --git a/shared/src/schema/community.schema.ts b/shared/src/schema/community.schema.ts index 6067e3538..6957b16b0 100644 --- a/shared/src/schema/community.schema.ts +++ b/shared/src/schema/community.schema.ts @@ -1,7 +1,7 @@ -import { object, date } from 'zod' +import { object, date, array, string } from 'zod' import { uuidv4Schema } from './base.schema' export const communityAuthenticatedSchema = object({ communityUuid: uuidv4Schema, authenticatedAt: date(), -}) \ No newline at end of file +})