diff --git a/backend/src/federation/authenticateCommunities.ts b/backend/src/federation/authenticateCommunities.ts index 6a06889f5..a5beb3333 100644 --- a/backend/src/federation/authenticateCommunities.ts +++ b/backend/src/federation/authenticateCommunities.ts @@ -1,6 +1,6 @@ import { CommunityLoggingView, Community as DbCommunity, FederatedCommunity as DbFederatedCommunity, FederatedCommunityLoggingView, getHomeCommunity } from 'database' import { validate as validateUUID, version as versionUUID } from 'uuid' - +import { randombytes_random } from 'sodium-native' import { CONFIG } from '@/config' import { AuthenticationClient as V1_0_AuthenticationClient } from '@/federation/client/1_0/AuthenticationClient' @@ -16,6 +16,8 @@ const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.authenticateCo export async function startCommunityAuthentication( fedComB: DbFederatedCommunity, ): Promise { + const handshakeID = randombytes_random().toString() + logger.addContext('handshakeID', handshakeID) logger.debug(`startCommunityAuthentication()...`, { fedComB: new FederatedCommunityLoggingView(fedComB), }) @@ -37,7 +39,7 @@ export async function startCommunityAuthentication( validateUUID(comB.communityUuid) && versionUUID(comB.communityUuid) === 4)) ) { - logger.debug('comB has a valid v4Uuid and not still a temporary onetimecode') + logger.debug('comB.uuid is null or is a valid v4Uuid...', comB.communityUuid, comB.authenticatedAt) const client = AuthenticationClientFactory.getInstance(fedComB) if (client instanceof V1_0_AuthenticationClient) { @@ -45,7 +47,7 @@ export async function startCommunityAuthentication( 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( + const payload = new OpenConnectionJwtPayloadType(handshakeID, ensureUrlEndsWithSlash(homeFedComA.endPoint).concat(homeFedComA.apiVersion), ) logger.debug('payload', payload) @@ -68,4 +70,5 @@ export async function startCommunityAuthentication( } catch (err) { logger.error(`Error:`, err) } + logger.removeContext('handshakeID') } diff --git a/backend/src/federation/client/1_0/AuthenticationClient.ts b/backend/src/federation/client/1_0/AuthenticationClient.ts index 0d478a521..b504c0030 100644 --- a/backend/src/federation/client/1_0/AuthenticationClient.ts +++ b/backend/src/federation/client/1_0/AuthenticationClient.ts @@ -5,7 +5,7 @@ import { ensureUrlEndsWithSlash } from '@/util/utilities' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { getLogger } from 'log4js' -import { OpenConnectionArgs } from './model/OpenConnectionArgs' +import { EncryptedTransferArgs } from 'core/src/graphql/model/EncryptedTransferArgs' import { openConnection } from './query/openConnection' const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.client.1_0.AuthenticationClient`) @@ -27,7 +27,7 @@ export class AuthenticationClient { }) } - async openConnection(args: OpenConnectionArgs): Promise { + async openConnection(args: EncryptedTransferArgs): Promise { logger.debug(`openConnection at ${this.endpoint} for args:`, args) try { const { data } = await this.client.rawRequest<{ openConnection: boolean }>(openConnection, { diff --git a/core/src/auth/jwt/JWT.ts b/core/src/auth/jwt/JWT.ts index 566eb51e2..2153cd3fc 100644 --- a/core/src/auth/jwt/JWT.ts +++ b/core/src/auth/jwt/JWT.ts @@ -21,7 +21,7 @@ export const createKeyPair = async (): Promise<{ publicKey: string; privateKey: return { publicKey: publicKeyPem, privateKey: privateKeyPem }; } -export const verify = async (token: string, publicKey: string): Promise => { +export const verify = async (handshakeID: string, token: string, publicKey: string): Promise => { if (!token) { logger.error('verify... token is empty') throw new Error('401 Unauthorized') @@ -39,6 +39,7 @@ export const verify = async (token: string, publicKey: string): Promise { } export const encrypt = async (payload: JwtPayloadType, publicKey: string): Promise => { + logger.addContext('handshakeID', payload.handshakeID) logger.debug('encrypt... payload=', payload) logger.debug('encrypt... publicKey=', publicKey) try { @@ -123,15 +125,18 @@ export const decrypt = async(jwe: string, privateKey: string): Promise = } export const encryptAndSign = async (payload: JwtPayloadType, privateKey: string, publicKey: string): Promise => { + logger.addContext('handshakeID', payload.handshakeID) const jwe = await encrypt(payload, publicKey) logger.debug('encryptAndSign... jwe=', jwe) - const jws = await encode(new EncryptedJWEJwtPayloadType(jwe), privateKey) + const jws = await encode(new EncryptedJWEJwtPayloadType(payload.handshakeID, jwe), privateKey) logger.debug('encryptAndSign... jws=', jws) + logger.removeContext('handshakeID') return jws } -export const verifyAndDecrypt = async (token: string, privateKey: string, publicKey: string): Promise => { - const jweVerifyResult = await verify(token, publicKey) +export const verifyAndDecrypt = async (handshakeID: string, token: string, privateKey: string, publicKey: string): Promise => { + logger.addContext('handshakeID', handshakeID) + const jweVerifyResult = await verify(handshakeID, token, publicKey) if (!jweVerifyResult) { return null } @@ -148,5 +153,6 @@ export const verifyAndDecrypt = async (token: string, privateKey: string, public logger.debug('verifyAndDecrypt... jwe=', jwe) const payload = await decrypt(jwe as string, privateKey) logger.debug('verifyAndDecrypt... payload=', payload) + logger.removeContext('handshakeID') return JSON.parse(payload) as JwtPayloadType } diff --git a/core/src/auth/jwt/payloadtypes/AuthenticationJwtPayloadType.ts b/core/src/auth/jwt/payloadtypes/AuthenticationJwtPayloadType.ts index d8e572d5e..bd7aef013 100644 --- a/core/src/auth/jwt/payloadtypes/AuthenticationJwtPayloadType.ts +++ b/core/src/auth/jwt/payloadtypes/AuthenticationJwtPayloadType.ts @@ -7,12 +7,11 @@ export class AuthenticationJwtPayloadType extends JwtPayloadType { uuid: string constructor( + handshakeID: string, oneTimeCode: string, uuid: string, ) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - super() - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + super(handshakeID) this.tokentype = AuthenticationJwtPayloadType.AUTHENTICATION_TYPE this.oneTimeCode = oneTimeCode this.uuid = uuid diff --git a/core/src/auth/jwt/payloadtypes/AuthenticationResponseJwtPayloadType.ts b/core/src/auth/jwt/payloadtypes/AuthenticationResponseJwtPayloadType.ts index 5a3342b39..ea220a713 100644 --- a/core/src/auth/jwt/payloadtypes/AuthenticationResponseJwtPayloadType.ts +++ b/core/src/auth/jwt/payloadtypes/AuthenticationResponseJwtPayloadType.ts @@ -6,11 +6,10 @@ export class AuthenticationResponseJwtPayloadType extends JwtPayloadType { uuid: string constructor( + handshakeID: string, uuid: string, ) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - super() - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + super(handshakeID) this.tokentype = AuthenticationResponseJwtPayloadType.AUTHENTICATION_RESPONSE_TYPE this.uuid = uuid } diff --git a/core/src/auth/jwt/payloadtypes/EncryptedJWEJwtPayloadType.ts b/core/src/auth/jwt/payloadtypes/EncryptedJWEJwtPayloadType.ts index 152f6161f..f353d885b 100644 --- a/core/src/auth/jwt/payloadtypes/EncryptedJWEJwtPayloadType.ts +++ b/core/src/auth/jwt/payloadtypes/EncryptedJWEJwtPayloadType.ts @@ -6,10 +6,11 @@ export class EncryptedJWEJwtPayloadType extends JwtPayloadType { jwe: string constructor( + handshakeID: string, jwe: string, ) { // eslint-disable-next-line @typescript-eslint/no-unsafe-call - super() + super(handshakeID) // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment this.tokentype = EncryptedJWEJwtPayloadType.ENCRYPTED_JWE_TYPE this.jwe = jwe diff --git a/core/src/auth/jwt/payloadtypes/JwtPayloadType.ts b/core/src/auth/jwt/payloadtypes/JwtPayloadType.ts index 1379653cc..3ed4b466f 100644 --- a/core/src/auth/jwt/payloadtypes/JwtPayloadType.ts +++ b/core/src/auth/jwt/payloadtypes/JwtPayloadType.ts @@ -15,10 +15,12 @@ export class JwtPayloadType implements JWTPayload { iss?: string | undefined; [propName: string]: unknown + handshakeID: string // used as logger context during authentication handshake between comA and comB tokentype: string expiration: string // in minutes (format: 10m for ten minutes) - constructor() { + constructor(handshakeID: string) { this.tokentype = 'unknown jwt type' this.expiration = REDEEM_JWT_TOKEN_EXPIRATION || '10m' + this.handshakeID = handshakeID } } diff --git a/core/src/auth/jwt/payloadtypes/OpenConnectionCallbackJwtPayloadType.ts b/core/src/auth/jwt/payloadtypes/OpenConnectionCallbackJwtPayloadType.ts index 0174cb620..aa7906b60 100644 --- a/core/src/auth/jwt/payloadtypes/OpenConnectionCallbackJwtPayloadType.ts +++ b/core/src/auth/jwt/payloadtypes/OpenConnectionCallbackJwtPayloadType.ts @@ -8,11 +8,12 @@ export class OpenConnectionCallbackJwtPayloadType extends JwtPayloadType { url: string constructor( + handshakeID: string, oneTimeCode: string, url: string, ) { // eslint-disable-next-line @typescript-eslint/no-unsafe-call - super() + super(handshakeID) // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment this.tokentype = OpenConnectionCallbackJwtPayloadType.OPEN_CONNECTION_CALLBACK_TYPE this.oneTimeCode = oneTimeCode diff --git a/core/src/auth/jwt/payloadtypes/OpenConnectionJwtPayloadType.ts b/core/src/auth/jwt/payloadtypes/OpenConnectionJwtPayloadType.ts index 48e0ae0a3..8843ff4e3 100644 --- a/core/src/auth/jwt/payloadtypes/OpenConnectionJwtPayloadType.ts +++ b/core/src/auth/jwt/payloadtypes/OpenConnectionJwtPayloadType.ts @@ -7,10 +7,11 @@ export class OpenConnectionJwtPayloadType extends JwtPayloadType { url: string constructor( + handshakeID: string, url: string, ) { // eslint-disable-next-line @typescript-eslint/no-unsafe-call - super() + super(handshakeID) // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment this.tokentype = OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE this.url = url diff --git a/core/src/graphql/logic/interpretEncryptedTransferArgs.ts b/core/src/graphql/logic/interpretEncryptedTransferArgs.ts index bd7c3285e..a1058ccc1 100644 --- a/core/src/graphql/logic/interpretEncryptedTransferArgs.ts +++ b/core/src/graphql/logic/interpretEncryptedTransferArgs.ts @@ -9,27 +9,32 @@ import { LOG4JS_BASE_CATEGORY_NAME } from '../../config/const' const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.util.interpretEncryptedTransferArgs`) export const interpretEncryptedTransferArgs = async (args: EncryptedTransferArgs): Promise => { + logger.addContext('handshakeID', args.handshakeID) logger.debug('interpretEncryptedTransferArgs()... args:', args) // first find with args.publicKey the community 'requestingCom', which starts the request const requestingCom = await DbCommunity.findOneBy({ publicKey: Buffer.from(args.publicKey, 'hex') }) if (!requestingCom) { const errmsg = `unknown requesting community with publicKey ${args.publicKey}` logger.error(errmsg) + logger.removeContext('handshakeID') throw new Error(errmsg) } if (!requestingCom.publicJwtKey) { const errmsg = `missing publicJwtKey of requesting community with publicKey ${args.publicKey}` logger.error(errmsg) + logger.removeContext('handshakeID') throw new Error(errmsg) } logger.debug(`found requestingCom:`, new CommunityLoggingView(requestingCom)) // verify the signing of args.jwt with homeCom.privateJwtKey and decrypt args.jwt with requestingCom.publicJwtKey const homeCom = await getHomeCommunity() - const jwtPayload = await verifyAndDecrypt(args.jwt, homeCom!.privateJwtKey!, requestingCom.publicJwtKey) as JwtPayloadType + const jwtPayload = await verifyAndDecrypt(args.handshakeID, args.jwt, homeCom!.privateJwtKey!, requestingCom.publicJwtKey) as JwtPayloadType if (!jwtPayload) { const errmsg = `invalid payload of community with publicKey ${args.publicKey}` logger.error(errmsg) + logger.removeContext('handshakeID') throw new Error(errmsg) } + logger.removeContext('handshakeID') return jwtPayload } diff --git a/core/src/graphql/model/EncryptedTransferArgs.ts b/core/src/graphql/model/EncryptedTransferArgs.ts index 1155cd102..78972ac85 100644 --- a/core/src/graphql/model/EncryptedTransferArgs.ts +++ b/core/src/graphql/model/EncryptedTransferArgs.ts @@ -2,6 +2,9 @@ import { Field, InputType } from 'type-graphql' @InputType() export class EncryptedTransferArgs { + @Field(() => String) + handshakeID: string + @Field(() => String) publicKey: string diff --git a/federation/src/client/1_0/AuthenticationClient.ts b/federation/src/client/1_0/AuthenticationClient.ts index 20d9b8d42..8caad2343 100644 --- a/federation/src/client/1_0/AuthenticationClient.ts +++ b/federation/src/client/1_0/AuthenticationClient.ts @@ -29,6 +29,7 @@ export class AuthenticationClient { } async openConnectionCallback(args: EncryptedTransferArgs): Promise { + logger.addContext('handshakeID', args.handshakeID) logger.debug('openConnectionCallback with endpoint', this.endpoint, args) try { const { data } = await this.client.rawRequest(openConnectionCallback, { args }) @@ -46,6 +47,7 @@ export class AuthenticationClient { } async authenticate(args: EncryptedTransferArgs): Promise { + logger.addContext('handshakeID', args.handshakeID) logger.debug('authenticate with endpoint=', this.endpoint) try { const { data } = await this.client.rawRequest(authenticate, { args }) diff --git a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts index f89955f94..c7d4fe958 100644 --- a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts @@ -21,32 +21,38 @@ export class AuthenticationResolver { @Arg('data') args: EncryptedTransferArgs, ): Promise { + logger.addContext('handshakeID', args.handshakeID) logger.debug(`openConnection() via apiVersion=1_0:`, args) const openConnectionJwtPayload = await interpretEncryptedTransferArgs(args) as OpenConnectionJwtPayloadType if (!openConnectionJwtPayload) { const errmsg = `invalid OpenConnection payload of requesting community with publicKey` + args.publicKey logger.error(errmsg) + logger.removeContext('handshakeID') throw new Error(errmsg) } if (openConnectionJwtPayload.tokentype !== OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE) { const errmsg = `invalid tokentype of community with publicKey` + args.publicKey logger.error(errmsg) + logger.removeContext('handshakeID') throw new Error(errmsg) } if (!openConnectionJwtPayload.url) { const errmsg = `invalid url of community with publicKey` + args.publicKey logger.error(errmsg) + logger.removeContext('handshakeID') throw new Error(errmsg) } const fedComA = await DbFedCommunity.findOneByOrFail({ publicKey: Buffer.from(args.publicKey, 'hex') }) if (!openConnectionJwtPayload.url.startsWith(fedComA.endPoint)) { const errmsg = `invalid url of community with publicKey` + args.publicKey logger.error(errmsg) + logger.removeContext('handshakeID') throw new Error(errmsg) } // biome-ignore lint/complexity/noVoid: no await to respond immediately and invoke callback-request asynchronously void startOpenConnectionCallback(args.publicKey, CONFIG.FEDERATION_API) + logger.removeContext('handshakeID') return true } @@ -55,12 +61,14 @@ export class AuthenticationResolver { @Arg('data') args: EncryptedTransferArgs, ): Promise { + logger.addContext('handshakeID', args.handshakeID) logger.debug(`openConnectionCallback() via apiVersion=1_0 ...`, args) // 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 logger.error(errmsg) + logger.removeContext('handshakeID') throw new Error(errmsg) } @@ -71,6 +79,7 @@ export class AuthenticationResolver { if (!fedComB) { const errmsg = `unknown callback community with url` + openConnectionCallbackJwtPayload.url logger.error(errmsg) + logger.removeContext('handshakeID') throw new Error(errmsg) } logger.debug( @@ -79,6 +88,7 @@ export class AuthenticationResolver { ) // biome-ignore lint/complexity/noVoid: no await to respond immediately and invoke authenticate-request asynchronously void startAuthentication(openConnectionCallbackJwtPayload.oneTimeCode, fedComB) + logger.removeContext('handshakeID') return true } @@ -87,11 +97,13 @@ export class AuthenticationResolver { @Arg('data') args: EncryptedTransferArgs, ): Promise { + logger.addContext('handshakeID', args.handshakeID) logger.debug(`authenticate() via apiVersion=1_0 ...`, args) const authArgs = await interpretEncryptedTransferArgs(args) as AuthenticationJwtPayloadType if (!authArgs) { const errmsg = `invalid authentication payload of requesting community with publicKey` + args.publicKey logger.error(errmsg) + logger.removeContext('handshakeID') throw new Error(errmsg) } const authCom = await DbCommunity.findOneByOrFail({ communityUuid: authArgs.oneTimeCode }) @@ -105,9 +117,11 @@ export class AuthenticationResolver { if (homeComB?.communityUuid) { const responseArgs = new AuthenticationResponseJwtPayloadType(homeComB.communityUuid) const responseJwt = await encryptAndSign(responseArgs, homeComB.privateJwtKey!, authCom.publicJwtKey!) + logger.removeContext('handshakeID') return responseJwt } } + logger.removeContext('handshakeID') return null } } diff --git a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts index 1945ad384..c1e419465 100644 --- a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts +++ b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts @@ -19,9 +19,11 @@ import { AuthenticationJwtPayloadType, AuthenticationResponseJwtPayloadType, enc const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.util.authenticateCommunity`) export async function startOpenConnectionCallback( + handshakeID: string, publicKey: string, api: string, ): Promise { + logger.addContext('handshakeID', handshakeID) logger.debug(`Authentication: startOpenConnectionCallback() with:`, { publicKey, }) @@ -53,13 +55,14 @@ export async function startOpenConnectionCallback( ? homeFedComB.endPoint + homeFedComB.apiVersion : homeFedComB.endPoint + '/' + homeFedComB.apiVersion - const callbackArgs = new OpenConnectionCallbackJwtPayloadType(oneTimeCode, url) + const callbackArgs = new OpenConnectionCallbackJwtPayloadType(handshakeID, oneTimeCode, url) logger.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 args = new EncryptedTransferArgs() args.publicKey = comA.publicKey.toString('hex') args.jwt = jwt + args.handshakeID = handshakeID if (await client.openConnectionCallback(args)) { logger.debug('startOpenConnectionCallback() successful:', jwt) } else { @@ -69,12 +72,15 @@ export async function startOpenConnectionCallback( } catch (err) { logger.error('error in startOpenConnectionCallback:', err) } + logger.removeContext('handshakeID') } export async function startAuthentication( + handshakeID: string, oneTimeCode: string, fedComB: DbFedCommunity, ): Promise { + logger.addContext('handshakeID', handshakeID) logger.debug(`startAuthentication()...`, { oneTimeCode, fedComB: new FederatedCommunityLoggingView(fedComB), @@ -92,17 +98,18 @@ export async function startAuthentication( const client = AuthenticationClientFactory.getInstance(fedComB) if (client instanceof V1_0_AuthenticationClient) { - const authenticationArgs = new AuthenticationJwtPayloadType(oneTimeCode, homeComA!.communityUuid!) + const authenticationArgs = new AuthenticationJwtPayloadType(handshakeID, oneTimeCode, homeComA!.communityUuid!) // 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 = comB.publicKey.toString('hex') args.jwt = jwt + args.handshakeID = handshakeID logger.debug(`invoke authenticate() with:`, args) const responseJwt = await client.authenticate(args) logger.debug(`response of authenticate():`, responseJwt) if (responseJwt !== null) { - const payload = await verifyAndDecrypt(responseJwt, homeComA!.privateJwtKey!, comB.publicJwtKey!) as AuthenticationResponseJwtPayloadType + const payload = await verifyAndDecrypt(handshakeID, responseJwt, homeComA!.privateJwtKey!, comB.publicJwtKey!) as AuthenticationResponseJwtPayloadType logger.debug( `received payload from authenticate ComB:`, payload, @@ -129,4 +136,5 @@ export async function startAuthentication( } catch (err) { logger.error('error in startAuthentication:', err) } + logger.removeContext('handshakeID') }