// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
// SPDX-License-Identifier: Apache-2.0
import type { WithId } from '@medplum/core';
import { ContentType, createReference } from '@medplum/core';
import type { ClientApplication, Login } from '@medplum/fhirtypes';
import { randomUUID } from 'crypto';
import express from 'express';
import request from 'supertest';
import { initApp, shutdownApp } from '../app';
import { getConfig, loadTestConfig } from '../config/loader';
import { getSystemRepo } from '../fhir/repo';
import { createTestClient, createTestProject, withTestContext } from '../test.setup';
import { generateAccessToken, generateSecret } from './keys';
import { PROMPT_BASIC_AUTH_PARAM } from './middleware';
describe('Auth middleware', () => {
const app = express();
const systemRepo = getSystemRepo();
let client: WithId<ClientApplication>;
beforeAll(async () => {
const config = await loadTestConfig();
await initApp(app, config);
client = await createTestClient();
});
afterAll(async () => {
await shutdownApp();
});
test('Login not found', async () => {
const accessToken = await generateAccessToken({
login_id: randomUUID(),
sub: client.id,
username: client.id,
client_id: client.id,
profile: client.resourceType + '/' + client.id,
scope: 'openid',
});
const res = await request(app)
.get('/fhir/R4/Patient')
.set('Authorization', 'Bearer ' + accessToken);
expect(res.status).toBe(401);
});
test('Login revoked', async () => {
const scope = 'openid';
const login = await withTestContext(() =>
systemRepo.createResource<Login>({
resourceType: 'Login',
authMethod: 'client',
user: createReference(client),
client: createReference(client),
authTime: new Date().toISOString(),
revoked: true,
scope,
})
);
const accessToken = await generateAccessToken({
login_id: login.id,
sub: client.id,
username: client.id,
client_id: client.id,
profile: client.resourceType + '/' + client.id,
scope,
});
const res = await request(app)
.get('/fhir/R4/Patient')
.set('Authorization', 'Bearer ' + accessToken);
expect(res.status).toBe(401);
});
test('No auth header', async () => {
const res = await request(app).get('/fhir/R4/Patient');
expect(res.header['www-authenticate']).toBeUndefined();
expect(res.status).toBe(401);
});
test('No auth header with magic param', async () => {
const res = await request(app).get(`/fhir/R4/Patient?${PROMPT_BASIC_AUTH_PARAM}=1`);
expect(res.header['www-authenticate']).toBe(`Basic realm="${getConfig().baseUrl}"`);
expect(res.status).toBe(401);
});
test('Unrecognized auth header', async () => {
const res = await request(app).get('/fhir/R4/Patient').set('Authorization', 'foo');
expect(res.status).toBe(401);
});
test('Unrecognized auth token type', async () => {
const res = await request(app).get('/fhir/R4/Patient').set('Authorization', 'foo foo');
expect(res.status).toBe(401);
});
test('Invalid bearer token', async () => {
const res = await request(app).get('/fhir/R4/Patient').set('Authorization', 'Bearer foo');
expect(res.status).toBe(401);
});
test('Basic auth empty string', async () => {
const res = await request(app).get('/fhir/R4/Patient').set('Authorization', 'Basic ');
expect(res.status).toBe(401);
});
test('Basic auth malformed string', async () => {
const res = await request(app).get('/fhir/R4/Patient').set('Authorization', 'Basic foo');
expect(res.status).toBe(401);
});
test('Basic auth empty username', async () => {
const res = await request(app)
.get('/fhir/R4/Patient')
.set('Authorization', 'Basic ' + Buffer.from(':' + client.secret).toString('base64'));
expect(res.status).toBe(401);
});
test('Basic auth empty password', async () => {
const res = await request(app)
.get('/fhir/R4/Patient')
.set('Authorization', 'Basic ' + Buffer.from(client.id + ':').toString('base64'));
expect(res.status).toBe(401);
});
test('Basic auth client not found', async () => {
const res = await request(app)
.get('/fhir/R4/Patient')
.set('Authorization', 'Basic ' + Buffer.from(randomUUID() + ':' + client.secret).toString('base64'));
expect(res.status).toBe(401);
});
test('Basic auth wrong password', async () => {
const res = await request(app)
.get('/fhir/R4/Patient')
.set('Authorization', 'Basic ' + Buffer.from(client.id + ':wrong').toString('base64'));
expect(res.status).toBe(401);
});
test('Basic auth success', async () => {
const res = await request(app)
.get('/fhir/R4/Patient')
.set('Authorization', 'Basic ' + Buffer.from(client.id + ':' + client.secret).toString('base64'));
expect(res.status).toBe(200);
});
test('Basic auth project', async () => {
const res = await request(app)
.post('/fhir/R4/Patient')
.set('Authorization', 'Basic ' + Buffer.from(client.id + ':' + client.secret).toString('base64'))
.set('Content-Type', ContentType.FHIR_JSON)
.send({
resourceType: 'Patient',
name: [
{
given: ['Given'],
family: 'Family',
},
],
});
expect(res.status).toBe(201);
expect(res.body.meta).toBeDefined();
expect(res.body.meta.project).toBeUndefined();
});
test('Basic auth project with extended mode', async () => {
const res = await request(app)
.post('/fhir/R4/Patient')
.set('Authorization', 'Basic ' + Buffer.from(client.id + ':' + client.secret).toString('base64'))
.set('Content-Type', ContentType.FHIR_JSON)
.set('X-Medplum', 'extended')
.send({
resourceType: 'Patient',
name: [
{
given: ['Given'],
family: 'Family',
},
],
});
expect(res.status).toBe(201);
expect(res.body.meta).toBeDefined();
expect(res.body.meta.project).toBeDefined();
});
test('Basic auth without project membership', async () => {
const client = await withTestContext(() =>
systemRepo.createResource<ClientApplication>({
resourceType: 'ClientApplication',
name: 'Client without project membership',
secret: generateSecret(32),
})
);
const res = await request(app)
.get('/fhir/R4/Patient')
.set('Authorization', 'Basic ' + Buffer.from(client.id + ':' + client.secret).toString('base64'));
expect(res.status).toBe(401);
});
test('Basic auth with super admin client', async () => {
const { client } = await createTestProject({ superAdmin: true, withClient: true });
const res = await request(app)
.get('/fhir/R4/Project?_total=accurate')
.set('Authorization', 'Basic ' + Buffer.from(client.id + ':' + client.secret).toString('base64'));
expect(res.status).toBe(200);
expect(res.body.total).toBeGreaterThan(1);
});
});