refactor startOpenConnectionCallback

This commit is contained in:
einhornimmond 2025-10-13 10:11:27 +02:00
parent ae23aafd87
commit ced6f42fa0
10 changed files with 126 additions and 78 deletions

View File

@ -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

View File

@ -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
}
}

View File

@ -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<boolean> {
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
}
}

View File

@ -1 +1,2 @@
export { CommunityHandshakeStateLogic } from './CommunityHandshakeState.logic'
export { CommunityHandshakeStateLogic } from './CommunityHandshakeState.logic'
export { CommunityLogic } from './Community.logic'

View File

@ -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',

View File

@ -49,6 +49,16 @@ export async function getCommunityWithFederatedCommunityByIdentifier(
})
}
export async function getCommunityWithFederatedCommunityWithApiOrFail(
publicKey: Buffer,
apiVersion: string
): Promise<DbCommunity> {
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(

View File

@ -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<CommunityHandshakeState | null> {
export function findPendingCommunityHandshake(
publicKey: Buffer, apiVersion: string, withRelations = true
): Promise<CommunityHandshakeState | null> {
return CommunityHandshakeState.findOne({
where: {
publicKey: federatedCommunity.publicKey,
apiVersion: federatedCommunity.apiVersion,
publicKey,
apiVersion,
status: Not(In([
CommunityHandshakeStateType.EXPIRED,
CommunityHandshakeStateType.FAILED,

View File

@ -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')

View File

@ -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<DbCommunityHandshakeState> {
methodLogger.error(error)
state.status = CommunityHandshakeStateType.FAILED
state.lastError = error
return state.save()
}
export async function startOpenConnectionCallback(
handshakeID: string,
publicKey: string,
api: string,
): 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,
})
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<DbCommunityHandshakeState> | 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(

View File

@ -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(),
})
})