167 lines
4.1 KiB
TypeScript
167 lines
4.1 KiB
TypeScript
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<string> {
|
|
return bcrypt.hash(password, SALT_ROUNDS);
|
|
}
|
|
|
|
/**
|
|
* Verify a password against a hash
|
|
*/
|
|
export async function verifyPassword(password: string, hash: string): Promise<boolean> {
|
|
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<string> {
|
|
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,
|
|
};
|
|
}
|