refactor(api): collapse single-impl interfaces and facades (#3382)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-10 23:04:42 +02:00
1 parent e7b13fd19f
commit c16d414d93
426 files changed
+2085 -6445

No files matched your search

@@ -1,10 +1,16 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {randomBytes} from 'node:crypto';
import {createAuthHarness, createTestAccount, loginAccount, type TestAccount} from '@app/api/auth/tests/AuthTestUtils';
import {
createAuthHarness,
createTestAccount,
createTotpSecret,
loginAccount,
loginWithTotp,
type TestAccount,
totpCodeNow,
} from '@app/api/auth/tests/AuthTestUtils';
import type {ApiTestHarness} from '@app/api/test/ApiTestHarness';
import {createBuilder, createBuilderWithoutAuth} from '@app/api/test/TestRequestBuilder';
import {TotpGenerator} from '@app/api/utils/TotpGenerator';
import {createBuilder} from '@app/api/test/TestRequestBuilder';
import type {AuthSessionResponse} from '@fluxer/schema/src/domains/auth/AuthSchemas';
import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
@@ -25,65 +31,6 @@ interface OAuth2ApplicationResponse {
};
}
function generateTotpSecret(): string {
const buffer = randomBytes(20);
const base32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
let result = '';
for (let i = 0; i < buffer.length; i += 5) {
const bytes = [buffer[i]!, buffer[i + 1]!, buffer[i + 2]!, buffer[i + 3]!, buffer[i + 4]!];
const n = (bytes[0]! << 24) | (bytes[1]! << 16) | (bytes[2]! << 8) | bytes[3]!;
const indices = [
(n >> 3) & 0x1f,
((n >> 11) | ((bytes[4]! << 4) & 0xf)) & 0x1f,
((n >> 19) | ((bytes[4]! << 2) & 0x3c)) & 0x1f,
(bytes[4]! >> 1) & 0x1f,
];
result += base32Chars[indices[0]!];
result += base32Chars[indices[1]!];
result += base32Chars[indices[2]!];
result += base32Chars[indices[3]!];
}
return result;
}
async function generateTotpCode(secret: string): Promise<string> {
const totp = new TotpGenerator(secret);
const codes = await totp.generateTotp();
return codes[0]!;
}
async function loginWithTotp(harness: ApiTestHarness, account: TestAccount, secret: string): Promise<TestAccount> {
const loginResp = await createBuilderWithoutAuth<
| {
mfa: true;
ticket: string;
}
| {
mfa: false;
token: string;
}
>(harness)
.post('/auth/login')
.body({
email: account.email,
password: account.password,
})
.execute();
if (!loginResp.mfa) {
throw new Error('Expected MFA login');
}
const mfaLoginResp = await createBuilderWithoutAuth<{
token: string;
}>(harness)
.post('/auth/login/mfa/totp')
.body({
ticket: loginResp.ticket,
code: await generateTotpCode(secret),
})
.execute();
return {...account, token: mfaLoginResp.token};
}
async function createOAuth2BotApplication(
harness: ApiTestHarness,
account: TestAccount,
@@ -177,8 +124,8 @@ describe('Auth sudo required operations', () => {
});
it('requires sudo for disable TOTP MFA', async () => {
let account = await createTestAccount(harness);
const secret = generateTotpSecret();
const code = await generateTotpCode(secret);
const secret = createTotpSecret();
const code = totpCodeNow(secret);
const enableResp = await createBuilder<BackupCodesResponse>(harness, account.token)
.post('/users/@me/mfa/totp/enable')
.body({
@@ -202,15 +149,15 @@ describe('Auth sudo required operations', () => {
.body({
code: backupCode,
mfa_method: 'totp',
mfa_code: await generateTotpCode(secret),
mfa_code: totpCodeNow(secret),
})
.expect(204)
.execute();
});
it('requires sudo for enable TOTP MFA', async () => {
const account = await createTestAccount(harness);
const secret = generateTotpSecret();
const code = await generateTotpCode(secret);
const secret = createTotpSecret();
const code = totpCodeNow(secret);
await createBuilder(harness, account.token)
.post('/users/@me/mfa/totp/enable')
.body({
@@ -4,11 +4,11 @@ import {
createAuthHarness,
createTestAccount,
createTotpSecret,
type TestAccount,
loginWithTotp,
totpCodeNow,
} from '@app/api/auth/tests/AuthTestUtils';
import type {ApiTestHarness} from '@app/api/test/ApiTestHarness';
import {createBuilder, createBuilderWithoutAuth} from '@app/api/test/TestRequestBuilder';
import {createBuilder} from '@app/api/test/TestRequestBuilder';
import type {AuthSessionResponse} from '@fluxer/schema/src/domains/auth/AuthSchemas';
import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
@@ -21,38 +21,6 @@ interface BackupCodesResponse {
const SUDO_MODE_HEADER = 'X-Fluxer-Sudo-Mode-JWT';
async function loginWithTotp(harness: ApiTestHarness, account: TestAccount, secret: string): Promise<TestAccount> {
const loginResp = await createBuilderWithoutAuth<
| {
mfa: true;
ticket: string;
}
| {
mfa: false;
token: string;
}
>(harness)
.post('/auth/login')
.body({
email: account.email,
password: account.password,
})
.execute();
if (!loginResp.mfa) {
throw new Error('Expected MFA login');
}
const mfaLoginResp = await createBuilderWithoutAuth<{
token: string;
}>(harness)
.post('/auth/login/mfa/totp')
.body({
ticket: loginResp.ticket,
code: totpCodeNow(secret),
})
.execute();
return {...account, token: mfaLoginResp.token};
}
describe('Auth sudo TOTP verification', () => {
let harness: ApiTestHarness;
beforeAll(async () => {
@@ -260,6 +260,22 @@ export function totpCodeNow(secret: string): string {
return generateTotpCode(secret, Date.now());
}
export async function loginWithTotp(
harness: ApiTestHarness,
account: TestAccount,
secret: string,
): Promise<TestAccount> {
const login = await loginUser(harness, {email: account.email, password: account.password});
if (!('mfa' in login)) {
throw new Error('Expected MFA login');
}
const mfaLogin = await createBuilder<{token: string}>(harness, '')
.post('/auth/login/mfa/totp')
.body({ticket: login.ticket, code: totpCodeNow(secret)})
.execute();
return {...account, token: mfaLogin.token};
}
export async function seedMfaTicket(
harness: ApiTestHarness,
ticket: string,
@@ -4,6 +4,7 @@ import {
createTestAccount,
createTotpSecret,
generateTotpCode,
loginWithTotp,
type TestAccount,
} from '@app/api/auth/tests/AuthTestUtils';
import {
@@ -51,27 +52,6 @@ interface SudoModeRequiredResponse {
};
}
async function loginWithTotp(harness: ApiTestHarness, account: TestAccount, secret: string): Promise<TestAccount> {
const login = await createBuilderWithoutAuth<LoginMfaResponse>(harness)
.post('/auth/login')
.body({
email: account.email,
password: account.password,
})
.execute();
expect(login.mfa).toBe(true);
const mfaLogin = await createBuilderWithoutAuth<{
token: string;
}>(harness)
.post('/auth/login/mfa/totp')
.body({
code: generateTotpCode(secret),
ticket: login.ticket,
})
.execute();
return {...account, token: mfaLogin.token};
}
async function setupWebAuthnOnlyUser(
harness: ApiTestHarness,
account: TestAccount,
@@ -1,11 +1,17 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {randomBytes} from 'node:crypto';
import {createAuthHarness, createTestAccount, loginUser, type TestAccount} from '@app/api/auth/tests/AuthTestUtils';
import {
createAuthHarness,
createTestAccount,
createTotpSecret,
loginUser,
loginWithTotp,
type TestAccount,
totpCodeNow,
} from '@app/api/auth/tests/AuthTestUtils';
import type {ApiTestHarness} from '@app/api/test/ApiTestHarness';
import {HTTP_STATUS} from '@app/api/test/TestConstants';
import {createBuilder, createBuilderWithoutAuth} from '@app/api/test/TestRequestBuilder';
import {TotpGenerator} from '@app/api/utils/TotpGenerator';
import {createBuilder} from '@app/api/test/TestRequestBuilder';
import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
const SUDO_MODE_HEADER = 'X-Fluxer-Sudo-Mode-JWT';
@@ -22,35 +28,8 @@ interface BackupCodesResponse {
}>;
}
function generateTotpSecret(): string {
const buffer = randomBytes(20);
const base32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
let result = '';
for (let i = 0; i < buffer.length; i += 5) {
const bytes = [buffer[i]!, buffer[i + 1]!, buffer[i + 2]!, buffer[i + 3]!, buffer[i + 4]!];
const n = (bytes[0]! << 24) | (bytes[1]! << 16) | (bytes[2]! << 8) | bytes[3]!;
const indices = [
(n >> 3) & 0x1f,
((n >> 11) | ((bytes[4]! << 4) & 0xf)) & 0x1f,
((n >> 19) | ((bytes[4]! << 2) & 0x3c)) & 0x1f,
(bytes[4]! >> 1) & 0x1f,
];
result += base32Chars[indices[0]!];
result += base32Chars[indices[1]!];
result += base32Chars[indices[2]!];
result += base32Chars[indices[3]!];
}
return result;
}
async function generateTotpCode(secret: string): Promise<string> {
const totp = new TotpGenerator(secret);
const codes = await totp.generateTotp();
return codes[0]!;
}
async function enableTotpForAccount(harness: ApiTestHarness, account: TestAccount, secret: string): Promise<void> {
const code = await generateTotpCode(secret);
const code = totpCodeNow(secret);
await createBuilder(harness, account.token)
.post('/users/@me/mfa/totp/enable')
.body({
@@ -61,29 +40,12 @@ async function enableTotpForAccount(harness: ApiTestHarness, account: TestAccoun
.execute();
}
async function loginWithTotp(harness: ApiTestHarness, account: TestAccount, secret: string): Promise<TestAccount> {
const login = await loginUser(harness, {email: account.email, password: account.password});
if (!('mfa' in login)) {
throw new Error('Expected MFA login');
}
const mfaLoginResp = await createBuilderWithoutAuth<{
token: string;
}>(harness)
.post('/auth/login/mfa/totp')
.body({
ticket: login.ticket,
code: await generateTotpCode(secret),
})
.execute();
return {...account, token: mfaLoginResp.token};
}
async function getSudoTokenViaMfa(harness: ApiTestHarness, token: string, secret: string): Promise<string> {
const {response} = await createBuilder<BackupCodesResponse>(harness, token)
.post('/users/@me/mfa/backup-codes')
.body({
mfa_method: 'totp',
mfa_code: await generateTotpCode(secret),
mfa_code: totpCodeNow(secret),
regenerate: false,
})
.executeWithResponse();
@@ -108,7 +70,7 @@ describe('Sudo mode negative cases', () => {
describe('invalid sudo token rejected', () => {
it('rejects malformed sudo token', async () => {
const account = await createTestAccount(harness);
const secret = generateTotpSecret();
const secret = createTotpSecret();
await enableTotpForAccount(harness, account, secret);
const loggedIn = await loginWithTotp(harness, account, secret);
const invalidTokens = [
@@ -128,7 +90,7 @@ describe('Sudo mode negative cases', () => {
});
it('rejects empty sudo token header', async () => {
const account = await createTestAccount(harness);
const secret = generateTotpSecret();
const secret = createTotpSecret();
await enableTotpForAccount(harness, account, secret);
const loggedIn = await loginWithTotp(harness, account, secret);
const {json: errResp} = await createBuilder<ErrorResponse>(harness, loggedIn.token)
@@ -166,7 +128,7 @@ describe('Sudo mode negative cases', () => {
describe('wrong MFA code rejected', () => {
it('rejects incorrect TOTP codes', async () => {
const account = await createTestAccount(harness);
const secret = generateTotpSecret();
const secret = createTotpSecret();
await enableTotpForAccount(harness, account, secret);
const loggedIn = await loginWithTotp(harness, account, secret);
const wrongCodes = ['000000', '123456', '999999', '12345', '1234567', 'abcdef'];
@@ -185,12 +147,12 @@ describe('Sudo mode negative cases', () => {
describe('sudo token for wrong user rejected', () => {
it('rejects sudo token from different user', async () => {
const account1 = await createTestAccount(harness);
const secret1 = generateTotpSecret();
const secret1 = createTotpSecret();
await enableTotpForAccount(harness, account1, secret1);
const loggedIn1 = await loginWithTotp(harness, account1, secret1);
const user1SudoToken = await getSudoTokenViaMfa(harness, loggedIn1.token, secret1);
const account2 = await createTestAccount(harness);
const secret2 = generateTotpSecret();
const secret2 = createTotpSecret();
await enableTotpForAccount(harness, account2, secret2);
const loggedIn2 = await loginWithTotp(harness, account2, secret2);
const {json: errResp} = await createBuilder<ErrorResponse>(harness, loggedIn2.token)
@@ -243,7 +205,7 @@ describe('Sudo mode negative cases', () => {
describe('MFA registration without token fails', () => {
it('WebAuthn registration options do not issue sudo token', async () => {
const account = await createTestAccount(harness);
const secret = generateTotpSecret();
const secret = createTotpSecret();
await enableTotpForAccount(harness, account, secret);
const loggedIn = await loginWithTotp(harness, account, secret);
const sudoToken = await getSudoTokenViaMfa(harness, loggedIn.token, secret);
@@ -259,7 +221,7 @@ describe('Sudo mode negative cases', () => {
describe('existing MFA token allows skipping MFA', () => {
it('sudo token from MFA allows skipping MFA verification on sensitive endpoint', async () => {
const account = await createTestAccount(harness);
const secret = generateTotpSecret();
const secret = createTotpSecret();
await enableTotpForAccount(harness, account, secret);
const loggedIn = await loginWithTotp(harness, account, secret);
const sudoToken = await getSudoTokenViaMfa(harness, loggedIn.token, secret);
@@ -1,17 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {
createHash,
createHmac,
createPrivateKey,
createPublicKey,
createSign,
generateKeyPairSync,
randomBytes,
} from 'node:crypto';
import {createHash, createPrivateKey, createPublicKey, createSign, generateKeyPairSync, randomBytes} from 'node:crypto';
import type {ApiTestHarness} from '@app/api/test/ApiTestHarness';
import {createBuilder, createBuilderWithoutAuth} from '@app/api/test/TestRequestBuilder';
import {decode as base32Decode, encode as base32Encode} from 'hi-base32';
export {createTotpSecret, generateTotpCode} from '@app/api/auth/tests/AuthTestUtils';
export interface WebAuthnDevice {
privateKey: string;
@@ -103,30 +96,6 @@ interface WebAuthnAuthenticationResponse {
response: AuthenticatorAssertionResponse;
}
export function createTotpSecret(): string {
const buf = randomBytes(20);
return base32Encode(buf).replace(/=/g, '');
}
export function generateTotpCode(secret: string, time = Date.now()): string {
const key = Buffer.from(base32Decode.asBytes(secret.toUpperCase()));
const epoch = Math.floor(time / 1000);
const counter = Math.floor(epoch / 30);
const counterBuf = Buffer.alloc(8);
counterBuf.writeBigUInt64BE(BigInt(counter));
const hmac = createHmac('sha1', key);
hmac.update(counterBuf);
const hash = hmac.digest();
const offset = hash[hash.length - 1] & 0x0f;
const binary =
((hash[offset] & 0x7f) << 24) |
((hash[offset + 1] & 0xff) << 16) |
((hash[offset + 2] & 0xff) << 8) |
(hash[offset + 3] & 0xff);
const otp = binary % 1000000;
return otp.toString().padStart(6, '0');
}
function resolveWebAuthnOrigin(): {
rpId: string;
origin: string;