feat(api): make tor and breached password lookups opt-in (#2834)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-19 01:22:17 +02:00
1 parent efd677f32b
commit f32356801d
17 files changed
+186 -40

No files matched your search

+4
View File
@@ -5,6 +5,7 @@ import type {ApiContext} from '@app/api/ApiContext';
import * as AuthSession from '@app/api/auth/AuthSession';
import * as AuthUtility from '@app/api/auth/AuthUtility';
import {createMfaTicket, createPasswordResetToken} from '@app/api/BrandedTypes';
import {Config} from '@app/api/Config';
import {Logger} from '@app/api/Logger';
import type {User} from '@app/api/models/User';
import {EXTERNAL_RESPONSE_LIMITS} from '@app/api/utils/ExternalResponseLimits';
@@ -116,6 +117,9 @@ export async function verifyPassword(
}
export async function isPasswordPwned(_ctx: ApiContext, password: string): Promise<boolean> {
if (!Config.breachedPasswordCheck.enabled) {
return false;
}
const hashed = crypto.createHash('sha1').update(password).digest('hex').toUpperCase();
const hashPrefix = hashed.slice(0, 5);
const hashSuffix = hashed.slice(5);
@@ -3,6 +3,7 @@
import crypto from 'node:crypto';
import type {ApiContext} from '@app/api/ApiContext';
import {isPasswordPwned, resetPwnedPasswordCacheForTesting} from '@app/api/auth/AuthPassword';
import {getConfig} from '@app/api/Config';
import {server} from '@app/api/test/msw/server';
import {delay, HttpResponse, http} from 'msw';
import {beforeEach, describe, expect, test} from 'vitest';
@@ -68,6 +69,19 @@ describe('isPasswordPwned', () => {
await expect(isPasswordPwned(ctx, SAFE_PASSWORD_SAME_PREFIX)).resolves.toBe(false);
expect(requestedPrefixes).toHaveLength(1);
});
test('makes no upstream call when the check is switched off', async () => {
const config = getConfig();
const originalEnabled = config.breachedPasswordCheck.enabled;
const requestedPrefixes: Array<string> = [];
server.use(rangeHandler(requestedPrefixes, [suffixOf(PWNED_PASSWORD)]));
try {
config.breachedPasswordCheck.enabled = false;
await expect(isPasswordPwned(ctx, PWNED_PASSWORD)).resolves.toBe(false);
expect(requestedPrefixes).toHaveLength(0);
} finally {
config.breachedPasswordCheck.enabled = originalEnabled;
}
});
test('fails open on a non-OK response', async () => {
server.use(http.get('https://api.pwnedpasswords.com/range/:prefix', () => HttpResponse.text('', {status: 503})));
await expect(isPasswordPwned(ctx, PWNED_PASSWORD)).resolves.toBe(false);