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,126 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { createSecretProtector } from '../../src/modules/auth/domain/secret-protection.js';
|
||||
import { createMfaService } from '../../src/modules/auth/services/mfa-service.js';
|
||||
|
||||
function createService({ method, isCodeValid = true } = {}) {
|
||||
const createdMethods = [];
|
||||
const auditEvents = [];
|
||||
const repository = {
|
||||
async create(input) {
|
||||
const createdMethod = { id: 'method-id', isEnabled: false, ...input };
|
||||
createdMethods.push(createdMethod);
|
||||
return createdMethod;
|
||||
},
|
||||
async findActiveByIdAndUserId() {
|
||||
return method ?? createdMethods[0] ?? null;
|
||||
},
|
||||
async enable(id) {
|
||||
return { id, name: 'Authenticator', type: 'TOTP', isEnabled: true };
|
||||
},
|
||||
async disable(id) {
|
||||
return { id, isEnabled: false };
|
||||
},
|
||||
};
|
||||
const service = createMfaService({
|
||||
mfaMethodRepository: repository,
|
||||
now: () => new Date('2026-09-07T00:00:00.000Z'),
|
||||
secretProtector: {
|
||||
encrypt(secret) { return `encrypted:${secret}`; },
|
||||
decrypt(ciphertext) { return ciphertext.replace('encrypted:', ''); },
|
||||
},
|
||||
securityAuditRepository: { async record(event) { auditEvents.push(event); } },
|
||||
totpEnrollment: () => ({ secret: 'TOTPSECRET', provisioningUri: 'otpauth://test' }),
|
||||
totpVerifier: () => isCodeValid,
|
||||
});
|
||||
|
||||
return { auditEvents, createdMethods, service };
|
||||
}
|
||||
|
||||
test('encrypts the TOTP secret and returns the provisioning URI only during enrollment', async () => {
|
||||
const { createdMethods, service } = createService();
|
||||
|
||||
const enrollment = await service.enroll({ userId: 'user-id', name: 'Authenticator' });
|
||||
|
||||
assert.deepEqual(enrollment, { id: 'method-id', provisioningUri: 'otpauth://test' });
|
||||
assert.equal(createdMethods[0].secretCiphertext, 'encrypted:TOTPSECRET');
|
||||
assert.equal(createdMethods[0].isEnabled, false);
|
||||
assert.equal('secret' in enrollment, false);
|
||||
});
|
||||
|
||||
test('encrypts MFA secrets with authenticated encryption', () => {
|
||||
const protector = createSecretProtector(Buffer.alloc(32, 7).toString('base64'));
|
||||
const ciphertext = protector.encrypt('TOTPSECRET');
|
||||
|
||||
assert.notEqual(ciphertext, 'TOTPSECRET');
|
||||
assert.equal(protector.decrypt(ciphertext), 'TOTPSECRET');
|
||||
});
|
||||
|
||||
test('enables an owned MFA method only after a valid code and records enrollment', async () => {
|
||||
const { auditEvents, service } = createService({
|
||||
method: {
|
||||
id: 'method-id',
|
||||
userId: 'user-id',
|
||||
type: 'TOTP',
|
||||
name: 'Authenticator',
|
||||
secretCiphertext: 'encrypted:TOTPSECRET',
|
||||
isEnabled: false,
|
||||
},
|
||||
});
|
||||
|
||||
const method = await service.verifyEnrollment({
|
||||
userId: 'user-id',
|
||||
methodId: 'method-id',
|
||||
code: '123456',
|
||||
});
|
||||
|
||||
assert.deepEqual(method, { id: 'method-id', name: 'Authenticator', type: 'TOTP' });
|
||||
assert.deepEqual(auditEvents, [{ userId: 'user-id', event: 'MFA_ENROLLED' }]);
|
||||
});
|
||||
|
||||
test('rejects invalid MFA codes without enabling or disabling a method', async () => {
|
||||
const { auditEvents, service } = createService({
|
||||
isCodeValid: false,
|
||||
method: {
|
||||
id: 'method-id',
|
||||
userId: 'user-id',
|
||||
type: 'TOTP',
|
||||
name: 'Authenticator',
|
||||
secretCiphertext: 'encrypted:TOTPSECRET',
|
||||
isEnabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => service.disable({ userId: 'user-id', methodId: 'method-id', code: '000000' }),
|
||||
(error) => error.code === 'MFA_CODE_INVALID',
|
||||
);
|
||||
assert.deepEqual(auditEvents, [{ userId: 'user-id', event: 'MFA_FAILURE' }]);
|
||||
});
|
||||
|
||||
test('requires a valid TOTP code when an enabled MFA method exists at login', async () => {
|
||||
let markedMethod;
|
||||
const repository = {
|
||||
async listEnabledByUserId(userId) {
|
||||
assert.equal(userId, 'user-id');
|
||||
return [{ id: 'method-id', secretCiphertext: 'encrypted:TOTPSECRET' }];
|
||||
},
|
||||
async markUsed(id) {
|
||||
markedMethod = id;
|
||||
},
|
||||
};
|
||||
const service = createMfaService({
|
||||
mfaMethodRepository: repository,
|
||||
secretProtector: { decrypt() { return 'TOTPSECRET'; } },
|
||||
totpVerifier: ({ code }) => code === '123456',
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => service.verifyLogin({ userId: 'user-id', code: undefined }),
|
||||
(error) => error.code === 'MFA_CODE_INVALID',
|
||||
);
|
||||
await service.verifyLogin({ userId: 'user-id', code: '123456' });
|
||||
|
||||
assert.equal(markedMethod, 'method-id');
|
||||
});
|
||||
Reference in New Issue
Block a user