Skip to main content
Glama
keys.test.ts8.38 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { randomUUID } from 'crypto'; import { generateKeyPair, SignJWT } from 'jose'; import { initAppServices, shutdownApp } from '../app'; import { loadTestConfig } from '../config/loader'; import type { MedplumServerConfig } from '../config/types'; import { generateAccessToken, generateIdToken, generateRefreshToken, generateSecret, getSigningKey, initKeys, verifyJwt, } from './keys'; describe('Keys', () => { beforeAll(async () => { const config = await loadTestConfig(); await initAppServices(config); }); afterAll(async () => { await shutdownApp(); }); test('Missing issuer', async () => { const config = await loadTestConfig(); delete (config as any).issuer; try { await initKeys(config); fail('Expected error'); } catch (err) { expect((err as Error).message).toStrictEqual('Missing issuer'); } }); test('Generate before initialized', async () => { const config = await loadTestConfig(); expect.assertions(2); try { await initKeys(undefined as unknown as MedplumServerConfig); } catch (err) { expect((err as Error).message).toStrictEqual('Invalid server configuration'); } try { await generateIdToken({ iss: config.issuer, login_id: '123', nonce: randomUUID() }); } catch (err) { expect((err as Error).message).toStrictEqual('Signing key not initialized'); } }); test('Missing issuer', async () => { const config = await loadTestConfig(); expect.assertions(3); try { await initKeys({} as unknown as MedplumServerConfig); } catch (err) { expect((err as Error).message).toStrictEqual('Missing issuer'); } try { await generateIdToken({ iss: config.issuer, login_id: '123', nonce: randomUUID() }); } catch (err) { expect((err as Error).message).toStrictEqual('Signing key not initialized'); } try { await verifyJwt('xyz'); } catch (err) { expect((err as Error).message).toStrictEqual('Signing key not initialized'); } }); test('Missing kid', async () => { expect.assertions(1); const config = await loadTestConfig(); await initKeys(config); // Construct a broken JWT with empty "kid" const accessToken = await new SignJWT({}) .setProtectedHeader({ alg: 'ES256', kid: '', typ: 'JWT' }) .setIssuedAt() .setIssuer(config.issuer) .setAudience('my-audience') .setExpirationTime('1h') .sign(getSigningKey()); try { await verifyJwt(accessToken); } catch (err) { expect((err as Error).message).toStrictEqual('Missing kid header'); } }); test('Key not found', async () => { expect.assertions(1); const config = await loadTestConfig(); await initKeys(config); // Construct a JWT with different key const { privateKey } = await generateKeyPair('RS256'); const accessToken = await new SignJWT({}) .setProtectedHeader({ alg: 'RS256', kid: 'my-kid', typ: 'JWT' }) .setIssuedAt() .setIssuer(config.issuer) .setAudience('my-audience') .setExpirationTime('1h') .sign(privateKey); try { await verifyJwt(accessToken); } catch (err) { expect((err as Error).message).toStrictEqual('Key not found'); } }); test('Generate ID token', async () => { const config = await loadTestConfig(); await initKeys(config); const token = await generateIdToken({ iss: config.issuer, login_id: '123', nonce: randomUUID(), }); expect(token).toBeDefined(); const result = await verifyJwt(token); expect(result.payload.login_id).toStrictEqual('123'); }); test('Generate access token', async () => { const config = await loadTestConfig(); await initKeys(config); const token = await generateAccessToken({ iss: config.issuer, login_id: '123', username: 'username', scope: 'scope', profile: 'profile', }); expect(token).toBeDefined(); const result = await verifyJwt(token); expect(result.payload.login_id).toStrictEqual('123'); }); test('Generate refresh token', async () => { const config = await loadTestConfig(); await initKeys(config); const token = await generateRefreshToken({ iss: config.issuer, login_id: '123', refresh_secret: 'secret', }); expect(token).toBeDefined(); const result = await verifyJwt(token); expect(result.payload.login_id).toStrictEqual('123'); }); test('Tokens include NotBefore claim', async () => { const config = await loadTestConfig(); await initKeys(config); const currentTime = Math.floor(Date.now() / 1000); // Test access token const accessToken = await generateAccessToken({ iss: config.issuer, login_id: '123', username: 'username', scope: 'scope', profile: 'profile', }); const accessResult = await verifyJwt(accessToken); expect(accessResult.payload.nbf).toBeDefined(); expect(typeof accessResult.payload.nbf).toBe('number'); // nbf should be close to current time (within 5 seconds) expect(Math.abs((accessResult.payload.nbf as number) - currentTime)).toBeLessThan(5); // nbf should be close to iat (within 1 second) expect(Math.abs((accessResult.payload.nbf as number) - (accessResult.payload.iat as number))).toBeLessThan(1); // Test ID token const idToken = await generateIdToken({ iss: config.issuer, login_id: '123', nonce: randomUUID(), }); const idResult = await verifyJwt(idToken); expect(idResult.payload.nbf).toBeDefined(); expect(typeof idResult.payload.nbf).toBe('number'); // Test refresh token const refreshToken = await generateRefreshToken({ iss: config.issuer, login_id: '123', refresh_secret: 'secret', }); const refreshResult = await verifyJwt(refreshToken); expect(refreshResult.payload.nbf).toBeDefined(); expect(typeof refreshResult.payload.nbf).toBe('number'); }); test('Generate secret', () => { expect(generateSecret(16)).toHaveLength(32); expect(generateSecret(32)).toHaveLength(64); }); test('Generate access token with email', async () => { const config = await loadTestConfig(); await initKeys(config); const userEmail = 'test@example.com'; const token = await generateAccessToken({ iss: config.issuer, login_id: '123', username: 'username', scope: 'openid profile email', profile: 'Patient/123', email: userEmail, }); expect(token).toBeDefined(); const result = await verifyJwt(token); expect(result.payload.login_id).toStrictEqual('123'); expect(result.payload.email).toStrictEqual(userEmail); }); test('Generate access token without email', async () => { const config = await loadTestConfig(); await initKeys(config); const token = await generateAccessToken({ iss: config.issuer, login_id: '123', username: 'username', scope: 'openid profile', profile: 'Patient/123', }); expect(token).toBeDefined(); const result = await verifyJwt(token); expect(result.payload.login_id).toStrictEqual('123'); expect(result.payload.email).toBeUndefined(); }); test('Generate ID token with email', async () => { const config = await loadTestConfig(); await initKeys(config); const userEmail = 'test@example.com'; const token = await generateIdToken({ iss: config.issuer, login_id: '123', nonce: randomUUID(), email: userEmail, }); expect(token).toBeDefined(); const result = await verifyJwt(token); expect(result.payload.login_id).toStrictEqual('123'); expect(result.payload.email).toStrictEqual(userEmail); }); test('Generate ID token without email', async () => { const config = await loadTestConfig(); await initKeys(config); const token = await generateIdToken({ iss: config.issuer, login_id: '123', nonce: randomUUID(), }); expect(token).toBeDefined(); const result = await verifyJwt(token); expect(result.payload.login_id).toStrictEqual('123'); expect(result.payload.email).toBeUndefined(); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/medplum/medplum'

If you have feedback or need assistance with the MCP directory API, please join our Discord server