fix(api): let SSO accounts without a password satisfy sudo verification (#1690)

This commit is contained in:
Hampus authored and GitHub committed 2026-08-16 23:35:47 +02:00
1 parent e0bb10d5b7
commit 6c68202222
2 files changed
+65 -2

No files matched your search

@@ -31,6 +31,16 @@ export function userHasMfa(user: {authenticatorTypes?: Set<number> | null}): boo
);
}
export function hasNoVerifiableCredential(
user: {passwordHash: string | null; isBot: boolean},
hasMfa: boolean,
): boolean {
if (user.isBot || hasMfa) {
return false;
}
return user.passwordHash === null;
}
export function deriveSudoMethods(user: {
totpSecret?: string | null;
authenticatorTypes?: Set<number> | null;
@@ -86,8 +96,7 @@ async function verifySudoMode(
const sudoToken = issueSudoToken ? await sudoModeService.generateSudoToken(user.id) : undefined;
return {verified: true, sudoToken, method: 'mfa'};
}
const isUnclaimedAccount = user.isUnclaimedAccount();
if (isUnclaimedAccount && !hasMfa) {
if (hasNoVerifiableCredential(user, hasMfa)) {
return {verified: true, method: 'password'};
}
if (body.password && !hasMfa) {
@@ -0,0 +1,54 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {UserAuthenticatorTypes} from '@fluxer/constants/src/UserConstants';
import {describe, expect, it} from 'vitest';
import {createUserID} from '../../BrandedTypes';
import {EMPTY_USER_ROW, type UserRow} from '../../database/types/UserTypes';
import {User} from '../../models/User';
import {hasNoVerifiableCredential, userHasMfa} from '../services/SudoVerificationService';
function createUser(overrides: Partial<UserRow> = {}): User {
return new User({
...EMPTY_USER_ROW,
user_id: createUserID(1n),
username: 'test_user',
discriminator: 1,
bot: false,
password_hash: 'hash',
traits: new Set<string>(),
...overrides,
});
}
describe('sudo verification credential capability', () => {
it('lets an SSO provisioned account without a password satisfy sudo mode', () => {
const user = createUser({password_hash: null, traits: new Set<string>(['sso'])});
expect(user.isUnclaimedAccount()).toBe(false);
expect(hasNoVerifiableCredential(user, userHasMfa(user))).toBe(true);
});
it('still lets an unclaimed account satisfy sudo mode', () => {
const user = createUser({password_hash: null});
expect(hasNoVerifiableCredential(user, userHasMfa(user))).toBe(true);
});
it('still requires a password from accounts that have one', () => {
const user = createUser({password_hash: 'hash', traits: new Set<string>(['sso'])});
expect(hasNoVerifiableCredential(user, userHasMfa(user))).toBe(false);
});
it('still requires MFA from an SSO account that enrolled a second factor', () => {
const user = createUser({
password_hash: null,
traits: new Set<string>(['sso']),
authenticator_types: new Set<number>([UserAuthenticatorTypes.TOTP]),
});
expect(userHasMfa(user)).toBe(true);
expect(hasNoVerifiableCredential(user, userHasMfa(user))).toBe(false);
});
it('never applies to bots', () => {
const user = createUser({password_hash: null, bot: true});
expect(hasNoVerifiableCredential(user, userHasMfa(user))).toBe(false);
});
});