feat: implement identity and security features
- 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.
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
const crypto = require("crypto");
|
||||
const db = require("../../models");
|
||||
|
||||
const digest = (token) => crypto.createHash("sha256").update(token).digest("hex");
|
||||
const safeEqual = (left, right) => left?.length === right?.length && crypto.timingSafeEqual(Buffer.from(left), Buffer.from(right));
|
||||
const rawToken = (id) => `${id}.${crypto.randomBytes(48).toString("base64url")}`;
|
||||
const tokenId = (token) => typeof token === "string" ? token.split(".", 1)[0] : null;
|
||||
const ttlDays = (rememberMe) => Number(process.env[rememberMe ? "REMEMBER_ME_REFRESH_TOKEN_TTL_DAYS" : "REFRESH_TOKEN_TTL_DAYS"] || (rememberMe ? 30 : 7));
|
||||
|
||||
const createSession = async ({ user, rememberMe = false, deviceName, userAgent, ipAddress, familyId, transaction }) => {
|
||||
const id = crypto.randomUUID();
|
||||
const refreshToken = rawToken(id);
|
||||
const expiresAt = new Date(Date.now() + ttlDays(rememberMe) * 86400000);
|
||||
const session = await db.AuthSession.create({
|
||||
id, user_id: user.id, token_family_id: familyId || crypto.randomUUID(), refresh_token_hash: digest(refreshToken),
|
||||
device_name: deviceName, user_agent: userAgent, ip_address: ipAddress, remember_me: rememberMe,
|
||||
token_version: user.tokenVersion, last_used_at: new Date(), expires_at: expiresAt,
|
||||
}, { transaction });
|
||||
return { session, refreshToken, expiresAt };
|
||||
};
|
||||
|
||||
const revokeFamily = async (familyId, reason, transaction) => db.AuthSession.update(
|
||||
{ revoked_at: new Date(), revoked_reason: reason },
|
||||
{ where: { token_family_id: familyId, revoked_at: null }, transaction },
|
||||
);
|
||||
|
||||
const rotateSession = async (token, context = {}) => db.sequelize.transaction(async (transaction) => {
|
||||
const id = tokenId(token);
|
||||
if (!id) throw Object.assign(new Error("Invalid session"), { code: "INVALID_SESSION", status: 401 });
|
||||
const session = await db.AuthSession.findByPk(id, { transaction, lock: transaction.LOCK.UPDATE });
|
||||
if (!session) throw Object.assign(new Error("Invalid session"), { code: "INVALID_SESSION", status: 401 });
|
||||
if (!safeEqual(digest(token), session.refresh_token_hash)) throw Object.assign(new Error("Invalid session"), { code: "INVALID_SESSION", status: 401 });
|
||||
if (session.revoked_at) {
|
||||
if (session.revoked_reason === "ROTATED") await revokeFamily(session.token_family_id, "REFRESH_TOKEN_REUSE", transaction);
|
||||
throw Object.assign(new Error("Invalid session"), { code: session.revoked_reason === "ROTATED" ? "REFRESH_TOKEN_REUSE" : "INVALID_SESSION", status: 401 });
|
||||
}
|
||||
if (session.expires_at <= new Date()) {
|
||||
session.revoked_at = new Date(); session.revoked_reason = "EXPIRED"; await session.save({ transaction });
|
||||
throw Object.assign(new Error("Invalid session"), { code: "INVALID_SESSION", status: 401 });
|
||||
}
|
||||
const user = await db.User.findByPk(session.user_id, { transaction, lock: transaction.LOCK.UPDATE });
|
||||
if (!user || user.accountStatus !== "ACTIVE" || user.tokenVersion !== session.token_version) {
|
||||
await revokeFamily(session.token_family_id, "ACCOUNT_OR_TOKEN_VERSION_INVALID", transaction);
|
||||
throw Object.assign(new Error("Invalid session"), { code: "INVALID_SESSION", status: 401 });
|
||||
}
|
||||
const replacement = await createSession({ user, rememberMe: session.remember_me, familyId: session.token_family_id, ...context, transaction });
|
||||
session.revoked_at = new Date(); session.revoked_reason = "ROTATED"; session.replaced_by_session_id = replacement.session.id;
|
||||
session.last_used_at = new Date(); await session.save({ transaction });
|
||||
return { ...replacement, user };
|
||||
});
|
||||
|
||||
const revokeSession = async (id, reason = "LOGOUT", transaction) => db.AuthSession.update(
|
||||
{ revoked_at: new Date(), revoked_reason: reason }, { where: { id, revoked_at: null }, transaction },
|
||||
);
|
||||
const revokeAllUserSessions = async (userId, reason, transaction) => db.AuthSession.update(
|
||||
{ revoked_at: new Date(), revoked_reason: reason }, { where: { user_id: userId, revoked_at: null }, transaction },
|
||||
);
|
||||
|
||||
module.exports = { createSession, rotateSession, revokeSession, revokeFamily, revokeAllUserSessions, hashRefreshToken: digest, tokenId };
|
||||
Reference in New Issue
Block a user