JWT signing and encryption methods and tests

This commit is contained in:
clauspeterhuebner 2025-06-25 16:13:23 +02:00
parent de2566d09b
commit c75e43eafe
3 changed files with 258 additions and 22 deletions

View File

@ -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/',
}))
})
})

View File

@ -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<JwtPayloadType | null> => {
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<JwtPayloadType | null> => {
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<JwtPayload
logger.info('JWT.verify... keyObject.asymmetricKeyType=', keyObject.asymmetricKeyType)
logger.info('JWT.verify... keyObject.asymmetricKeySize=', keyObject.asymmetricKeySize)
*/
const secret = new TextEncoder().encode(signkey)
const importedKey = await importSPKI(publicKey, 'RS256')
// Convert the key to JWK format if needed
const secret = typeof importedKey === 'string'
? JSON.parse(importedKey)
: importedKey;
// const secret = new TextEncoder().encode(publicKey)
const { payload } = await jwtVerify(token, secret, {
issuer: 'urn:gradido:issuer',
audience: 'urn:gradido:audience',
@ -37,14 +54,19 @@ export const verify = async (token: string, signkey: string): Promise<JwtPayload
}
}
export const encode = async (payload: JwtPayloadType, signkey: string): Promise<string> => {
export const encode = async (payload: JwtPayloadType, privatekey: string): Promise<string> => {
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<string> => {
const payload = await verify(token, signkey)
export const verifyJwtType = async (token: string, publicKey: string): Promise<string> => {
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<string> => {
export const encrypt = async (payload: JwtPayloadType, publicKey: string): Promise<string> => {
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
}
}
}
export const decrypt = async(jwe: string, privateKey: string): Promise<string> => {
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
}
}

View File

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