Quik Framework :: MFA
- Codename: Berlin
- Version: 0.2.0-beta.76
- License: Check license here
@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
QMFAServiceinServicesStore. - 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.enabledfromMFA_ENABLED.mfa.challenge.timeToLiveMsfromMFA_CHALLENGE_TIME_TO_LIVE_MSdefault300000.mfa.challenge.maxAttemptsfromMFA_CHALLENGE_MAX_ATTEMPTSdefault5.mfa.code.lengthfromMFA_CODE_LENGTHdefault6.mfa.email.subjectfromMFA_EMAIL_SUBJECTdefaultYour authentication code.mfa.totp.digitsfromMFA_TOTP_DIGITSdefault6.mfa.totp.stepSecondsfromMFA_TOTP_STEP_SECONDSdefault30.mfa.totp.windowfromMFA_TOTP_WINDOWdefault1.mfa.totp.issuerfromMFA_TOTP_ISSUERdefaultQuik.mfa.totp.qrCode.sizePixelsfromMFA_TOTP_QR_CODE_SIZE_PIXELSdefault256.mfa.totp.qrCode.quietZoneModulesfromMFA_TOTP_QR_CODE_QUIET_ZONE_MODULESdefault4.
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.