From 8807d201563f9da3a3da8b8564ec519f8aa55ef5 Mon Sep 17 00:00:00 2001 From: Fabian Meyer <3982806+meyfa@users.noreply.github.com> Date: Thu, 18 Jul 2024 10:19:10 +0200 Subject: [PATCH] test: Add tests for authentication strategies (#64) Co-authored-by: Lukas Jost --- backend/test/auth/local-strategy.test.ts | 84 +++++++++++++++++++++ backend/test/auth/oidc-strategy.test.ts | 95 ++++++++++++++++++++++++ 2 files changed, 179 insertions(+) create mode 100644 backend/test/auth/local-strategy.test.ts create mode 100644 backend/test/auth/oidc-strategy.test.ts diff --git a/backend/test/auth/local-strategy.test.ts b/backend/test/auth/local-strategy.test.ts new file mode 100644 index 0000000..2a2c151 --- /dev/null +++ b/backend/test/auth/local-strategy.test.ts @@ -0,0 +1,84 @@ +import { makeLocalStrategy } from '../../src/auth/local-strategy.js' +import assert from 'node:assert' + +describe('auth/local-strategy.ts', () => { + describe('makeLocalStrategy()', () => { + it('has name "local"', async () => { + const strategy = await makeLocalStrategy({ username: 'admin', password: 'password' }) + assert.strictEqual(strategy.name, 'local') + }) + + it('denies access if the admin user has no password', async () => { + const mockRequest = { + body: { + username: 'admin', + password: '' + } + } + const strategy = await makeLocalStrategy({ username: 'admin', password: '' }) + const promise = new Promise((resolve, reject) => { + strategy.fail = () => resolve() + strategy.success = () => reject(new Error('should not have succeeded')) + }) + strategy.authenticate(mockRequest as any) + await promise + }) + + it('denies access if the username is incorrect', async () => { + const mockRequest = { + body: { + username: 'aaaaa', + password: 'password' + } + } + const strategy = await makeLocalStrategy({ username: 'admin', password: 'password' }) + const promise = new Promise((resolve, reject) => { + strategy.fail = () => resolve() + strategy.success = () => reject(new Error('should not have succeeded')) + }) + strategy.authenticate(mockRequest as any) + await promise + }) + + it('denies access if the password is incorrect', async () => { + const mockRequest = { + body: { + username: 'admin', + password: 'wrongpassword' + } + } + const strategy = await makeLocalStrategy({ username: 'admin', password: 'password' }) + const promise = new Promise((resolve, reject) => { + strategy.fail = () => resolve() + strategy.success = () => reject(new Error('should not have succeeded')) + }) + strategy.authenticate(mockRequest as any) + await promise + }) + + it('allows access if the username and password are correct', async () => { + const mockRequest = { + body: { + username: 'admin', + password: 'password' + } + } + const strategy = await makeLocalStrategy({ username: 'admin', password: 'password' }) + const promise = new Promise((resolve, reject) => { + strategy.fail = () => reject(new Error('should not have failed')) + strategy.success = (user, info) => { + assert.deepStrictEqual(user, { + strategy: 'local', + username: 'admin', + createdAt: user.createdAt + }) + assert.ok(Math.abs(user.createdAt - Date.now()) < 1000) + assert.strictEqual(info, undefined) + resolve() + } + }) + strategy.authenticate(mockRequest as any) + await promise + }) + }) +}) diff --git a/backend/test/auth/oidc-strategy.test.ts b/backend/test/auth/oidc-strategy.test.ts new file mode 100644 index 0000000..016ba8d --- /dev/null +++ b/backend/test/auth/oidc-strategy.test.ts @@ -0,0 +1,95 @@ +import { fastify, FastifyInstance } from 'fastify' +import { makeOidcStrategy } from '../../src/auth/oidc-strategy.js' +import assert from 'node:assert' +import { Strategy } from 'openid-client' + +describe('auth/oidc-strategy.ts', () => { + describe('makeOidcStrategy()', () => { + let app: FastifyInstance | undefined + + afterEach(async () => { + await app?.close() + app = undefined + }) + + it('performs issuer discovery', async () => { + app = fastify() + let called = false + app.get('/.well-known/openid-configuration', async (req, reply) => { + called = true + return { issuer: 'http://127.0.0.1:58080' } + }) + await app.listen({ host: '127.0.0.1', port: 58080 }) + const strategy = await makeOidcStrategy({ + issuer: 'http://127.0.0.1:58080', + clientId: 'foobar', + clientSecret: 'bazqux', + redirectUri: 'http://localhost:3000/oidc/callback' + }) + assert.ok(strategy instanceof Strategy) + assert.ok(called) + }) + + it('accepts issuer URL with full .well-known path', async () => { + app = fastify() + let called = false + app.get('/.well-known/openid-configuration', async (req, reply) => { + called = true + return { issuer: 'http://127.0.0.1:58080' } + }) + await app.listen({ host: '127.0.0.1', port: 58080 }) + const strategy = await makeOidcStrategy({ + issuer: 'http://127.0.0.1:58080/.well-known/openid-configuration', + clientId: 'foobar', + clientSecret: 'bazqux', + redirectUri: 'http://localhost:3000/oidc/callback' + }) + assert.ok(strategy instanceof Strategy) + assert.ok(called) + }) + + it('redirects unauthenticated users to the OIDC provider', async () => { + app = fastify() + app.get('/.well-known/openid-configuration', async (req, reply) => { + return { + // https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata + issuer: 'http://127.0.0.1:58080', + authorization_endpoint: 'http://127.0.0.1:58080/authorize', + token_endpoint: 'http://127.0.0.1:58080/token', + jwks_uri: 'http://127.0.0.1:58080/jwks', + response_types_supported: ['code'] + } + }) + await app.listen({ host: '127.0.0.1', port: 58080 }) + const strategy = await makeOidcStrategy({ + issuer: 'http://127.0.0.1:58080', + clientId: 'foobar', + clientSecret: 'bazqux', + redirectUri: 'http://localhost:3000/oidc/callback' + }) + const mockRequest = { + method: 'GET', + url: '/', + body: {}, + session: {} + } + const promise = new Promise((resolve, reject) => { + strategy.error = (err: any) => reject(err) + strategy.fail = () => reject(new Error('should not have failed')) + strategy.success = () => reject(new Error('should not have succeeded')) + strategy.pass = () => reject(new Error('should not have passed')) + strategy.redirect = (url: string) => { + const urlObj = new URL(url) + assert.strictEqual(urlObj.origin, 'http://127.0.0.1:58080') + assert.strictEqual(urlObj.pathname, '/authorize') + assert.strictEqual(urlObj.searchParams.get('client_id'), 'foobar') + assert.strictEqual(urlObj.searchParams.get('response_type'), 'code') + assert.strictEqual(urlObj.searchParams.get('redirect_uri'), 'http://localhost:3000/oidc/callback') + resolve() + } + }) + strategy.authenticate(mockRequest as any) + await promise + }) + }) +})