9d3d431416
- Added account types and privileged account types constants. - Created admin user controller for updating user security fields. - Developed role assignment controller for managing user roles. - Implemented validation middleware for request schemas. - Defined user role and auth session models for database interactions. - Created services for authentication, email notifications, and OTP handling. - Developed OAuth service for Google and Apple authentication. - Added JWT utility functions for token generation and verification. - Implemented comprehensive tests for authentication, session management, and password policies. - Created migration for updating user schema and adding new tables for auth sessions and user identities.
30 lines
1.7 KiB
JavaScript
30 lines
1.7 KiB
JavaScript
const crypto = require("crypto");
|
|
const redis = require("../../config/redisClient");
|
|
|
|
const otpHash = (challengeId, otp) => crypto.createHmac("sha256", process.env.JWT_SECRET).update(`${challengeId}:${otp}`).digest("hex");
|
|
const generateOtp = () => crypto.randomInt(0, 1000000).toString().padStart(6, "0");
|
|
|
|
const createLoginChallenge = async ({ userId, rememberMe, context }) => {
|
|
const challengeId = crypto.randomUUID();
|
|
const otp = generateOtp();
|
|
await redis.set(`login:${challengeId}`, JSON.stringify({ userId, otpHash: otpHash(challengeId, otp), attempts: 0, rememberMe, context }), "EX", Number(process.env.LOGIN_OTP_TTL_SECONDS || 900));
|
|
return { challengeId, otp };
|
|
};
|
|
|
|
const VERIFY_SCRIPT = `
|
|
local raw=redis.call('GET',KEYS[1]); if not raw then return {-3} end
|
|
local value=cjson.decode(raw)
|
|
if value.otpHash==ARGV[1] then redis.call('DEL',KEYS[1]); return {1,value.userId,cjson.encode(value)} end
|
|
value.attempts=(value.attempts or 0)+1
|
|
if value.attempts>=tonumber(ARGV[2]) then redis.call('DEL',KEYS[1]); return {-2} end
|
|
redis.call('SET',KEYS[1],cjson.encode(value),'KEEPTTL'); return {-1}
|
|
`;
|
|
const verifyLoginChallenge = async (challengeId, otp) => {
|
|
const result = await redis.eval(VERIFY_SCRIPT, 1, `login:${challengeId}`, otpHash(challengeId, otp), Number(process.env.LOGIN_OTP_MAX_ATTEMPTS || 5));
|
|
if (Number(result[0]) !== 1) throw Object.assign(new Error("Invalid or expired challenge"), { code: "INVALID_OTP", status: 401 });
|
|
const stored = JSON.parse(result[2]);
|
|
return { userId: result[1], rememberMe: Boolean(stored.rememberMe), context: stored.context || {} };
|
|
};
|
|
|
|
module.exports = { generateOtp, otpHash, createLoginChallenge, verifyLoginChallenge };
|