import bcrypt from "bcrypt"; import { ConfidentialClientApplication } from "@azure/msal-node"; import { getUserByEmail } from "./db"; import jwt from "jsonwebtoken"; const SALT_ROUNDS = 10; // ============= LOCAL AUTHENTICATION ============= /** * Hash a password using bcrypt */ export async function hashPassword(password: string): Promise { return bcrypt.hash(password, SALT_ROUNDS); } /** * Verify a password against a hash */ export async function verifyPassword(password: string, hash: string): Promise { return bcrypt.compare(password, hash); } /** * Authenticate a user with email and password (local auth) * Returns user and JWT token if successful, null otherwise */ export async function loginLocal(email: string, password: string) { const user = await getUserByEmail(email); if (!user) { return null; } // Check if user is active if (user.isActive === 0) { throw new Error("Account is inactive"); } // Check if user has a password (local auth) if (!user.passwordHash) { throw new Error("This account does not support local authentication"); } // Verify password const isValid = await verifyPassword(password, user.passwordHash); if (!isValid) { return null; } // Generate JWT token const token = generateToken(user); return { user, token }; } // ============= JWT TOKEN GENERATION ============= /** * Generate a JWT token for a user */ export function generateToken(user: { id: number; email: string; role: string }): string { const secret = process.env.JWT_SECRET || "default-secret-change-in-production"; return jwt.sign( { userId: user.id, email: user.email, role: user.role, }, secret, { expiresIn: "7d" } ); } /** * Verify and decode a JWT token */ export function verifyToken(token: string): { userId: number; email: string; role: string } | null { try { const secret = process.env.JWT_SECRET || "default-secret-change-in-production"; const decoded = jwt.verify(token, secret) as { userId: number; email: string; role: string }; return decoded; } catch (error) { return null; } } // ============= AZURE AD AUTHENTICATION ============= let msalClient: ConfidentialClientApplication | null = null; /** * Check if Azure AD is configured */ export function isAzureAdConfigured(): boolean { return !!( process.env.AZURE_AD_TENANT_ID && process.env.AZURE_AD_CLIENT_ID && process.env.AZURE_AD_CLIENT_SECRET ); } /** * Get MSAL client instance (lazy initialization) */ function getMsalClient(): ConfidentialClientApplication { if (!isAzureAdConfigured()) { throw new Error("Azure AD is not configured"); } if (!msalClient) { msalClient = new ConfidentialClientApplication({ auth: { clientId: process.env.AZURE_AD_CLIENT_ID!, authority: `https://login.microsoftonline.com/${process.env.AZURE_AD_TENANT_ID}`, clientSecret: process.env.AZURE_AD_CLIENT_SECRET!, }, }); } return msalClient; } /** * Get Azure AD authorization URL for user login */ export async function getAzureAuthUrl(): Promise { const client = getMsalClient(); const redirectUri = process.env.AZURE_AD_REDIRECT_URI || "http://localhost:3000/api/auth/azure/callback"; const authCodeUrlParameters = { scopes: ["user.read"], redirectUri, }; return client.getAuthCodeUrl(authCodeUrlParameters); } /** * Handle Azure AD callback and exchange code for tokens */ export async function handleAzureCallback(code: string) { const client = getMsalClient(); const redirectUri = process.env.AZURE_AD_REDIRECT_URI || "http://localhost:3000/api/auth/azure/callback"; const tokenRequest = { code, scopes: ["user.read"], redirectUri, }; const response = await client.acquireTokenByCode(tokenRequest); if (!response || !response.account) { throw new Error("Failed to acquire token from Azure AD"); } return { azureAdId: response.account.homeAccountId, email: response.account.username, name: response.account.name || response.account.username, }; }