feat: establish auth listing and image vertical slices
Includes MFA-capable authentication, session and CSRF security, password recovery, listing lifecycle and ownership rules, inventory image metadata, local ImageStorage, migrations, tests, and implementation status documentation. Verification: npm test (62 passing) and integration suite (14 passing).
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { buildApp } from '../../src/app/build-app.js';
|
||||
|
||||
const userId = '464e9b49-8617-4d00-af3c-78b3294e1d70';
|
||||
const methodId = '6833868a-2a1b-4d33-90d1-fc9f2b7be869';
|
||||
const headers = {
|
||||
cookie: 'pokemon_inventory_session=session-token',
|
||||
'x-csrf-token': 'csrf-token',
|
||||
};
|
||||
|
||||
function createTestApp(mfaService) {
|
||||
return buildApp({
|
||||
authenticationService: {
|
||||
async authorizeUnsafeRequest(sessionToken, csrfToken) {
|
||||
assert.equal(sessionToken, 'session-token');
|
||||
assert.equal(csrfToken, 'csrf-token');
|
||||
return { userId };
|
||||
},
|
||||
},
|
||||
loginRateLimiter: { async consume() { return { allowed: true }; } },
|
||||
mfaService,
|
||||
});
|
||||
}
|
||||
|
||||
test('enrolls MFA only for the authenticated user and returns its provisioning URI', async (testContext) => {
|
||||
const app = createTestApp({
|
||||
async enroll(input) {
|
||||
assert.deepEqual(input, { userId, name: 'Authenticator' });
|
||||
return { id: methodId, provisioningUri: 'otpauth://totp/example' };
|
||||
},
|
||||
});
|
||||
testContext.after(() => app.close());
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/v1/auth/mfa/enroll',
|
||||
headers,
|
||||
payload: { name: 'Authenticator' },
|
||||
});
|
||||
|
||||
assert.equal(response.statusCode, 201);
|
||||
assert.equal(response.json().data.provisioningUri, 'otpauth://totp/example');
|
||||
});
|
||||
|
||||
test('verifies MFA with CSRF protection and does not expose secret material', async (testContext) => {
|
||||
const app = createTestApp({
|
||||
async verifyEnrollment(input) {
|
||||
assert.deepEqual(input, { userId, methodId, code: '123456' });
|
||||
return { id: methodId, name: 'Authenticator', type: 'TOTP', secretCiphertext: 'hidden' };
|
||||
},
|
||||
});
|
||||
testContext.after(() => app.close());
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/v1/auth/mfa/verify',
|
||||
headers,
|
||||
payload: { methodId, code: '123456' },
|
||||
});
|
||||
|
||||
assert.equal(response.statusCode, 200);
|
||||
assert.equal(response.json().data.secretCiphertext, undefined);
|
||||
assert.equal(response.json().data.type, 'TOTP');
|
||||
});
|
||||
|
||||
test('rejects MFA enrollment without CSRF validation', async (testContext) => {
|
||||
const app = buildApp({
|
||||
authenticationService: {
|
||||
async authorizeUnsafeRequest() {
|
||||
const error = new Error('CSRF validation failed.');
|
||||
error.code = 'FORBIDDEN';
|
||||
error.statusCode = 403;
|
||||
throw error;
|
||||
},
|
||||
},
|
||||
loginRateLimiter: { async consume() { return { allowed: true }; } },
|
||||
mfaService: { async enroll() {} },
|
||||
});
|
||||
testContext.after(() => app.close());
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/v1/auth/mfa/enroll',
|
||||
payload: { name: 'Authenticator' },
|
||||
});
|
||||
|
||||
assert.equal(response.statusCode, 403);
|
||||
assert.equal(response.json().error.code, 'FORBIDDEN');
|
||||
});
|
||||
Reference in New Issue
Block a user