mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-05 00:18:00 +00:00
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:
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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 ────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user