test(mfa): cover session invalidation on admin MFA reset (#1755)

Add seedMfaUserWithToken helper that wraps seedMfaUser and returns a
signed JWT with the current token_version claim, removing the manual
sign-and-read-boilerplate from callers.

Add an integration test in the MFA reset describe block that exercises
the full invalidation cycle: a target user's pre-reset JWT is accepted
before reset and rejected with 401 'Session invalidated' after an admin
resets their MFA. The test also confirms a pre-minted admin JWT survives
the reset unchanged.
This commit is contained in:
Anso
2026-08-02 22:46:16 -04:00
committed by GitHub
parent 41bf075eb0
commit 2ad1212bbb
2 changed files with 46 additions and 0 deletions
@@ -121,3 +121,24 @@ export async function seedMfaUser(
return { userId, secret, backupCodes };
}
/**
* Seed a user with MFA enrolled and return a session JWT signed with the
* user's current token_version so tests can verify that an operation that
* bumps it (MFA reset, password change) invalidates pre-existing sessions.
*/
export async function seedMfaUserWithToken(
username: string,
password: string,
): Promise<{ userId: number; secret: string; backupCodes: string[]; token: string }> {
const { userId, secret, backupCodes } = await seedMfaUser(username, password);
const { DatabaseService } = await import('../../services/DatabaseService');
const jwtLib = (await import('jsonwebtoken')).default;
const user = DatabaseService.getInstance().getUser(userId)!;
const token = jwtLib.sign(
{ username, role: user.role, tv: user.token_version },
TEST_JWT_SECRET,
{ expiresIn: '1m' },
);
return { userId, secret, backupCodes, token };
}
+25
View File
@@ -18,6 +18,7 @@ import {
setupTestDb,
cleanupTestDb,
seedMfaUser,
seedMfaUserWithToken,
TEST_USERNAME,
TEST_JWT_SECRET,
} from './helpers/setupTestDb';
@@ -568,6 +569,30 @@ describe('POST /api/users/:id/mfa/reset', () => {
expect(resetRows).toHaveLength(1);
expect(resetRows[0].summary).toBe(`Reset two-factor authentication: ${userId}`);
});
it('invalidates target pre-reset JWT after admin MFA reset', async () => {
const { userId, token } = await seedMfaUserWithToken('victim4', 'victim4pass123');
const adminJwt = adminToken(); // Pre-minted so we prove the admin session survives the reset.
const stacksWith = (jwt: string) =>
request(app).get('/api/stacks').set('Authorization', `Bearer ${jwt}`);
// Pre-reset JWT is accepted (viewer role grants stack:read).
expect((await stacksWith(token)).status).toBe(200);
// Admin reset bumps the target's token_version, invalidating their sessions.
const reset = await request(app)
.post(`/api/users/${userId}/mfa/reset`)
.set('Authorization', `Bearer ${adminJwt}`);
expect(reset.status).toBe(200);
// The same pre-reset JWT is now rejected.
const after = await stacksWith(token);
expect(after.status).toBe(401);
expect(after.body.error).toContain('Session invalidated');
// Admin session is untouched (only the target's token_version was bumped).
expect((await stacksWith(adminJwt)).status).toBe(200);
});
});
// ─── SSO bypass toggle ────────────────────────────────────────────────────────