From c75e43eafe8e0c8c6117c8fd4e8210247f0ab54e Mon Sep 17 00:00:00 2001
From: clauspeterhuebner
Date: Wed, 25 Jun 2025 16:13:23 +0200
Subject: [PATCH] JWT signing and encryption methods and tests
---
backend/src/auth/jwt/JWT.test.ts | 161 ++++++++++++++++++
backend/src/auth/jwt/JWT.ts | 101 ++++++++---
.../EncryptedJWEJwtPayloadType.ts | 18 ++
3 files changed, 258 insertions(+), 22 deletions(-)
create mode 100644 backend/src/auth/jwt/JWT.test.ts
create mode 100644 backend/src/auth/jwt/payloadtypes/EncryptedJWEJwtPayloadType.ts
diff --git a/backend/src/auth/jwt/JWT.test.ts b/backend/src/auth/jwt/JWT.test.ts
new file mode 100644
index 000000000..45d812ee0
--- /dev/null
+++ b/backend/src/auth/jwt/JWT.test.ts
@@ -0,0 +1,161 @@
+import { ApolloServerTestClient } from 'apollo-server-testing'
+import { FederatedCommunity as DbFederatedCommunity } from 'database'
+import { GraphQLClient } from 'graphql-request'
+import { Response } from 'graphql-request/dist/types'
+import { DataSource } from 'typeorm'
+
+import { cleanDB, testEnvironment } from '@test/helpers'
+import { logger } from '@test/testSetup'
+
+import { encode, decode, verify, encrypt, decrypt, createKeyPair } from './JWT'
+import { OpenConnectionJwtPayloadType } from './payloadtypes/OpenConnectionJwtPayloadType'
+import { EncryptedJWEJwtPayloadType } from './payloadtypes/EncryptedJWEJwtPayloadType'
+
+let con: DataSource
+let testEnv: {
+ mutate: ApolloServerTestClient['mutate']
+ query: ApolloServerTestClient['query']
+ con: DataSource
+}
+let keypairComA: { publicKey: string; privateKey: string }
+let keypairComB: { publicKey: string; privateKey: string }
+
+beforeAll(async () => {
+ testEnv = await testEnvironment(logger)
+ con = testEnv.con
+ await cleanDB()
+
+ keypairComA = await createKeyPair()
+ keypairComB = await createKeyPair()
+})
+
+afterAll(async () => {
+ // await cleanDB()
+ await con.destroy()
+})
+
+describe('test JWS creation and verification', () => {
+ let jwsComA: string
+ let jwsComB: string
+ 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,
+ url: 'http://localhost:5001/api/',
+ })
+ })
+ it('decode jwsComB', async () => {
+ const decodedJwsComB = await decode(jwsComB)
+ console.log('decodedJwsComB=', decodedJwsComB)
+ expect(decodedJwsComB).toEqual({
+ expiration: '10m',
+ tokentype: OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE,
+ url: 'http://localhost:5002/api/',
+ })
+ })
+ it('verify jwsComA', async () => {
+ const verifiedJwsComA = await verify(jwsComA, keypairComA.publicKey)
+ console.log('verify jwsComA=', verifiedJwsComA)
+ expect(verifiedJwsComA).toEqual(expect.objectContaining({
+ payload: expect.objectContaining({
+ tokentype: OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE,
+ url: 'http://localhost:5001/api/',
+ })
+ }))
+ })
+ it('verify jwsComB', async () => {
+ const verifiedJwsComB = await verify(jwsComB, keypairComB.publicKey)
+ console.log('verify jwsComB=', verifiedJwsComB)
+ expect(verifiedJwsComB).toEqual(expect.objectContaining({
+ payload: expect.objectContaining({
+ tokentype: OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE,
+ url: 'http://localhost:5002/api/',
+ })
+ }))
+ })
+})
+
+describe('test JWE encryption and decryption', () => {
+ let jweComA: string
+ let jweComB: string
+ 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/',
+ }))
+ })
+ 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/',
+ }))
+ })
+})
+
+describe('test encrypted and signed JWT', () => {
+ let jweComA: string
+ let jwsComA: string
+ let jwtComA: string
+ let jweComB: string
+ let jwsComB: string
+ let jwtComB: string
+ 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({
+ payload: expect.objectContaining({
+ jwe: jweComA,
+ tokentype: EncryptedJWEJwtPayloadType.ENCRYPTED_JWE_TYPE,
+ })
+ }))
+ })
+ it('verify jwsComB', async () => {
+ expect(await verify(jwsComB, keypairComB.publicKey)).toEqual(expect.objectContaining({
+ payload: expect.objectContaining({
+ jwe: jweComB,
+ tokentype: EncryptedJWEJwtPayloadType.ENCRYPTED_JWE_TYPE,
+ })
+ }))
+ })
+ it('decrypt jweComA', async () => {
+ expect(JSON.parse(await decrypt(jweComA, keypairComB.privateKey))).toEqual(expect.objectContaining({
+ tokentype: OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE,
+ url: 'http://localhost:5001/api/',
+ }))
+ })
+ it('decrypt jweComB', async () => {
+ expect(JSON.parse(await decrypt(jweComB, keypairComA.privateKey))).toEqual(expect.objectContaining({
+ tokentype: OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE,
+ url: 'http://localhost:5002/api/',
+ }))
+ })
+})
\ No newline at end of file
diff --git a/backend/src/auth/jwt/JWT.ts b/backend/src/auth/jwt/JWT.ts
index 6f1fa34c2..b0687b4b9 100644
--- a/backend/src/auth/jwt/JWT.ts
+++ b/backend/src/auth/jwt/JWT.ts
@@ -1,16 +1,28 @@
-import { GeneralEncrypt, KeyLike, SignJWT, decodeJwt, jwtVerify } from 'jose'
+import { generateKeyPair, exportSPKI, exportPKCS8, KeyLike, SignJWT, decodeJwt, generalDecrypt, 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'
-export const verify = async (token: string, signkey: string): Promise => {
+export const createKeyPair = async (): Promise<{ publicKey: string; privateKey: string }> => {
+ // Generate key pair using jose library
+ const keyPair = await generateKeyPair('RS256');
+ logger.debug(`Federation: writeJwtKeyPairInHomeCommunity generated keypair=`, keyPair);
+
+ // Convert keys to PEM format for storage in database
+ const publicKeyPem = await exportSPKI(keyPair.publicKey);
+ const privateKeyPem = await exportPKCS8(keyPair.privateKey);
+ return { publicKey: publicKeyPem, privateKey: privateKeyPem };
+}
+
+export const verify = async (token: string, publicKey: string): Promise => {
if (!token) {
throw new LogError('401 Unauthorized')
}
- logger.info('JWT.verify... token, signkey=', token, signkey)
+ logger.info('JWT.verify... token, publicKey=', token, publicKey)
try {
/*
@@ -24,7 +36,12 @@ export const verify = async (token: string, signkey: string): Promise => {
+export const encode = async (payload: JwtPayloadType, privatekey: string): Promise => {
logger.info('JWT.encode... payload=', payload)
- logger.info('JWT.encode... signkey=', signkey)
+ logger.info('JWT.encode... privatekey=', privatekey)
try {
- const secret = new TextEncoder().encode(signkey)
+ const importedKey = await importPKCS8(privatekey, 'RS256')
+ const secret = typeof importedKey === 'string'
+ ? JSON.parse(importedKey)
+ : importedKey;
+
+ // const secret = new TextEncoder().encode(privatekey)
const token = await new SignJWT({ payload, 'urn:gradido:claim': true })
.setProtectedHeader({
- alg: 'HS256',
+ alg: 'RS256',
})
.setIssuedAt()
.setIssuer('urn:gradido:issuer')
@@ -58,8 +80,8 @@ export const encode = async (payload: JwtPayloadType, signkey: string): Promise<
}
}
-export const verifyJwtType = async (token: string, signkey: string): Promise => {
- const payload = await verify(token, signkey)
+export const verifyJwtType = async (token: string, publicKey: string): Promise => {
+ const payload = await verify(token, publicKey)
return payload ? payload.tokentype : 'unknown token type'
}
@@ -68,26 +90,33 @@ export const decode = (token: string): JwtPayloadType => {
return payload as JwtPayloadType
}
-export const encrypt = async(payload: JwtPayloadType, encryptkey: KeyLike): Promise => {
+export const encrypt = async (payload: JwtPayloadType, publicKey: string): Promise => {
logger.info('JWT.encrypt... payload=', payload)
- logger.info('JWT.encrypt... encryptkey=', encryptkey)
+ logger.info('JWT.encrypt... publicKey=', publicKey)
try {
+ const encryptKey = await importSPKI(publicKey, 'RS256')
// Convert the key to JWK format if needed
- const recipientKey = typeof encryptkey === 'string'
- ? JSON.parse(encryptkey)
- : encryptkey;
+ const recipientKey = typeof encryptKey === 'string'
+ ? JSON.parse(encryptKey)
+ : encryptKey;
- const jwe = await new GeneralEncrypt(
+ const jwe = await new CompactEncrypt(
new TextEncoder().encode(JSON.stringify(payload)),
)
- .setProtectedHeader({ enc: 'A256GCM' })
- .addRecipient(recipientKey)
- .setUnprotectedHeader({ alg: 'ECDH-ES+A256KW' })
- .encrypt()
+ .setProtectedHeader({ alg: 'RSA-OAEP-256', enc: 'A256GCM' })
+ .encrypt(recipientKey)
/*
+ const jwe = await new EncryptJWT({ payload, 'urn:gradido:claim': true })
+ .setProtectedHeader({ alg: 'RSA-OAEP-256', enc: 'A256GCM' })
+ .setIssuedAt()
+ .setIssuer('urn:gradido:issuer')
+ .setAudience('urn:gradido:audience')
+ .setExpirationTime('5m')
+ .encrypt(recipientKey);
+
const token = await new EncryptJWT({ payload, 'urn:gradido:claim': true })
.setProtectedHeader({
- alg: 'HS256',
+ alg: 'RS256',
enc: 'A256GCM',
})
.setIssuedAt()
@@ -102,4 +131,32 @@ export const encrypt = async(payload: JwtPayloadType, encryptkey: KeyLike): Prom
logger.error('Failed to encrypt JWT:', e)
throw e
}
-}
\ No newline at end of file
+}
+
+export const decrypt = async(jwe: string, privateKey: string): Promise => {
+ logger.info('JWT.decrypt... jwe=', jwe)
+ logger.info('JWT.decrypt... privateKey=', privateKey)
+ try {
+ const decryptKey = await importPKCS8(privateKey, 'RS256')
+ const { plaintext, protectedHeader } =
+ await compactDecrypt(jwe, decryptKey)
+ logger.info('JWT.decrypt... plaintext=', plaintext)
+ logger.info('JWT.decrypt... protectedHeader=', protectedHeader)
+ return plaintext.toString()
+ /*
+ const generalJwe = await GeneralJWE.parse(jwe)
+ const jws = await generalDecrypt(generalJwe, privateKey, { alg: 'ECDH-ES+A256KW', enc: 'A256GCM' })
+
+ const { payload, protectedHeader } = await jwtDecrypt(jwe, privateKey);
+
+ console.log(payload);
+ console.log(protectedHeader);
+
+ logger.info('JWT.decrypt... jws=', jws)
+ return jws.toString()
+ */
+ } catch (e) {
+ logger.error('Failed to decrypt JWT:', e)
+ throw e
+ }
+}
diff --git a/backend/src/auth/jwt/payloadtypes/EncryptedJWEJwtPayloadType.ts b/backend/src/auth/jwt/payloadtypes/EncryptedJWEJwtPayloadType.ts
new file mode 100644
index 000000000..31f7d5377
--- /dev/null
+++ b/backend/src/auth/jwt/payloadtypes/EncryptedJWEJwtPayloadType.ts
@@ -0,0 +1,18 @@
+// import { JWTPayload } from 'jose'
+import { JwtPayloadType } from './JwtPayloadType'
+
+export class EncryptedJWEJwtPayloadType extends JwtPayloadType {
+ static ENCRYPTED_JWE_TYPE = 'encrypted-jwe'
+
+ jwe: string
+
+ constructor(
+ jwe: string,
+ ) {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
+ super()
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ this.tokentype = EncryptedJWEJwtPayloadType.ENCRYPTED_JWE_TYPE
+ this.jwe = jwe
+ }
+}