introduce handshakeID

This commit is contained in:
clauspeterhuebner 2025-07-09 18:48:32 +02:00
parent 7b399dcd32
commit 18835e55af
14 changed files with 69 additions and 25 deletions

View File

@ -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<void> {
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')
}

View File

@ -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<boolean | undefined> {
async openConnection(args: EncryptedTransferArgs): Promise<boolean | undefined> {
logger.debug(`openConnection at ${this.endpoint} for args:`, args)
try {
const { data } = await this.client.rawRequest<{ openConnection: boolean }>(openConnection, {

View File

@ -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<JwtPayloadType | null> => {
export const verify = async (handshakeID: string, token: string, publicKey: string): Promise<JwtPayloadType | null> => {
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<JwtPaylo
issuer: JwtPayloadType.ISSUER,
audience: JwtPayloadType.AUDIENCE,
})
payload.handshakeID = handshakeID
logger.debug('verify after jwtVerify... payload=', payload)
return payload as JwtPayloadType
} catch (err) {
@ -73,8 +74,8 @@ export const encode = async (payload: JwtPayloadType, privatekey: string): Promi
}
}
export const verifyJwtType = async (token: string, publicKey: string): Promise<string> => {
const payload = await verify(token, publicKey)
export const verifyJwtType = async (handshakeID: string, token: string, publicKey: string): Promise<string> => {
const payload = await verify(handshakeID, token, publicKey)
return payload ? payload.tokentype : 'unknown token type'
}
@ -84,6 +85,7 @@ export const decode = (token: string): JwtPayloadType => {
}
export const encrypt = async (payload: JwtPayloadType, publicKey: string): Promise<string> => {
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<string> =
}
export const encryptAndSign = async (payload: JwtPayloadType, privateKey: string, publicKey: string): Promise<string> => {
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<JwtPayloadType | null> => {
const jweVerifyResult = await verify(token, publicKey)
export const verifyAndDecrypt = async (handshakeID: string, token: string, privateKey: string, publicKey: string): Promise<JwtPayloadType | null> => {
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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<JwtPayloadType | null> => {
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
}

View File

@ -2,6 +2,9 @@ import { Field, InputType } from 'type-graphql'
@InputType()
export class EncryptedTransferArgs {
@Field(() => String)
handshakeID: string
@Field(() => String)
publicKey: string

View File

@ -29,6 +29,7 @@ export class AuthenticationClient {
}
async openConnectionCallback(args: EncryptedTransferArgs): Promise<boolean> {
logger.addContext('handshakeID', args.handshakeID)
logger.debug('openConnectionCallback with endpoint', this.endpoint, args)
try {
const { data } = await this.client.rawRequest<any>(openConnectionCallback, { args })
@ -46,6 +47,7 @@ export class AuthenticationClient {
}
async authenticate(args: EncryptedTransferArgs): Promise<string | null> {
logger.addContext('handshakeID', args.handshakeID)
logger.debug('authenticate with endpoint=', this.endpoint)
try {
const { data } = await this.client.rawRequest<any>(authenticate, { args })

View File

@ -21,32 +21,38 @@ export class AuthenticationResolver {
@Arg('data')
args: EncryptedTransferArgs,
): Promise<boolean> {
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<boolean> {
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<string | null> {
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
}
}

View File

@ -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<void> {
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<void> {
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')
}