chore: Move JWK generation to separate class

This commit is contained in:
Joachim Van Herwegen
2022-10-13 09:58:03 +02:00
parent 37ba404058
commit bc119dbd3e
11 changed files with 264 additions and 55 deletions

View File

@@ -0,0 +1,88 @@
import { generateKeyPair, importJWK, jwtVerify, SignJWT } from 'jose';
import * as jose from 'jose';
import { CachedJwkGenerator } from '../../../../src/identity/configuration/CachedJwkGenerator';
import type { AlgJwk } from '../../../../src/identity/configuration/JwkGenerator';
import type { KeyValueStorage } from '../../../../src/storage/keyvalue/KeyValueStorage';
describe('A CachedJwkGenerator', (): void => {
const alg = 'ES256';
const storageKey = 'jwks';
let storageMap: Map<string, AlgJwk>;
let storage: jest.Mocked<KeyValueStorage<string, AlgJwk>>;
let generator: CachedJwkGenerator;
beforeEach(async(): Promise<void> => {
storageMap = new Map();
storage = {
get: jest.fn(async(key: string): Promise<AlgJwk | undefined> => storageMap.get(key)),
set: jest.fn(async(key: string, value: AlgJwk): Promise<any> => storageMap.set(key, value)),
} as any;
generator = new CachedJwkGenerator(alg, storageKey, storage);
});
it('generates a matching key set.', async(): Promise<void> => {
const privateKey = await generator.getPrivateKey();
expect(privateKey.alg).toBe(alg);
const publicKey = await generator.getPublicKey();
expect(publicKey.alg).toBe(alg);
const privateObject = await importJWK(privateKey);
const publicObject = await importJWK(publicKey);
const signed = await new SignJWT({ data: 'signed data' }).setProtectedHeader({ alg }).sign(privateObject);
await expect(jwtVerify(signed, publicObject)).resolves.toMatchObject({
payload: {
data: 'signed data',
},
});
const otherKey = (await generateKeyPair(alg)).publicKey;
await expect(jwtVerify(signed, otherKey)).rejects.toThrow();
});
it('caches the private key in memory.', async(): Promise<void> => {
const spy = jest.spyOn(jose, 'generateKeyPair');
const privateKey = await generator.getPrivateKey();
// 1 call from checking the storage
expect(storage.get).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledTimes(1);
const privateKey2 = await generator.getPrivateKey();
expect(privateKey).toBe(privateKey2);
expect(spy).toHaveBeenCalledTimes(1);
expect(storage.get).toHaveBeenCalledTimes(1);
spy.mockRestore();
});
it('caches the public key in memory.', async(): Promise<void> => {
const spy = jest.spyOn(jose, 'generateKeyPair');
const publicKey = await generator.getPublicKey();
// 1 call from checking the storage
expect(storage.get).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledTimes(1);
const publicKey2 = await generator.getPublicKey();
expect(publicKey).toBe(publicKey2);
expect(spy).toHaveBeenCalledTimes(1);
expect(storage.get).toHaveBeenCalledTimes(1);
spy.mockRestore();
});
it('caches the key in storage in case of server restart.', async(): Promise<void> => {
const spy = jest.spyOn(jose, 'generateKeyPair');
const privateKey = await generator.getPrivateKey();
// 1 call from checking the storage
expect(storage.get).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledTimes(1);
const generator2 = new CachedJwkGenerator(alg, storageKey, storage);
const privateKey2 = await generator2.getPrivateKey();
expect(privateKey).toBe(privateKey2);
expect(spy).toHaveBeenCalledTimes(1);
expect(storage.get).toHaveBeenCalledTimes(2);
spy.mockRestore();
});
});

View File

@@ -1,10 +1,12 @@
import { Readable } from 'stream';
import { exportJWK, generateKeyPair } from 'jose';
import type * as Koa from 'koa';
import type { errors, Configuration, KoaContextWithOIDC } from 'oidc-provider';
import type { ErrorHandler } from '../../../../src/http/output/error/ErrorHandler';
import type { ResponseWriter } from '../../../../src/http/output/ResponseWriter';
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
import { IdentityProviderFactory } from '../../../../src/identity/configuration/IdentityProviderFactory';
import type { JwkGenerator } from '../../../../src/identity/configuration/JwkGenerator';
import type {
ClientCredentials,
} from '../../../../src/identity/interaction/email-password/credentials/ClientCredentialsAdapterFactory';
@@ -45,6 +47,7 @@ describe('An IdentityProviderFactory', (): void => {
let interactionHandler: jest.Mocked<InteractionHandler>;
let adapterFactory: jest.Mocked<AdapterFactory>;
let storage: jest.Mocked<KeyValueStorage<string, any>>;
let jwkGenerator: jest.Mocked<JwkGenerator>;
let credentialStorage: jest.Mocked<KeyValueStorage<string, ClientCredentials>>;
let errorHandler: jest.Mocked<ErrorHandler>;
let responseWriter: jest.Mocked<ResponseWriter>;
@@ -77,6 +80,13 @@ describe('An IdentityProviderFactory', (): void => {
set: jest.fn((id: string, value: any): any => map.set(id, value)),
} as any;
const { privateKey, publicKey } = await generateKeyPair('ES256');
jwkGenerator = {
alg: 'ES256',
getPrivateKey: jest.fn().mockResolvedValue({ ...await exportJWK(privateKey), alg: 'ES256' }),
getPublicKey: jest.fn().mockResolvedValue({ ...await exportJWK(publicKey), alg: 'ES256' }),
};
credentialStorage = {
get: jest.fn((id: string): any => map.get(id)),
set: jest.fn((id: string, value: any): any => map.set(id, value)),
@@ -94,6 +104,7 @@ describe('An IdentityProviderFactory', (): void => {
oidcPath,
interactionHandler,
storage,
jwkGenerator,
credentialStorage,
showStackTrace: true,
errorHandler,
@@ -179,6 +190,7 @@ describe('An IdentityProviderFactory', (): void => {
oidcPath,
interactionHandler,
storage,
jwkGenerator,
credentialStorage,
showStackTrace: true,
errorHandler,
@@ -203,6 +215,7 @@ describe('An IdentityProviderFactory', (): void => {
oidcPath,
interactionHandler,
storage,
jwkGenerator,
credentialStorage,
showStackTrace: true,
errorHandler,
@@ -210,10 +223,8 @@ describe('An IdentityProviderFactory', (): void => {
});
const result2 = await factory2.getProvider() as unknown as { issuer: string; config: Configuration };
expect(result1.config.cookies).toEqual(result2.config.cookies);
expect(result1.config.jwks).toEqual(result2.config.jwks);
expect(storage.get).toHaveBeenCalledTimes(4);
expect(storage.set).toHaveBeenCalledTimes(2);
expect(storage.set).toHaveBeenCalledWith('jwks', result1.config.jwks);
expect(storage.get).toHaveBeenCalledTimes(2);
expect(storage.set).toHaveBeenCalledTimes(1);
expect(storage.set).toHaveBeenCalledWith('cookie-secret', result1.config.cookies?.keys);
});