Skip to main content
Glama
mfa.test.ts9.46 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { allOk, badRequest } from '@medplum/core'; import type { OperationOutcome } from '@medplum/fhirtypes'; import { randomUUID } from 'crypto'; import express from 'express'; import { authenticator } from 'otplib'; import request from 'supertest'; import { initApp, shutdownApp } from '../app'; import { loadTestConfig } from '../config/loader'; import { withTestContext } from '../test.setup'; import { registerNew } from './register'; const app = express(); describe('MFA', () => { beforeAll(async () => { const config = await loadTestConfig(); await initApp(app, config); }); afterAll(async () => { await shutdownApp(); }); test('Enroll end-to-end', async () => { const email = `alex${randomUUID()}@example.com`; const password = 'password!@#'; const { accessToken } = await withTestContext(() => registerNew({ firstName: 'Alexander', lastName: 'Hamilton', projectName: 'Hamilton Project', email, password, remoteAddress: '5.5.5.5', userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/107.0.0.0', }) ); // Try to enroll before ever getting status, should fail const res1 = await request(app) .post('/auth/mfa/enroll') .set('Authorization', `Bearer ${accessToken}`) .type('json') .send({ token: authenticator.generate('1234567890') }); expect(res1.status).toBe(400); expect(res1.body.issue[0].details.text).toBe('Secret not found'); // Start new login const res2 = await request(app).post('/auth/login').type('json').send({ email, password, scope: 'openid', }); expect(res2.status).toBe(200); expect(res2.body.login).toBeDefined(); // Try to verify before enrolling, should fail const res3 = await request(app) .post('/auth/mfa/verify') .set('Authorization', `Bearer ${accessToken}`) .type('json') .send({ login: res2.body.login, token: authenticator.generate('1234567890') }); expect(res3.status).toBe(400); expect(res3.body.issue[0].details.text).toBe('User not enrolled in MFA'); // Get MFA status, should be disabled const res4 = await request(app).get('/auth/mfa/status').set('Authorization', `Bearer ${accessToken}`); expect(res4.status).toBe(200); expect(res4.body).toBeDefined(); expect(res4.body.enrolled).toBe(false); expect(res4.body.enrollUri).toBeDefined(); const secret = new URL(res4.body.enrollUri).searchParams.get('secret') as string; // Get MFA status again, should be the same enroll URI const res5 = await request(app).get('/auth/mfa/status').set('Authorization', `Bearer ${accessToken}`); expect(res5.status).toBe(200); expect(res5.body).toBeDefined(); expect(res5.body.enrollUri).toBe(res4.body.enrollUri); // Try to enroll with invalid token, should fail const res6 = await request(app) .post('/auth/mfa/enroll') .set('Authorization', `Bearer ${accessToken}`) .type('json') .send({ token: '1234567890' }); expect(res6.status).toBe(400); expect(res6.body.issue[0].details.text).toBe('Invalid token'); // Enroll MFA const res7 = await request(app) .post('/auth/mfa/enroll') .set('Authorization', `Bearer ${accessToken}`) .type('json') .send({ token: authenticator.generate(secret) }); expect(res7.status).toBe(200); // Try to enroll again, should fail const res8 = await request(app) .post('/auth/mfa/enroll') .set('Authorization', `Bearer ${accessToken}`) .type('json') .send({ token: authenticator.generate(secret) }); expect(res8.status).toBe(400); expect(res8.body.issue[0].details.text).toBe('Already enrolled'); // Get MFA status, should be enrolled const res9 = await request(app).get('/auth/mfa/status').set('Authorization', `Bearer ${accessToken}`); expect(res9.status).toBe(200); expect(res9.body).toBeDefined(); expect(res9.body.enrolled).toBe(true); // Start new login const res10 = await request(app).post('/auth/login').type('json').send({ email, password, scope: 'openid', }); expect(res10.status).toBe(200); expect(res10.body.login).toBeDefined(); expect(res10.body.code).not.toBeDefined(); // Verify without token, should fail const res11 = await request(app) .post('/auth/mfa/verify') .set('Authorization', `Bearer ${accessToken}`) .type('json') .send({ login: res10.body.login, token: '' }); expect(res11.status).toBe(400); expect(res11.body.issue[0].details.text).toBe('Missing token'); // Verify with invalid token, should fail const res12 = await request(app) .post('/auth/mfa/verify') .set('Authorization', `Bearer ${accessToken}`) .type('json') .send({ login: res10.body.login, token: '1234567890' }); expect(res12.status).toBe(400); expect(res12.body.issue[0].details.text).toBe('Invalid MFA token'); // Verify MFA success const res13 = await request(app) .post('/auth/mfa/verify') .set('Authorization', `Bearer ${accessToken}`) .type('json') .send({ login: res10.body.login, token: authenticator.generate(secret) }); expect(res13.status).toBe(200); expect(res13.body.login).toBeDefined(); expect(res13.body.code).toBeDefined(); }); test('Disable end-to-end', async () => { const email = `alex${randomUUID()}@example.com`; const password = 'password!@#'; const { accessToken } = await withTestContext(() => registerNew({ firstName: 'Alexander', lastName: 'The Great', projectName: 'Macedonian Project', email, password, remoteAddress: '5.5.5.5', userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/107.0.0.0', }) ); // Call disable while not enrolled yet and before status, should error const res1 = await request(app) .post('/auth/mfa/disable') .set('Authorization', `Bearer ${accessToken}`) .type('json') .send({ token: '123', }); expect(res1.status).toBe(400); expect(res1.body).toMatchObject<OperationOutcome>(badRequest('Secret not found')); // Get status; should not be enrolled and should get a secret const res2 = await request(app).get('/auth/mfa/status').set('Authorization', `Bearer ${accessToken}`); expect(res2.status).toBe(200); expect(res2.body.enrolled).toBe(false); expect(res2.body).toBeDefined(); expect(res2.body.enrollUri).toBeDefined(); // Start new login const res3 = await request(app).post('/auth/login').type('json').send({ email, password, scope: 'openid', }); expect(res3.status).toBe(200); expect(res3.body.login).toBeDefined(); // Get MFA status, should be disabled const res4 = await request(app).get('/auth/mfa/status').set('Authorization', `Bearer ${accessToken}`); expect(res4.status).toBe(200); expect(res4.body).toBeDefined(); expect(res4.body.enrolled).toBe(false); expect(res4.body.enrollUri).toBeDefined(); const secret = new URL(res4.body.enrollUri).searchParams.get('secret') as string; // Enroll MFA const res5 = await request(app) .post('/auth/mfa/enroll') .set('Authorization', `Bearer ${accessToken}`) .type('json') .send({ token: authenticator.generate(secret) }); expect(res5.status).toBe(200); // Call disable without token, should fail const res6 = await request(app) .post('/auth/mfa/disable') .set('Authorization', `Bearer ${accessToken}`) .type('json'); expect(res6.status).toBe(400); expect(res6.body).toMatchObject<OperationOutcome>(badRequest('Missing token')); // Call disable with invalid token, should fail const res7 = await request(app) .post('/auth/mfa/disable') .set('Authorization', `Bearer ${accessToken}`) .type('json') .send({ token: 'invalid' }); expect(res7.status).toBe(400); expect(res7.body).toMatchObject<OperationOutcome>(badRequest('Invalid token')); // Call disable with token, should succeed const res8 = await request(app) .post('/auth/mfa/disable') .set('Authorization', `Bearer ${accessToken}`) .type('json') .send({ token: authenticator.generate(secret) }); expect(res8.status).toBe(200); expect(res8.body).toMatchObject<OperationOutcome>(allOk); // Get status should not be enrolled and should have a new secret const res9 = await request(app).get('/auth/mfa/status').set('Authorization', `Bearer ${accessToken}`); expect(res9.status).toBe(200); expect(res9.body.enrolled).toBe(false); expect(res9.body).toBeDefined(); expect(res9.body.enrollUri).not.toBe(res4.body.enrollUri); const secret2 = new URL(res9.body.enrollUri).searchParams.get('secret') as string; // Call disable while no longer enrolled, should error const res10 = await request(app) .post('/auth/mfa/disable') .set('Authorization', `Bearer ${accessToken}`) .type('json') .send({ token: authenticator.generate(secret2), }); expect(res10.status).toBe(400); expect(res10.body).toMatchObject<OperationOutcome>(badRequest('User not enrolled in MFA')); }); });

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