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)