Skip to main content
Glama
external.test.ts6.42 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import type { ProfileResource, WithId } from '@medplum/core'; import { ContentType, encodeBase64Url, getReferenceString } from '@medplum/core'; import type { Practitioner } from '@medplum/fhirtypes'; import { randomUUID } from 'crypto'; import express from 'express'; import fetch from 'node-fetch'; import request from 'supertest'; import { inviteUser } from '../admin/invite'; import { initApp, shutdownApp } from '../app'; import { loadTestConfig } from '../config/loader'; import { getSystemRepo } from '../fhir/repo'; import { createTestProject } from '../test.setup'; jest.mock('node-fetch'); // RFC 7662 - External auth describe('External auth', () => { const app = express(); const npi = randomUUID(); let practitioner: WithId<ProfileResource>; beforeAll(async () => { const config = await loadTestConfig(); config.externalAuthProviders = [ { issuer: 'https://external-auth.example.com', userInfoUrl: 'https://external-auth.example.com/oauth2/userinfo', }, ]; // Initialize the app with the test config await initApp(app, config); // Create a test project const { project } = await createTestProject(); // Invite a normal Practitioner user to the project const inviteResult = await inviteUser({ project, resourceType: 'Practitioner', firstName: 'Test', lastName: 'Person', }); // Add NPI identifier to the practitioner practitioner = await getSystemRepo().updateResource<ProfileResource>({ ...inviteResult.profile, identifier: [{ system: 'npi', value: npi }], }); }); afterAll(async () => { await shutdownApp(); }); test('Not a JWT', async () => { const res = await request(app).get(`/oauth2/userinfo`).set('Authorization', 'Bearer opaque_string'); expect(res.status).toBe(401); }); test('Missing issuer', async () => { const jwt = createFakeJwt({ foo: 'bar' }); const res = await request(app) .get(`/oauth2/userinfo`) .set('Authorization', 'Bearer ' + jwt); expect(res.status).toBe(401); }); test('Unknown issuer', async () => { const jwt = createFakeJwt({ iss: 'https://unknown-issuer.example.com' }); const res = await request(app) .get(`/oauth2/userinfo`) .set('Authorization', 'Bearer ' + jwt); expect(res.status).toBe(401); }); test('Missing fhirUser', async () => { const jwt = createFakeJwt({ iss: 'https://external-auth.example.com' }); const res = await request(app) .get(`/oauth2/userinfo`) .set('Authorization', 'Bearer ' + jwt); expect(res.status).toBe(401); }); test('Remote call to userinfo fails', async () => { (fetch as unknown as jest.Mock).mockImplementationOnce(() => ({ status: 401, headers: { get: () => ContentType.JSON }, json: () => ({ ok: false }), })); const jwt = createFakeJwt({ iss: 'https://external-auth.example.com', fhirUser: getReferenceString(practitioner), }); const res = await request(app) .get(`/oauth2/userinfo`) .set('Authorization', 'Bearer ' + jwt); expect(res.status).toBe(401); }); test('Profile not found', async () => { (fetch as unknown as jest.Mock).mockImplementationOnce(() => ({ status: 200, headers: { get: () => ContentType.JSON }, json: () => ({ ok: true }), })); const jwt = createFakeJwt({ iss: 'https://external-auth.example.com', fhirUser: 'Patient/123', }); const res = await request(app) .get(`/oauth2/userinfo`) .set('Authorization', 'Bearer ' + jwt); expect(res.status).toBe(401); }); test('Profile without membership', async () => { (fetch as unknown as jest.Mock).mockImplementationOnce(() => ({ status: 200, headers: { get: () => ContentType.JSON }, json: () => ({ ok: true }), })); // Create a Practitioner profile that is not a member of the project const p2 = await getSystemRepo().createResource<Practitioner>({ resourceType: 'Practitioner' }); const jwt = createFakeJwt({ iss: 'https://external-auth.example.com', fhirUser: getReferenceString(p2), }); const res = await request(app) .get(`/oauth2/userinfo`) .set('Authorization', 'Bearer ' + jwt); expect(res.status).toBe(401); }); test('Success by reference', async () => { (fetch as unknown as jest.Mock).mockImplementationOnce(() => ({ status: 200, headers: { get: () => ContentType.JSON }, json: () => ({ ok: true }), })); const jwt = createFakeJwt({ iss: 'https://external-auth.example.com', fhirUser: getReferenceString(practitioner), scope: 'openid profile', nonce: randomUUID(), }); const res = await request(app) .get(`/oauth2/userinfo`) .set('Authorization', 'Bearer ' + jwt); expect(res.status).toBe(200); // Call it again to ensure caching works const res2 = await request(app) .get(`/oauth2/userinfo`) .set('Authorization', 'Bearer ' + jwt); expect(res2.status).toBe(200); }); test('Success by search string', async () => { (fetch as unknown as jest.Mock).mockImplementationOnce(() => ({ status: 200, headers: { get: () => ContentType.JSON }, json: () => ({ ok: true }), })); const jwt = createFakeJwt({ iss: 'https://external-auth.example.com', fhirUser: `Practitioner?identifier=${npi}`, }); const res = await request(app) .get(`/oauth2/userinfo`) .set('Authorization', 'Bearer ' + jwt); expect(res.status).toBe(200); }); test('Success by absolute URL', async () => { (fetch as unknown as jest.Mock).mockImplementationOnce(() => ({ status: 200, headers: { get: () => ContentType.JSON }, json: () => ({ ok: true }), })); const jwt = createFakeJwt({ iss: 'https://external-auth.example.com', fhirUser: `https://external.idp/fhir/Practitioner?identifier=${npi}`, }); const res = await request(app) .get(`/oauth2/userinfo`) .set('Authorization', 'Bearer ' + jwt); expect(res.status).toBe(200); }); }); function createFakeJwt(claims: Record<string, unknown>): string { return `header.${encodeBase64Url(JSON.stringify(claims))}.signature`; }

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