Skip to main content

Quik Framework :: MFA

@quik/mfa provides multi-factor authentication challenge orchestration with pluggable factors and stores.

Installation

pnpm add @quik/mfa

What The Module Does

  • Loads MFA locales.
  • Registers QMFAService in ServicesStore.
  • Registers default delivery factors when missing:
    • email.
    • sms.
    • whatsapp.
  • Exposes TOTP enrollment helpers (secret, otpauth, optional QR URL).
  • Supports policy/risk/step-up hooks through IQMFAPolicyEngine.

Configuration (mfa)

  • mfa.enabled from MFA_ENABLED.
  • mfa.challenge.timeToLiveMs from MFA_CHALLENGE_TIME_TO_LIVE_MS default 300000.
  • mfa.challenge.maxAttempts from MFA_CHALLENGE_MAX_ATTEMPTS default 5.
  • mfa.code.length from MFA_CODE_LENGTH default 6.
  • mfa.email.subject from MFA_EMAIL_SUBJECT default Your authentication code.
  • mfa.totp.digits from MFA_TOTP_DIGITS default 6.
  • mfa.totp.stepSeconds from MFA_TOTP_STEP_SECONDS default 30.
  • mfa.totp.window from MFA_TOTP_WINDOW default 1.
  • mfa.totp.issuer from MFA_TOTP_ISSUER default Quik.
  • mfa.totp.qrCode.sizePixels from MFA_TOTP_QR_CODE_SIZE_PIXELS default 256.
  • mfa.totp.qrCode.quietZoneModules from MFA_TOTP_QR_CODE_QUIET_ZONE_MODULES default 4.

Use The Service

import { ServicesStore } from '@quik/services';
import { QMFAService } from '@quik/mfa';

const mfa = ServicesStore.get(QMFAService);
mfa.assertEnabled();

const challenge = await mfa.requestChallenge({
method: 'email',
userId: 'u1',
metadata: { email: 'user@example.com' }
});

const result = await mfa.verifyChallenge({
challengeId: challenge.id,
code: '123456'
});

TOTP Enrollment With Inline QR

const enrollment = mfa.createTOTPEnrollment({
userId: 'u1',
accountName: 'user@example.com',
includeQrCodeUrl: true
});

console.log(enrollment.secret);
console.log(enrollment.otpauthUrl);
console.log(enrollment.qrCodeUrl); // data:image/svg+xml;base64,...

Policy/Risk Hooks

import { QMFAService, type IQMFAPolicyEngine } from '@quik/mfa';

const policyEngine: IQMFAPolicyEngine = {
resolveRequest: ({ request }) => ({ ...request, maxAttempts: 3 }),
evaluateRisk: ({ challenge }) => (challenge.method === 'sms' ? 80 : 20),
resolveStepUp: ({ riskScore }) => {
if ((riskScore ?? 0) >= 70) {
return { method: 'passkey', reason: 'high-risk' };
}
}
};

const mfa = new QMFAService({ policyEngine });

Register Custom Factors

import { QCodeFactor, registerFactor } from '@quik/mfa';

registerFactor(
new QCodeFactor({
method: 'push',
deliver: async ({ code, userId }) => {
console.log('Send code', code, 'to user', userId);
}
})
);

Register A TOTP Factor

import { QTOTPFactor, registerFactor } from '@quik/mfa';

registerFactor(
new QTOTPFactor({
getSecret: async (userId) => {
return process.env[`TOTP_SECRET_${userId}`];
}
})
);

Store Extensibility

  • Replace challenge persistence with setMFAChallengeStore(store).
  • Read active store with getMFAChallengeStore().
  • Run cleanup with QMFAService.cleanup().
  • Build repository-backed stores by extending QAbstractMFAChallengeStore.
import { QAbstractMFAChallengeStore, type QMFAChallenge } from '@quik/mfa';

class DBChallengeStore extends QAbstractMFAChallengeStore<MyChallengeRepository> {
protected persistChallenge(challenge: QMFAChallenge): QMFAChallenge {
return this.repository.upsert(challenge);
}

protected fetchChallenge(id: string): QMFAChallenge | undefined {
return this.repository.findById(id);
}

protected removeChallenge(id: string): void {
this.repository.deleteById(id);
}

protected clearChallenges(): void {
this.repository.deleteAll();
}

protected removeExpiredOrConsumed(now: number): number {
return this.repository.deleteExpiredOrConsumed(now);
}
}

API Reference

Generated API documentation is available in the mfa API section.