From fd7d7704c290e6bb25f1c4b2bd7053e46a137ace Mon Sep 17 00:00:00 2001 From: clauspeterhuebner Date: Fri, 27 Jun 2025 16:39:55 +0200 Subject: [PATCH] only for save current code --- backend/src/auth/jwt/JWT.test.ts | 22 +++++- backend/src/auth/jwt/JWT.ts | 70 ++++++------------- .../auth/jwt/payloadtypes/JwtPayloadType.ts | 3 + .../OpenConnectionCallbackJwtPayloadType.ts | 21 ++++++ .../src/federation/authenticateCommunities.ts | 20 +++--- backend/src/federation/validateCommunities.ts | 26 +++---- .../api/1_0/model/OpenConnectionArgs.ts | 2 +- .../1_0/resolver/AuthenticationResolver.ts | 36 +++++++++- .../api/1_0/util/authenticateCommunity.ts | 20 +++--- 9 files changed, 132 insertions(+), 88 deletions(-) create mode 100644 backend/src/auth/jwt/payloadtypes/OpenConnectionCallbackJwtPayloadType.ts diff --git a/backend/src/auth/jwt/JWT.test.ts b/backend/src/auth/jwt/JWT.test.ts index 4ab61e18f..8ca01b206 100644 --- a/backend/src/auth/jwt/JWT.test.ts +++ b/backend/src/auth/jwt/JWT.test.ts @@ -35,10 +35,13 @@ describe('test JWS creation and verification', () => { beforeEach(async () => { jest.clearAllMocks() jwsComA = await encode(new OpenConnectionJwtPayloadType('http://localhost:5001/api/'), keypairComA.privateKey) + console.log('jwsComA', jwsComA) jwsComB = await encode(new OpenConnectionJwtPayloadType('http://localhost:5002/api/'), keypairComB.privateKey) + console.log('jwsComB', jwsComB) }) it('decode jwsComA', async () => { const decodedJwsComA = await decode(jwsComA) + console.log('decodedJwsComA', decodedJwsComA) expect(decodedJwsComA).toEqual({ expiration: '10m', tokentype: OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE, @@ -47,6 +50,7 @@ describe('test JWS creation and verification', () => { }) it('decode jwsComB', async () => { const decodedJwsComB = await decode(jwsComB) + console.log('decodedJwsComB', decodedJwsComB) expect(decodedJwsComB).toEqual({ expiration: '10m', tokentype: OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE, @@ -55,6 +59,7 @@ describe('test JWS creation and verification', () => { }) it('verify jwsComA', async () => { const verifiedJwsComA = await verify(jwsComA, keypairComA.publicKey) + console.log('verifiedJwsComA', verifiedJwsComA) expect(verifiedJwsComA).toEqual(expect.objectContaining({ payload: expect.objectContaining({ tokentype: OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE, @@ -64,6 +69,7 @@ describe('test JWS creation and verification', () => { }) it('verify jwsComB', async () => { const verifiedJwsComB = await verify(jwsComB, keypairComB.publicKey) + console.log('verifiedJwsComB', verifiedJwsComB) expect(verifiedJwsComB).toEqual(expect.objectContaining({ payload: expect.objectContaining({ tokentype: OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE, @@ -79,10 +85,13 @@ describe('test JWE encryption and decryption', () => { beforeEach(async () => { jest.clearAllMocks() jweComA = await encrypt(new OpenConnectionJwtPayloadType('http://localhost:5001/api/'), keypairComB.publicKey) + console.log('jweComA', jweComA) jweComB = await encrypt(new OpenConnectionJwtPayloadType('http://localhost:5002/api/'), keypairComA.publicKey) + console.log('jweComB', jweComB) }) it('decrypt jweComA', async () => { const decryptedAJwT = await decrypt(jweComA, keypairComB.privateKey) + console.log('decryptedAJwT', decryptedAJwT) expect(JSON.parse(decryptedAJwT)).toEqual(expect.objectContaining({ tokentype: OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE, url: 'http://localhost:5001/api/', @@ -90,6 +99,7 @@ describe('test JWE encryption and decryption', () => { }) it('decrypt jweComB', async () => { const decryptedBJwT = await decrypt(jweComB, keypairComA.privateKey) + console.log('decryptedBJwT', decryptedBJwT) expect(JSON.parse(decryptedBJwT)).toEqual(expect.objectContaining({ tokentype: OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE, url: 'http://localhost:5002/api/', @@ -107,12 +117,18 @@ describe('test encrypted and signed JWT', () => { beforeEach(async () => { jest.clearAllMocks() jweComA = await encrypt(new OpenConnectionJwtPayloadType('http://localhost:5001/api/'), keypairComB.publicKey) + console.log('jweComA', jweComA) jwsComA = await encode(new EncryptedJWEJwtPayloadType(jweComA), keypairComA.privateKey) + console.log('jwsComA', jwsComA) jweComB = await encrypt(new OpenConnectionJwtPayloadType('http://localhost:5002/api/'), keypairComA.publicKey) + console.log('jweComB', jweComB) jwsComB = await encode(new EncryptedJWEJwtPayloadType(jweComB), keypairComB.privateKey) + console.log('jwsComB', jwsComB) }) it('verify jwsComA', async () => { - expect(await verify(jwsComA, keypairComA.publicKey)).toEqual(expect.objectContaining({ + const verifiedJwsComA = await verify(jwsComA, keypairComA.publicKey) + console.log('verifiedJwsComA', verifiedJwsComA) + expect(verifiedJwsComA).toEqual(expect.objectContaining({ payload: expect.objectContaining({ jwe: jweComA, tokentype: EncryptedJWEJwtPayloadType.ENCRYPTED_JWE_TYPE, @@ -120,7 +136,9 @@ describe('test encrypted and signed JWT', () => { })) }) it('verify jwsComB', async () => { - expect(await verify(jwsComB, keypairComB.publicKey)).toEqual(expect.objectContaining({ + const verifiedJwsComB = await verify(jwsComB, keypairComB.publicKey) + console.log('verifiedJwsComB', verifiedJwsComB) + expect(verifiedJwsComB).toEqual(expect.objectContaining({ payload: expect.objectContaining({ jwe: jweComB, tokentype: EncryptedJWEJwtPayloadType.ENCRYPTED_JWE_TYPE, diff --git a/backend/src/auth/jwt/JWT.ts b/backend/src/auth/jwt/JWT.ts index b0687b4b9..ff5db0542 100644 --- a/backend/src/auth/jwt/JWT.ts +++ b/backend/src/auth/jwt/JWT.ts @@ -1,11 +1,11 @@ -import { generateKeyPair, exportSPKI, exportPKCS8, KeyLike, SignJWT, decodeJwt, generalDecrypt, importPKCS8, importSPKI, jwtVerify, CompactEncrypt, compactDecrypt } from 'jose' +import { generateKeyPair, exportSPKI, exportPKCS8, SignJWT, decodeJwt, importPKCS8, importSPKI, jwtVerify, CompactEncrypt, compactDecrypt } from 'jose' -import { GeneralJWE } from 'jose/dist/types/types' import { LogError } from '@/server/LogError' import { backendLogger as logger } from '@/server/logger' import { JwtPayloadType } from './payloadtypes/JwtPayloadType' +import { EncryptedJWEJwtPayloadType } from './payloadtypes/EncryptedJWEJwtPayloadType' export const createKeyPair = async (): Promise<{ publicKey: string; privateKey: string }> => { // Generate key pair using jose library @@ -25,17 +25,6 @@ export const verify = async (token: string, publicKey: string): Promise => { + const jwe = await encrypt(payload, publicKey) + const jws = await encode(new EncryptedJWEJwtPayloadType(jwe), privateKey) + return jws +} + +export const verifyAndDecrypt = async (token: string, privateKey: string, publicKey: string): Promise => { + const jwePayload = await verify(token, privateKey) as EncryptedJWEJwtPayloadType + if (!jwePayload) { + return null + } + const payload = await decrypt(jwePayload.jwe as string, publicKey) + return JSON.parse(payload) as JwtPayloadType +} diff --git a/backend/src/auth/jwt/payloadtypes/JwtPayloadType.ts b/backend/src/auth/jwt/payloadtypes/JwtPayloadType.ts index 48881ee32..c69ed1da4 100644 --- a/backend/src/auth/jwt/payloadtypes/JwtPayloadType.ts +++ b/backend/src/auth/jwt/payloadtypes/JwtPayloadType.ts @@ -3,6 +3,9 @@ import { JWTPayload } from 'jose' import { CONFIG } from '@/config' export class JwtPayloadType implements JWTPayload { + static ISSUER = 'urn:gradido:issuer' + static AUDIENCE = 'urn:gradido:audience' + iat?: number | undefined exp?: number | undefined nbf?: number | undefined diff --git a/backend/src/auth/jwt/payloadtypes/OpenConnectionCallbackJwtPayloadType.ts b/backend/src/auth/jwt/payloadtypes/OpenConnectionCallbackJwtPayloadType.ts new file mode 100644 index 000000000..0174cb620 --- /dev/null +++ b/backend/src/auth/jwt/payloadtypes/OpenConnectionCallbackJwtPayloadType.ts @@ -0,0 +1,21 @@ +// import { JWTPayload } from 'jose' +import { JwtPayloadType } from './JwtPayloadType' + +export class OpenConnectionCallbackJwtPayloadType extends JwtPayloadType { + static OPEN_CONNECTION_CALLBACK_TYPE = 'open-connection-callback' + + oneTimeCode: string + url: string + + constructor( + oneTimeCode: string, + url: string, + ) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + super() + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + this.tokentype = OpenConnectionCallbackJwtPayloadType.OPEN_CONNECTION_CALLBACK_TYPE + this.oneTimeCode = oneTimeCode + this.url = url + } +} diff --git a/backend/src/federation/authenticateCommunities.ts b/backend/src/federation/authenticateCommunities.ts index 6b4f3b52c..953f20976 100644 --- a/backend/src/federation/authenticateCommunities.ts +++ b/backend/src/federation/authenticateCommunities.ts @@ -7,11 +7,10 @@ import { AuthenticationClient as V1_0_AuthenticationClient } from '@/federation/ import { backendLogger as logger } from '@/server/logger' import { ensureUrlEndsWithSlash } from '@/util/utilities' +import { encryptAndSign } from '@/auth/jwt/JWT' +import { OpenConnectionJwtPayloadType } from '@/auth/jwt/payloadtypes/OpenConnectionJwtPayloadType' import { OpenConnectionArgs } from './client/1_0/model/OpenConnectionArgs' import { AuthenticationClientFactory } from './client/AuthenticationClientFactory' -import { OpenConnectionJwtPayloadType } from '@/auth/jwt/payloadtypes/OpenConnectionJwtPayloadType' -import { importSPKI } from 'jose' -import { encrypt } from '@/auth/jwt/JWT' export async function startCommunityAuthentication( foreignFedCom: DbFederatedCommunity, @@ -26,6 +25,7 @@ export async function startCommunityAuthentication( 'Authentication: started with foreignFedCom:', foreignFedCom.endPoint, foreignFedCom.publicKey.toString('hex'), + foreignCom.publicJwtKey, ) // check if communityUuid is a valid v4Uuid and not still a temporary onetimecode if ( @@ -40,17 +40,17 @@ export async function startCommunityAuthentication( if (client instanceof V1_0_AuthenticationClient) { if (!foreignCom.publicJwtKey) { - throw new Error('Public JWT key not found for foreign community') + throw new Error('Public JWT key still not exist for foreign community') } - const args = new OpenConnectionArgs() - args.publicKey = homeCom.publicKey.toString('hex') - //create JWT with url in payload encrypted by foreignCom.publicKey and signed with homeCom.privateKey + //create JWT with url in payload encrypted by foreignCom.publicJwtKey and signed with homeCom.privateJwtKey const payload = new OpenConnectionJwtPayloadType( ensureUrlEndsWithSlash(homeFedCom.endPoint).concat(homeFedCom.apiVersion), ) - const encryptKey = await importSPKI(foreignCom.publicJwtKey!, 'RS256') - const jwt = await encrypt(payload, encryptKey) - args.jwt = jwt + const jws = await encryptAndSign(payload, homeCom.privateJwtKey!, foreignCom.publicJwtKey) + // prepare the args for the client invocation + const args = new OpenConnectionArgs() + args.publicKey = homeCom.publicKey.toString('hex') + args.jwt = jws logger.debug( 'Authentication: before client.openConnection() args:', homeCom.publicKey.toString('hex'), diff --git a/backend/src/federation/validateCommunities.ts b/backend/src/federation/validateCommunities.ts index 4ba350322..9ad44d232 100644 --- a/backend/src/federation/validateCommunities.ts +++ b/backend/src/federation/validateCommunities.ts @@ -14,9 +14,7 @@ import { backendLogger as logger } from '@/server/logger' import { startCommunityAuthentication } from './authenticateCommunities' import { PublicCommunityInfoLoggingView } from './client/1_0/logging/PublicCommunityInfoLogging.view' import { ApiVersionType } from './enum/apiVersionType' -import { generateKeyPair, exportSPKI, exportPKCS8 } from 'jose' - -// import { CONFIG } from '@/config/' +import { createKeyPair } from '@/auth/jwt/JWT' export async function startValidateCommunities(timerInterval: number): Promise { if (Number.isNaN(timerInterval) || timerInterval <= 0) { @@ -68,7 +66,11 @@ export async function validateCommunities(): Promise { const pubComInfo = await client.getPublicCommunityInfo() if (pubComInfo) { await writeForeignCommunity(dbCom, pubComInfo) - await startCommunityAuthentication(dbCom) + try { + await startCommunityAuthentication(dbCom) + } catch (err) { + logger.warn(`Warning: Community Authentication still not ready:`, err) + } logger.debug(`Federation: write publicInfo of community: name=${pubComInfo.name}`) } else { logger.debug('Federation: missing result of getPublicCommunityInfo') @@ -95,19 +97,13 @@ export async function writeJwtKeyPairInHomeCommunity(): Promise { if (homeCom) { if (!homeCom.publicJwtKey && !homeCom.privateJwtKey) { // Generate key pair using jose library - const keyPair = await generateKeyPair('RS256'); - logger.debug(`Federation: writeJwtKeyPairInHomeCommunity generated keypair=`, keyPair); + const { publicKey, privateKey } = await createKeyPair(); + logger.debug(`Federation: writeJwtKeyPairInHomeCommunity publicKey=`, publicKey); + logger.debug(`Federation: writeJwtKeyPairInHomeCommunity privateKey=`, privateKey); - // Convert keys to PEM format - const publicKeyPem = await exportSPKI(keyPair.publicKey); - const privateKeyPem = await exportPKCS8(keyPair.privateKey); - - logger.debug(`Federation: writeJwtKeyPairInHomeCommunity publicKey=`, publicKeyPem); - logger.debug(`Federation: writeJwtKeyPairInHomeCommunity privateKey=`, privateKeyPem); - - homeCom.publicJwtKey = publicKeyPem; + homeCom.publicJwtKey = publicKey; logger.debug(`Federation: writeJwtKeyPairInHomeCommunity publicJwtKey.length=`, homeCom.publicJwtKey.length); - homeCom.privateJwtKey = privateKeyPem; + homeCom.privateJwtKey = privateKey; logger.debug(`Federation: writeJwtKeyPairInHomeCommunity privateJwtKey.length=`, homeCom.privateJwtKey.length); await DbCommunity.save(homeCom) logger.debug(`Federation: writeJwtKeyPairInHomeCommunity done`) diff --git a/federation/src/graphql/api/1_0/model/OpenConnectionArgs.ts b/federation/src/graphql/api/1_0/model/OpenConnectionArgs.ts index 9afdbca5f..7e436da05 100644 --- a/federation/src/graphql/api/1_0/model/OpenConnectionArgs.ts +++ b/federation/src/graphql/api/1_0/model/OpenConnectionArgs.ts @@ -6,5 +6,5 @@ export class OpenConnectionArgs { publicKey: string @Field(() => String) - url: string + jwt: string } diff --git a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts index 2479bb8be..441d4e7b3 100644 --- a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts @@ -12,6 +12,9 @@ import { AuthenticationArgs } from '../model/AuthenticationArgs' import { OpenConnectionArgs } from '../model/OpenConnectionArgs' import { OpenConnectionCallbackArgs } from '../model/OpenConnectionCallbackArgs' import { startAuthentication, startOpenConnectionCallback } from '../util/authenticateCommunity' +import { verifyAndDecrypt } from 'backend/src/auth/jwt/JWT' +import { OpenConnectionJwtPayloadType } from 'backend/src/auth/jwt/payloadtypes/OpenConnectionJwtPayloadType' +import { JwtPayloadType } from 'backend/src/auth/jwt/payloadtypes/JwtPayloadType' @Resolver() export class AuthenticationResolver { @@ -30,9 +33,38 @@ export class AuthenticationResolver { if (!comA) { throw new LogError(`unknown requesting community with publicKey`, pubKeyBuf.toString('hex')) } + if (!comA.publicJwtKey) { + throw new LogError(`missing publicJwtKey of community with publicKey`, pubKeyBuf.toString('hex')) + } logger.debug(`Authentication: found requestedCom:`, new CommunityLoggingView(comA)) + // verify the signing of args.jwt with homeCom.privateJwtKey and decrypt args.jwt with comA.publicJwtKey + const homeCom = await DbCommunity.findOneByOrFail({ foreign: false }) + const openConnectionJwtPayload = await verifyAndDecrypt(args.jwt, homeCom.privateJwtKey!, comA.publicJwtKey) as OpenConnectionJwtPayloadType + if (!openConnectionJwtPayload) { + throw new LogError(`invalid payload of community with publicKey`, pubKeyBuf.toString('hex')) + } + if (!openConnectionJwtPayload.url) { + throw new LogError(`invalid url of community with publicKey`, pubKeyBuf.toString('hex')) + } + if (openConnectionJwtPayload.tokentype !== OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE) { + throw new LogError(`invalid tokentype of community with publicKey`, pubKeyBuf.toString('hex')) + } + if (openConnectionJwtPayload.expiration < new Date().toISOString()) { + throw new LogError(`invalid expiration of community with publicKey`, pubKeyBuf.toString('hex')) + } + if (openConnectionJwtPayload.issuer !== JwtPayloadType.ISSUER) { + throw new LogError(`invalid issuer of community with publicKey`, pubKeyBuf.toString('hex')) + } + if (openConnectionJwtPayload.audience !== JwtPayloadType.AUDIENCE) { + throw new LogError(`invalid audience of community with publicKey`, pubKeyBuf.toString('hex')) + } + const fedComA = await DbFedCommunity.findOneByOrFail({ publicKey: comA.publicKey }) + if (!openConnectionJwtPayload.url.startsWith(fedComA.endPoint)) { + throw new LogError(`invalid url of community with publicKey`, pubKeyBuf.toString('hex')) + } + // biome-ignore lint/complexity/noVoid: no await to respond immediately and invoke callback-request asynchronously - void startOpenConnectionCallback(args, comA, CONFIG.FEDERATION_API) + void startOpenConnectionCallback(comA, CONFIG.FEDERATION_API) return true } @@ -42,7 +74,7 @@ export class AuthenticationResolver { args: OpenConnectionCallbackArgs, ): Promise { logger.debug(`Authentication: openConnectionCallback() via apiVersion=1_0 ...`, args) - // TODO decrypt args.url with homeCom.privateKey and verify signing with callbackFedCom.publicKey + // TODO decrypt args.url with homeCom.privateJwtKey and verify signing with callbackFedCom.publicKey const endPoint = args.url.slice(0, args.url.lastIndexOf('/') + 1) const apiVersion = args.url.slice(args.url.lastIndexOf('/') + 1, args.url.length) logger.debug(`Authentication: search fedComB per:`, endPoint, apiVersion) diff --git a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts index 0004fb5b6..2c6ea14a8 100644 --- a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts +++ b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts @@ -13,17 +13,18 @@ import { randombytes_random } from 'sodium-native' import { AuthenticationClient as V1_0_AuthenticationClient } from '@/client/1_0/AuthenticationClient' import { AuthenticationArgs } from '../model/AuthenticationArgs' +import { encryptAndSign } from 'backend/src/auth/jwt/JWT' +import { OpenConnectionCallbackJwtPayloadType } from 'backend/src/auth/jwt/payloadtypes/OpenConnectionCallbackJwtPayloadType' export async function startOpenConnectionCallback( - args: OpenConnectionArgs, comA: DbCommunity, api: string, ): Promise { logger.debug(`Authentication: startOpenConnectionCallback() with:`, { - args, comA: new CommunityLoggingView(comA), }) try { + const homeCom = await DbCommunity.findOneByOrFail({ foreign: false }) const homeFedCom = await DbFedCommunity.findOneByOrFail({ foreign: false, apiVersion: api, @@ -33,9 +34,9 @@ export async function startOpenConnectionCallback( apiVersion: api, publicKey: comA.publicKey, }) - const oneTimeCode = randombytes_random() + const oneTimeCode = randombytes_random().toString() // store oneTimeCode in requestedCom.community_uuid as authenticate-request-identifier - comA.communityUuid = oneTimeCode.toString() + comA.communityUuid = oneTimeCode await DbCommunity.save(comA) logger.debug( `Authentication: stored oneTimeCode in requestedCom:`, @@ -45,14 +46,15 @@ export async function startOpenConnectionCallback( const client = AuthenticationClientFactory.getInstance(fedComA) if (client instanceof V1_0_AuthenticationClient) { - const callbackArgs = new OpenConnectionCallbackArgs() - callbackArgs.oneTimeCode = oneTimeCode.toString() - // TODO encrypt callbackArgs.url with requestedCom.publicKey and sign it with homeCom.privateKey - callbackArgs.url = homeFedCom.endPoint.endsWith('/') + const url = homeFedCom.endPoint.endsWith('/') ? homeFedCom.endPoint + homeFedCom.apiVersion : homeFedCom.endPoint + '/' + homeFedCom.apiVersion + + const callbackArgs = new OpenConnectionCallbackJwtPayloadType(oneTimeCode, url) logger.debug(`Authentication: start openConnectionCallback with args:`, callbackArgs) - if (await client.openConnectionCallback(callbackArgs)) { + // encrypt callbackArgs with requestedCom.publicKey and sign it with homeCom.privateKey + const encryptedCallbackArgs = await encryptAndSign(callbackArgs, homeCom.privateJwtKey!, comA.publicJwtKey!) + if (await client.openConnectionCallback(encryptedCallbackArgs)) { logger.debug('Authentication: startOpenConnectionCallback() successful:', callbackArgs) } else { logger.error('Authentication: startOpenConnectionCallback() failed:', callbackArgs)