mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
refactor startOpenConnectionCallback
This commit is contained in:
parent
ae23aafd87
commit
ced6f42fa0
@ -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
|
||||
|
||||
13
core/src/logic/Community.logic.ts
Normal file
13
core/src/logic/Community.logic.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -1 +1,2 @@
|
||||
export { CommunityHandshakeStateLogic } from './CommunityHandshakeState.logic'
|
||||
export { CommunityHandshakeStateLogic } from './CommunityHandshakeState.logic'
|
||||
export { CommunityLogic } from './Community.logic'
|
||||
@ -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',
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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(),
|
||||
})
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user