godcrm/backend/services/verification/methods/totp.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

62 lines
2 KiB
JavaScript

// ADR-0011 · Phase C · TOTP method plugin.
//
// Per-user TOTP: reuses `users.totp_secret` / `users.totp_enabled` (set via
// POST /api/v2/auth/2fa/setup + /verify). The verification column does NOT
// provision its own secrets — the user enrolls once in account settings and
// the same authenticator covers every verification call.
//
// If the user has no TOTP enrolled → 412 `totp_not_enrolled` (surfaced as
// method-level failure, verify endpoint aggregates into 401 overall).
import speakeasy from 'speakeasy';
import { dbGet } from '../../../database/connection.js';
import { hashCode } from './hash.js';
export const totpMethod = {
name: 'totp',
/**
* @param {object} args
* @param {{userId:number, rowId:number|string, columnId:number|string}} args.context
* @param {{code?:string}} args.submission
* @returns {Promise<{ok:true, at:string, code_hash:string} | {ok:false, code:string, message:string}>}
*/
async verify({ context, submission }) {
const code = submission?.code;
if (!code || typeof code !== 'string') {
return { ok: false, code: 'totp_code_missing', message: 'TOTP code is required' };
}
const user = await dbGet(
`SELECT totp_secret, totp_enabled FROM users WHERE id = ?`,
[context.userId]
);
if (!user) {
return { ok: false, code: 'user_not_found', message: 'User not found' };
}
if (!user.totp_enabled || !user.totp_secret) {
return {
ok: false,
code: 'totp_not_enrolled',
message: 'TOTP is not enabled on this account — enroll via /2fa/setup first',
};
}
const ok = speakeasy.totp.verify({
secret: user.totp_secret,
encoding: 'base32',
token: String(code).trim(),
window: 1,
});
if (!ok) {
return { ok: false, code: 'totp_invalid', message: 'Invalid TOTP code' };
}
const at = new Date().toISOString();
return {
ok: true,
at,
code_hash: hashCode(code, context.rowId, at),
};
},
};