Files
documenso/packages/lib/utils/two-factor.test.ts
T
2026-09-14 14:16:23 +10:00

719 lines
23 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
calculateTwoFactorDeadline,
calculateTwoFactorDeadlineTimerDelay,
computeOrganisationTwoFactorEnforcementStatus,
computeTwoFactorEnforcementStatus,
evaluateInstanceTwoFactorEnforcementUpdate,
isInstanceTwoFactorEnforcementActive,
isTwoFactorGracePeriodReduction,
MAX_TWO_FACTOR_DEADLINE_TIMER_DELAY_MS,
} from './two-factor';
describe('calculateTwoFactorDeadline', () => {
it('adds the grace period to a single anchor', () => {
const anchor = new Date('2026-01-01T00:00:00.000Z');
const deadline = calculateTwoFactorDeadline({ anchors: [anchor], gracePeriodDays: 7 });
expect(deadline).toEqual(new Date('2026-01-08T00:00:00.000Z'));
});
it('uses the latest anchor when multiple are provided', () => {
const earlier = new Date('2026-01-01T00:00:00.000Z');
const latest = new Date('2026-02-01T00:00:00.000Z');
const deadline = calculateTwoFactorDeadline({
anchors: [earlier, latest],
gracePeriodDays: 1,
});
expect(deadline).toEqual(new Date('2026-02-02T00:00:00.000Z'));
});
it('ignores null and undefined anchors', () => {
const anchor = new Date('2026-01-01T00:00:00.000Z');
const deadline = calculateTwoFactorDeadline({
anchors: [null, anchor, undefined],
gracePeriodDays: 0,
});
expect(deadline).toEqual(anchor);
});
it('returns the anchor instant for a 0 day grace period', () => {
const anchor = new Date('2026-01-01T12:34:56.000Z');
const deadline = calculateTwoFactorDeadline({ anchors: [anchor], gracePeriodDays: 0 });
expect(deadline).toEqual(anchor);
});
it('returns null when no anchors are provided', () => {
expect(calculateTwoFactorDeadline({ anchors: [], gracePeriodDays: 7 })).toBeNull();
expect(calculateTwoFactorDeadline({ anchors: [null, undefined], gracePeriodDays: 7 })).toBeNull();
});
});
describe('isTwoFactorGracePeriodReduction', () => {
const anchor = new Date('2026-01-01T00:00:00.000Z');
const now = new Date('2026-01-02T00:00:00.000Z');
it('detects a reduced grace period while the previous grace is active', () => {
expect(
isTwoFactorGracePeriodReduction({
previous: { anchors: [anchor], gracePeriodDays: 30 },
next: { anchors: [anchor], gracePeriodDays: 7 },
now,
}),
).toBe(true);
});
it('detects a reduction caused by an earlier anchor set', () => {
const laterEnforcedFrom = new Date('2026-01-10T00:00:00.000Z');
expect(
isTwoFactorGracePeriodReduction({
previous: { anchors: [anchor, laterEnforcedFrom], gracePeriodDays: 7 },
next: { anchors: [anchor], gracePeriodDays: 7 },
now,
}),
).toBe(true);
});
it('is not a reduction when the deadline stays the same', () => {
expect(
isTwoFactorGracePeriodReduction({
previous: { anchors: [anchor], gracePeriodDays: 7 },
next: { anchors: [anchor], gracePeriodDays: 7 },
now,
}),
).toBe(false);
});
it('is not a reduction when the deadline moves later', () => {
expect(
isTwoFactorGracePeriodReduction({
previous: { anchors: [anchor], gracePeriodDays: 7 },
next: { anchors: [anchor], gracePeriodDays: 30 },
now,
}),
).toBe(false);
});
it('is not a reduction when the previous grace has already expired', () => {
const nowAfterExpiry = new Date('2026-03-01T00:00:00.000Z');
expect(
isTwoFactorGracePeriodReduction({
previous: { anchors: [anchor], gracePeriodDays: 7 },
next: { anchors: [anchor], gracePeriodDays: 0 },
now: nowAfterExpiry,
}),
).toBe(false);
});
it('is not a reduction when enforcement was not previously configured', () => {
expect(
isTwoFactorGracePeriodReduction({
previous: null,
next: { anchors: [anchor], gracePeriodDays: 0 },
now,
}),
).toBe(false);
});
it('is not a reduction when the update disables enforcement', () => {
expect(
isTwoFactorGracePeriodReduction({
previous: { anchors: [anchor], gracePeriodDays: 7 },
next: null,
now,
}),
).toBe(false);
});
});
describe('computeTwoFactorEnforcementStatus', () => {
const anchor = new Date('2026-01-01T00:00:00.000Z');
const deadline = new Date('2026-01-08T00:00:00.000Z');
const graceWindow = { anchors: [anchor], gracePeriodDays: 7 };
it('is not required when no grace window is configured', () => {
expect(
computeTwoFactorEnforcementStatus({
graceWindow: null,
userTwoFactorEnabled: false,
sessionTwoFactorVerified: false,
now: new Date('2026-01-01T00:00:00.000Z'),
}),
).toEqual({ required: false });
});
it('is not required when the grace window has no anchors', () => {
expect(
computeTwoFactorEnforcementStatus({
graceWindow: { anchors: [null, undefined], gracePeriodDays: 7 },
userTwoFactorEnabled: false,
sessionTwoFactorVerified: false,
now: new Date('2026-01-01T00:00:00.000Z'),
}),
).toEqual({ required: false });
});
it('is required but not blocked within grace while unsatisfied', () => {
expect(
computeTwoFactorEnforcementStatus({
graceWindow,
userTwoFactorEnabled: false,
sessionTwoFactorVerified: false,
now: new Date('2026-01-02T00:00:00.000Z'),
}),
).toEqual({
required: true,
deadline,
isDeadlineExpired: false,
isSatisfied: false,
isBlocked: false,
});
});
it('is required and satisfied within grace when enrolled and verified', () => {
expect(
computeTwoFactorEnforcementStatus({
graceWindow,
userTwoFactorEnabled: true,
sessionTwoFactorVerified: true,
now: new Date('2026-01-02T00:00:00.000Z'),
}),
).toEqual({
required: true,
deadline,
isDeadlineExpired: false,
isSatisfied: true,
isBlocked: false,
});
});
it('blocks after the deadline while unsatisfied', () => {
expect(
computeTwoFactorEnforcementStatus({
graceWindow,
userTwoFactorEnabled: false,
sessionTwoFactorVerified: false,
now: new Date('2026-02-01T00:00:00.000Z'),
}),
).toEqual({
required: true,
deadline,
isDeadlineExpired: true,
isSatisfied: false,
isBlocked: true,
});
});
it('does not block after the deadline when satisfied', () => {
expect(
computeTwoFactorEnforcementStatus({
graceWindow,
userTwoFactorEnabled: true,
sessionTwoFactorVerified: true,
now: new Date('2026-02-01T00:00:00.000Z'),
}),
).toEqual({
required: true,
deadline,
isDeadlineExpired: true,
isSatisfied: true,
isBlocked: false,
});
});
it('does not block when enrolled but the session is unverified within grace', () => {
const status = computeTwoFactorEnforcementStatus({
graceWindow,
userTwoFactorEnabled: true,
sessionTwoFactorVerified: false,
now: new Date('2026-01-02T00:00:00.000Z'),
});
expect(status).toMatchObject({ required: true, isSatisfied: false, isBlocked: false });
});
it('blocks an enrolled user with an unverified session after the deadline', () => {
const status = computeTwoFactorEnforcementStatus({
graceWindow,
userTwoFactorEnabled: true,
sessionTwoFactorVerified: false,
now: new Date('2026-02-01T00:00:00.000Z'),
});
expect(status).toMatchObject({ required: true, isSatisfied: false, isBlocked: true });
});
it('counts the deadline instant itself as expired', () => {
const status = computeTwoFactorEnforcementStatus({
graceWindow,
userTwoFactorEnabled: false,
sessionTwoFactorVerified: false,
now: deadline,
});
expect(status).toMatchObject({ required: true, isDeadlineExpired: true, isBlocked: true });
});
it('is not expired just before the deadline instant', () => {
const status = computeTwoFactorEnforcementStatus({
graceWindow,
userTwoFactorEnabled: false,
sessionTwoFactorVerified: false,
now: new Date(deadline.getTime() - 1),
});
expect(status).toMatchObject({ required: true, isDeadlineExpired: false, isBlocked: false });
});
it('uses the latest anchor for the deadline', () => {
const laterEnforcedFrom = new Date('2026-01-10T00:00:00.000Z');
const status = computeTwoFactorEnforcementStatus({
graceWindow: { anchors: [anchor, laterEnforcedFrom], gracePeriodDays: 7 },
userTwoFactorEnabled: false,
sessionTwoFactorVerified: false,
now: new Date('2026-01-09T00:00:00.000Z'),
});
expect(status).toMatchObject({
required: true,
deadline: new Date('2026-01-17T00:00:00.000Z'),
isDeadlineExpired: false,
});
});
it('blocks immediately with a 0 day grace period', () => {
const status = computeTwoFactorEnforcementStatus({
graceWindow: { anchors: [anchor], gracePeriodDays: 0 },
userTwoFactorEnabled: false,
sessionTwoFactorVerified: false,
now: anchor,
});
expect(status).toMatchObject({ required: true, deadline: anchor, isBlocked: true });
});
});
describe('isInstanceTwoFactorEnforcementActive', () => {
it('is active when the setting exists, is enabled and the license grants the flag', () => {
expect(isInstanceTwoFactorEnforcementActive({ setting: { enabled: true }, isLicensed: true })).toBe(true);
});
it('is inactive when the setting row is missing', () => {
expect(isInstanceTwoFactorEnforcementActive({ setting: null, isLicensed: true })).toBe(false);
});
it('is inactive when the setting is disabled', () => {
expect(isInstanceTwoFactorEnforcementActive({ setting: { enabled: false }, isLicensed: true })).toBe(false);
});
it('is inactive on an unlicensed instance even when a row is enabled', () => {
expect(isInstanceTwoFactorEnforcementActive({ setting: { enabled: true }, isLicensed: false })).toBe(false);
});
});
describe('computeOrganisationTwoFactorEnforcementStatus', () => {
const memberCreatedAt = new Date('2026-01-01T00:00:00.000Z');
const graceStartedAt = new Date('2025-12-01T00:00:00.000Z');
const baseOptions = {
memberCreatedAt,
userTwoFactorEnabled: false,
userTwoFactorGraceStartedAt: graceStartedAt,
sessionTwoFactorVerified: false,
};
it('is not required when the organisation does not require 2FA', () => {
const status = computeOrganisationTwoFactorEnforcementStatus({
...baseOptions,
organisationSettings: {
twoFactorRequired: false,
twoFactorGracePeriodDays: 7,
twoFactorEnforcedFrom: new Date('2026-01-05T00:00:00.000Z'),
},
now: new Date('2026-12-01T00:00:00.000Z'),
});
expect(status).toEqual({ required: false });
});
it('derives the deadline from max(member.createdAt, enforcedFrom, graceStartedAt) + gracePeriodDays', () => {
const enforcedFrom = new Date('2026-01-10T00:00:00.000Z');
const status = computeOrganisationTwoFactorEnforcementStatus({
...baseOptions,
organisationSettings: {
twoFactorRequired: true,
twoFactorGracePeriodDays: 7,
twoFactorEnforcedFrom: enforcedFrom,
},
now: new Date('2026-01-12T00:00:00.000Z'),
});
expect(status).toMatchObject({
required: true,
deadline: new Date('2026-01-17T00:00:00.000Z'),
isDeadlineExpired: false,
isBlocked: false,
});
});
it('anchors on member.createdAt when enforcedFrom is null and grace started earlier', () => {
const status = computeOrganisationTwoFactorEnforcementStatus({
...baseOptions,
organisationSettings: {
twoFactorRequired: true,
twoFactorGracePeriodDays: 7,
twoFactorEnforcedFrom: null,
},
now: new Date('2026-01-08T00:00:00.000Z'),
});
expect(status).toMatchObject({
required: true,
deadline: new Date('2026-01-08T00:00:00.000Z'),
isDeadlineExpired: true,
isBlocked: true,
});
});
it('an admin 2FA reset (later graceStartedAt) restarts the organisation grace window', () => {
const restartedGraceStartedAt = new Date('2026-02-01T00:00:00.000Z');
const status = computeOrganisationTwoFactorEnforcementStatus({
...baseOptions,
userTwoFactorGraceStartedAt: restartedGraceStartedAt,
organisationSettings: {
twoFactorRequired: true,
twoFactorGracePeriodDays: 7,
twoFactorEnforcedFrom: null,
},
now: new Date('2026-02-02T00:00:00.000Z'),
});
expect(status).toMatchObject({
required: true,
deadline: new Date('2026-02-08T00:00:00.000Z'),
isDeadlineExpired: false,
isBlocked: false,
});
});
it('is satisfied (never blocked) for an enrolled user with a verified session', () => {
const status = computeOrganisationTwoFactorEnforcementStatus({
...baseOptions,
userTwoFactorEnabled: true,
sessionTwoFactorVerified: true,
organisationSettings: {
twoFactorRequired: true,
twoFactorGracePeriodDays: 0,
twoFactorEnforcedFrom: null,
},
now: new Date('2027-01-01T00:00:00.000Z'),
});
expect(status).toMatchObject({
required: true,
isSatisfied: true,
isDeadlineExpired: true,
isBlocked: false,
});
});
it('an enrolled user with an unverified session (null/API context) is not satisfied', () => {
const status = computeOrganisationTwoFactorEnforcementStatus({
...baseOptions,
userTwoFactorEnabled: true,
sessionTwoFactorVerified: false,
organisationSettings: {
twoFactorRequired: true,
twoFactorGracePeriodDays: 0,
twoFactorEnforcedFrom: null,
},
now: new Date('2027-01-01T00:00:00.000Z'),
});
expect(status).toMatchObject({
required: true,
isSatisfied: false,
isBlocked: true,
});
});
it('blocks organisation access immediately with a 0 day grace period', () => {
const status = computeOrganisationTwoFactorEnforcementStatus({
...baseOptions,
organisationSettings: {
twoFactorRequired: true,
twoFactorGracePeriodDays: 0,
twoFactorEnforcedFrom: null,
},
now: memberCreatedAt,
});
expect(status).toMatchObject({ required: true, deadline: memberCreatedAt, isBlocked: true });
});
});
describe('evaluateInstanceTwoFactorEnforcementUpdate', () => {
const now = new Date('2026-06-01T00:00:00.000Z');
const satisfiedActor = { userTwoFactorEnabled: true, sessionTwoFactorVerified: true };
const unsatisfiedActor = { userTwoFactorEnabled: false, sessionTwoFactorVerified: false };
const storedDisabled = { enabled: false, gracePeriodDays: 7, enforcedFrom: null };
// Enabled with an active grace window: enforcedFrom 1 day ago + 30 days.
const storedEnabledActiveGrace = {
enabled: true,
gracePeriodDays: 30,
enforcedFrom: '2026-05-31T00:00:00.000Z',
};
describe('license gate', () => {
it('rejects enabling on an unlicensed instance', () => {
const decision = evaluateInstanceTwoFactorEnforcementUpdate({
stored: storedDisabled,
update: { enabled: true, gracePeriodDays: 7 },
isLicensed: false,
actor: satisfiedActor,
now,
});
expect(decision).toEqual({ allowed: false, reason: 'UNLICENSED' });
});
it('rejects changing values while enabled on an unlicensed instance', () => {
const decision = evaluateInstanceTwoFactorEnforcementUpdate({
stored: storedEnabledActiveGrace,
update: { enabled: true, gracePeriodDays: 60 },
isLicensed: false,
actor: satisfiedActor,
now,
});
expect(decision).toEqual({ allowed: false, reason: 'UNLICENSED' });
});
it('rejects an unlicensed disabled→disabled no-op write', () => {
const decision = evaluateInstanceTwoFactorEnforcementUpdate({
stored: storedDisabled,
update: { enabled: false, gracePeriodDays: 7 },
isLicensed: false,
actor: satisfiedActor,
now,
});
expect(decision).toEqual({ allowed: false, reason: 'UNLICENSED' });
});
it('rejects an unlicensed disable that also changes the grace period', () => {
const decision = evaluateInstanceTwoFactorEnforcementUpdate({
stored: storedEnabledActiveGrace,
update: { enabled: false, gracePeriodDays: 60 },
isLicensed: false,
actor: satisfiedActor,
now,
});
expect(decision).toEqual({ allowed: false, reason: 'UNLICENSED' });
});
it('allows an unlicensed disable-only update with values unchanged from stored', () => {
const decision = evaluateInstanceTwoFactorEnforcementUpdate({
stored: storedEnabledActiveGrace,
update: { enabled: false, gracePeriodDays: storedEnabledActiveGrace.gracePeriodDays },
isLicensed: false,
// Even an unenrolled admin may disable — walking the policy back must
// never be gated on satisfying it.
actor: unsatisfiedActor,
now,
});
expect(decision).toEqual({
allowed: true,
next: {
enabled: false,
gracePeriodDays: storedEnabledActiveGrace.gracePeriodDays,
enforcedFrom: storedEnabledActiveGrace.enforcedFrom,
},
});
});
});
describe('enable-time guard', () => {
it('rejects enabling when the acting admin does not satisfy the policy', () => {
const decision = evaluateInstanceTwoFactorEnforcementUpdate({
stored: storedDisabled,
update: { enabled: true, gracePeriodDays: 0 },
isLicensed: true,
actor: unsatisfiedActor,
now,
});
expect(decision).toEqual({ allowed: false, reason: 'ENABLE_REQUIRES_ACTOR_TWO_FACTOR' });
});
it('rejects enabling when the actor is enrolled but the session is unverified', () => {
const decision = evaluateInstanceTwoFactorEnforcementUpdate({
stored: storedDisabled,
update: { enabled: true, gracePeriodDays: 7 },
isLicensed: true,
actor: { userTwoFactorEnabled: true, sessionTwoFactorVerified: false },
now,
});
expect(decision).toEqual({ allowed: false, reason: 'ENABLE_REQUIRES_ACTOR_TWO_FACTOR' });
});
it('does not apply the guard when updating values while already enabled', () => {
const decision = evaluateInstanceTwoFactorEnforcementUpdate({
stored: storedEnabledActiveGrace,
update: { enabled: true, gracePeriodDays: 60 },
isLicensed: true,
actor: unsatisfiedActor,
now,
});
expect(decision).toMatchObject({ allowed: true });
});
});
describe('enforcedFrom handling', () => {
it('sets enforcedFrom to now on an off→on transition', () => {
const decision = evaluateInstanceTwoFactorEnforcementUpdate({
stored: storedDisabled,
update: { enabled: true, gracePeriodDays: 14 },
isLicensed: true,
actor: satisfiedActor,
now,
});
expect(decision).toEqual({
allowed: true,
next: { enabled: true, gracePeriodDays: 14, enforcedFrom: now.toISOString() },
});
});
it('preserves enforcedFrom when the policy stays enabled', () => {
const decision = evaluateInstanceTwoFactorEnforcementUpdate({
stored: storedEnabledActiveGrace,
update: { enabled: true, gracePeriodDays: 60 },
isLicensed: true,
actor: satisfiedActor,
now,
});
expect(decision).toEqual({
allowed: true,
next: { enabled: true, gracePeriodDays: 60, enforcedFrom: storedEnabledActiveGrace.enforcedFrom },
});
});
it('preserves enforcedFrom on disable', () => {
const decision = evaluateInstanceTwoFactorEnforcementUpdate({
stored: storedEnabledActiveGrace,
update: { enabled: false, gracePeriodDays: storedEnabledActiveGrace.gracePeriodDays },
isLicensed: true,
actor: satisfiedActor,
now,
});
expect(decision).toEqual({
allowed: true,
next: {
enabled: false,
gracePeriodDays: storedEnabledActiveGrace.gracePeriodDays,
enforcedFrom: storedEnabledActiveGrace.enforcedFrom,
},
});
});
});
describe('grace-reduction acknowledgement', () => {
it('rejects reducing an active grace period without acknowledgement', () => {
const decision = evaluateInstanceTwoFactorEnforcementUpdate({
stored: storedEnabledActiveGrace,
update: { enabled: true, gracePeriodDays: 1 },
isLicensed: true,
actor: satisfiedActor,
now,
});
expect(decision).toEqual({ allowed: false, reason: 'GRACE_REDUCTION_NOT_ACKNOWLEDGED' });
});
it('allows reducing an active grace period with acknowledgement', () => {
const decision = evaluateInstanceTwoFactorEnforcementUpdate({
stored: storedEnabledActiveGrace,
update: { enabled: true, gracePeriodDays: 1, acknowledgeGracePeriodReduction: true },
isLicensed: true,
actor: satisfiedActor,
now,
});
expect(decision).toEqual({
allowed: true,
next: { enabled: true, gracePeriodDays: 1, enforcedFrom: storedEnabledActiveGrace.enforcedFrom },
});
});
it('does not require acknowledgement when enabling for the first time', () => {
const decision = evaluateInstanceTwoFactorEnforcementUpdate({
stored: storedDisabled,
update: { enabled: true, gracePeriodDays: 0 },
isLicensed: true,
actor: satisfiedActor,
now,
});
expect(decision).toMatchObject({ allowed: true });
});
it('does not require acknowledgement when tightening an already-expired window', () => {
const decision = evaluateInstanceTwoFactorEnforcementUpdate({
stored: {
enabled: true,
gracePeriodDays: 7,
enforcedFrom: '2026-01-01T00:00:00.000Z',
},
update: { enabled: true, gracePeriodDays: 0 },
isLicensed: true,
actor: satisfiedActor,
now,
});
expect(decision).toMatchObject({ allowed: true });
});
});
});
describe('calculateTwoFactorDeadlineTimerDelay', () => {
const now = new Date('2026-06-01T00:00:00.000Z');
it('returns 0 for a past deadline', () => {
const deadline = new Date(now.getTime() - 60_000);
expect(calculateTwoFactorDeadlineTimerDelay({ deadline, now })).toBe(0);
});
it('returns the delay at exactly the setTimeout maximum and null just past it', () => {
const atMax = new Date(now.getTime() + MAX_TWO_FACTOR_DEADLINE_TIMER_DELAY_MS);
const pastMax = new Date(now.getTime() + MAX_TWO_FACTOR_DEADLINE_TIMER_DELAY_MS + 1);
expect(calculateTwoFactorDeadlineTimerDelay({ deadline: atMax, now })).toBe(MAX_TWO_FACTOR_DEADLINE_TIMER_DELAY_MS);
expect(calculateTwoFactorDeadlineTimerDelay({ deadline: pastMax, now })).toBeNull();
});
});