From 3a902bc6458817b11476ce1856f700941c8a087f Mon Sep 17 00:00:00 2001 From: alphaeusmote <41258468-alphaeusmote@users.noreply.replit.com> Date: Thu, 10 Apr 2025 03:10:49 +0000 Subject: [PATCH] Add LDAP and OIDC authentication, along with configurable user registration and admin user creation. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 9111ef36-26c8-4085-84ca-a35dc1fec1b5 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7083d608-d6d3-4a6a-9a27-6286c5109627/96fcf2d8-8471-4e81-8211-91059abd5e7f.jpg --- package-lock.json | 236 +++++++++++++++++++++++++++++++++++++++++++ package.json | 3 + server/auth.ts | 250 ++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 470 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7516da8..ab6527b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,7 @@ "cmdk": "^1.0.0", "connect-pg-simple": "^10.0.0", "date-fns": "^3.6.0", + "dotenv": "^16.4.7", "drizzle-orm": "^0.39.1", "drizzle-zod": "^0.7.0", "embla-carousel-react": "^8.3.0", @@ -57,7 +58,9 @@ "memorystore": "^1.6.7", "node-fetch": "^3.3.2", "passport": "^0.7.0", + "passport-ldapauth": "^3.0.1", "passport-local": "^1.0.0", + "passport-openidconnect": "^0.1.2", "react": "^18.3.1", "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", @@ -3367,6 +3370,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ldapjs": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@types/ldapjs/-/ldapjs-2.2.5.tgz", + "integrity": "sha512-Lv/nD6QDCmcT+V1vaTRnEKE8UgOilVv5pHcQuzkU1LcRe4mbHHuUo/KHi0LKrpdHhQY8FJzryF38fcVdeUIrzg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -3522,6 +3534,12 @@ "vite": "^4.2.0 || ^5.0.0" } }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -3602,6 +3620,24 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/autoprefixer": { "version": "10.4.20", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", @@ -3640,12 +3676,30 @@ "postcss": "^8.1.0" } }, + "node_modules/backoff": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz", + "integrity": "sha512-wC5ihrnUXmR2douXmXLCe5O3zg3GKIyvRi/hi58a/XyRxVI+3/yM0PYueQOZXPXQ9pxBislYkw+sF9b7C/RuMA==", + "license": "MIT", + "dependencies": { + "precond": "0.2" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -4000,6 +4054,12 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4273,6 +4333,18 @@ "csstype": "^3.0.2" } }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/drizzle-kit": { "version": "0.30.4", "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.30.4.tgz", @@ -5132,6 +5204,15 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, "node_modules/fast-equals": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz", @@ -5708,6 +5789,62 @@ "node": ">=6" } }, + "node_modules/ldap-filter": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.3.3.tgz", + "integrity": "sha512-/tFkx5WIn4HuO+6w9lsfxq4FN3O+fDZeO9Mek8dCD8rTUpqzRa766BOBO7BcGkn3X86m5+cBm1/2S/Shzz7gMg==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/ldapauth-fork": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/ldapauth-fork/-/ldapauth-fork-5.0.5.tgz", + "integrity": "sha512-LWUk76+V4AOZbny/3HIPQtGPWZyA3SW2tRhsWIBi9imP22WJktKLHV1ofd8Jo/wY7Ve6vAT7FCI5mEn3blZTjw==", + "license": "MIT", + "dependencies": { + "@types/ldapjs": "^2.2.2", + "bcryptjs": "^2.4.0", + "ldapjs": "^2.2.1", + "lru-cache": "^7.10.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/ldapauth-fork/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/ldapjs": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-2.3.3.tgz", + "integrity": "sha512-75QiiLJV/PQqtpH+HGls44dXweviFwQ6SiIK27EqzKQ5jU/7UFrl2E5nLdQ3IYRBzJ/AVFJI66u0MZ0uofKYwg==", + "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md", + "license": "MIT", + "dependencies": { + "abstract-logging": "^2.0.0", + "asn1": "^0.2.4", + "assert-plus": "^1.0.0", + "backoff": "^2.5.0", + "ldap-filter": "^0.3.3", + "once": "^1.4.0", + "vasync": "^2.2.0", + "verror": "^1.8.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -6065,6 +6202,12 @@ "node": ">=0.10.0" } }, + "node_modules/oauth": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz", + "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -6122,6 +6265,15 @@ "node": ">= 0.8" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -6155,6 +6307,19 @@ "url": "https://github.com/sponsors/jaredhanson" } }, + "node_modules/passport-ldapauth": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/passport-ldapauth/-/passport-ldapauth-3.0.1.tgz", + "integrity": "sha512-TRRx3BHi8GC8MfCT9wmghjde/EGeKjll7zqHRRfGRxXbLcaDce2OftbQrFG7/AWaeFhR6zpZHtBQ/IkINdLVjQ==", + "license": "MIT", + "dependencies": { + "ldapauth-fork": "^5.0.1", + "passport-strategy": "^1.0.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/passport-local": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", @@ -6166,6 +6331,23 @@ "node": ">= 0.4.0" } }, + "node_modules/passport-openidconnect": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/passport-openidconnect/-/passport-openidconnect-0.1.2.tgz", + "integrity": "sha512-JX3rTyW+KFZ/E9OF/IpXJPbyLO9vGzcmXB5FgSP2jfL3LGKJPdV7zUE8rWeKeeI/iueQggOeFa3onrCmhxXZTg==", + "license": "MIT", + "dependencies": { + "oauth": "0.10.x", + "passport-strategy": "1.x.x" + }, + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, "node_modules/passport-strategy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", @@ -6613,6 +6795,14 @@ "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==", "license": "MIT" }, + "node_modules/precond": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz", + "integrity": "sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -8179,6 +8369,32 @@ "node": ">= 0.8" } }, + "node_modules/vasync": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vasync/-/vasync-2.2.1.tgz", + "integrity": "sha512-Hq72JaTpcTFdWiNA4Y22Amej2GH3BFmBaKPPlDZ4/oC8HNn2ISHLkFrJU4Ds8R3jcUi7oo5Y9jcMHKjES+N9wQ==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "verror": "1.10.0" + } + }, + "node_modules/vasync/node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, "node_modules/vaul": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.1.tgz", @@ -8192,6 +8408,20 @@ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0" } }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/victory-vendor": { "version": "36.9.2", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", @@ -8833,6 +9063,12 @@ "node": ">=8" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/ws": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", diff --git a/package.json b/package.json index 1bfe642..918ee08 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "cmdk": "^1.0.0", "connect-pg-simple": "^10.0.0", "date-fns": "^3.6.0", + "dotenv": "^16.4.7", "drizzle-orm": "^0.39.1", "drizzle-zod": "^0.7.0", "embla-carousel-react": "^8.3.0", @@ -59,7 +60,9 @@ "memorystore": "^1.6.7", "node-fetch": "^3.3.2", "passport": "^0.7.0", + "passport-ldapauth": "^3.0.1", "passport-local": "^1.0.0", + "passport-openidconnect": "^0.1.2", "react": "^18.3.1", "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", diff --git a/server/auth.ts b/server/auth.ts index baad8fe..bf74460 100644 --- a/server/auth.ts +++ b/server/auth.ts @@ -1,5 +1,7 @@ import passport from "passport"; import { Strategy as LocalStrategy } from "passport-local"; +import { Strategy as LdapStrategy } from "passport-ldapauth"; +import { Strategy as OpenIDConnectStrategy } from "passport-openidconnect"; import { Express, Request } from "express"; import session from "express-session"; import { scrypt, randomBytes, timingSafeEqual } from "crypto"; @@ -7,6 +9,35 @@ import { promisify } from "util"; import { storage } from "./storage"; import { User, InsertUser } from "@shared/schema"; import { z } from "zod"; +import 'dotenv/config'; + +// Authentication configuration from environment variables +const CONFIG = { + // Local auth configuration + LOCAL_AUTH_ENABLED: process.env.LOCAL_AUTH_ENABLED !== 'false', // Enabled by default + DISABLE_REGISTRATION: process.env.DISABLE_REGISTRATION === 'true', // Disabled by default + DEFAULT_ADMIN_USERNAME: process.env.DEFAULT_ADMIN_USERNAME || 'admin', + DEFAULT_ADMIN_PASSWORD: process.env.DEFAULT_ADMIN_PASSWORD || 'password', + DEFAULT_ADMIN_EMAIL: process.env.DEFAULT_ADMIN_EMAIL || 'admin@example.com', + + // LDAP configuration + LDAP_ENABLED: process.env.LDAP_ENABLED === 'true', + LDAP_URL: process.env.LDAP_URL || 'ldap://localhost:389', + LDAP_BIND_DN: process.env.LDAP_BIND_DN || '', + LDAP_BIND_CREDENTIALS: process.env.LDAP_BIND_CREDENTIALS || '', + LDAP_SEARCH_BASE: process.env.LDAP_SEARCH_BASE || '', + LDAP_SEARCH_FILTER: process.env.LDAP_SEARCH_FILTER || '(uid={{username}})', + LDAP_AUTO_REGISTER: process.env.LDAP_AUTO_REGISTER === 'true', + + // OIDC configuration + OIDC_ENABLED: process.env.OIDC_ENABLED === 'true', + OIDC_ISSUER: process.env.OIDC_ISSUER || '', + OIDC_CLIENT_ID: process.env.OIDC_CLIENT_ID || '', + OIDC_CLIENT_SECRET: process.env.OIDC_CLIENT_SECRET || '', + OIDC_CALLBACK_URL: process.env.OIDC_CALLBACK_URL || 'http://localhost:5000/api/auth/oidc/callback', + OIDC_SCOPE: process.env.OIDC_SCOPE || 'openid profile email', + OIDC_AUTO_REGISTER: process.env.OIDC_AUTO_REGISTER === 'true' +}; declare global { namespace Express { @@ -29,6 +60,34 @@ async function comparePasswords(supplied: string, stored: string) { return timingSafeEqual(hashedBuf, suppliedBuf); } +// Function to ensure admin user exists with environment-configured credentials +async function ensureAdminUser() { + try { + // Check if admin user with configured username exists + const adminUsername = CONFIG.DEFAULT_ADMIN_USERNAME; + let adminUser = await storage.getUserByUsername(adminUsername); + + if (!adminUser) { + console.log(`Creating default admin user: ${adminUsername}`); + + // Create default admin user from environment variables + adminUser = await storage.createUser({ + username: adminUsername, + password: await hashPassword(CONFIG.DEFAULT_ADMIN_PASSWORD), + email: CONFIG.DEFAULT_ADMIN_EMAIL, + role: 'admin', + fullName: 'System Administrator' + }); + + console.log(`Default admin user created successfully: ${adminUsername}`); + } else { + console.log(`Default admin user already exists: ${adminUsername}`); + } + } catch (error) { + console.error('Error ensuring admin user exists:', error); + } +} + // API Token Authentication middleware export function authenticateApiToken(req: Request, res: any, next: any) { const authHeader = req.headers.authorization; @@ -70,6 +129,55 @@ export function requireRole(roles: string[]) { }; } +// Function to find or create a user from external auth (LDAP/OIDC) +async function findOrCreateExternalUser(profile: any, source: string): Promise { + try { + // Try to find user by email first + const email = source === 'ldap' + ? profile.mail || profile.email + : profile.emails?.[0]?.value; + + if (!email) { + throw new Error(`No email found in ${source} profile`); + } + + let user = await storage.getUserByEmail(email); + + if (!user) { + // No user found, create a new one if auto-registration is enabled + if ((source === 'ldap' && CONFIG.LDAP_AUTO_REGISTER) || + (source === 'oidc' && CONFIG.OIDC_AUTO_REGISTER)) { + + // Generate username from email or name + const username = source === 'ldap' + ? profile.uid || profile.sAMAccountName || email.split('@')[0] + : profile.displayName?.replace(/\s+/g, '.').toLowerCase() || email.split('@')[0]; + + // Generate a secure random password (user won't know it, but we need something in the DB) + const password = await hashPassword(randomBytes(32).toString('hex')); + + // Create the user + user = await storage.createUser({ + username, + email, + password, + role: "user", + fullName: source === 'ldap' + ? profile.displayName || `${profile.givenName || ''} ${profile.sn || ''}`.trim() + : profile.displayName || profile.name?.givenName + }); + } else { + throw new Error(`User with email ${email} not found and auto-registration is disabled`); + } + } + + return user; + } catch (error) { + console.error(`Error in findOrCreateExternalUser (${source}):`, error); + throw error; + } +} + export function setupAuth(app: Express) { const sessionSettings: session.SessionOptions = { secret: process.env.SESSION_SECRET || 'dynamidns-secret', @@ -91,29 +199,76 @@ export function setupAuth(app: Express) { // Apply API token authentication middleware before routes app.use(authenticateApiToken); - passport.use( - new LocalStrategy(async (username, password, done) => { + // Initialize local auth strategy if enabled + if (CONFIG.LOCAL_AUTH_ENABLED) { + passport.use( + new LocalStrategy(async (username, password, done) => { + try { + // Check if username is email format + const isEmail = username.includes('@'); + + let user; + if (isEmail) { + user = await storage.getUserByEmail(username); + } else { + user = await storage.getUserByUsername(username); + } + + if (!user || !(await comparePasswords(password, user.password))) { + return done(null, false); + } else { + return done(null, user); + } + } catch (error) { + return done(error); + } + }), + ); + } + + // Initialize LDAP strategy if enabled + if (CONFIG.LDAP_ENABLED) { + passport.use(new LdapStrategy({ + server: { + url: CONFIG.LDAP_URL, + bindDN: CONFIG.LDAP_BIND_DN, + bindCredentials: CONFIG.LDAP_BIND_CREDENTIALS, + searchBase: CONFIG.LDAP_SEARCH_BASE, + searchFilter: CONFIG.LDAP_SEARCH_FILTER, + searchAttributes: ['uid', 'mail', 'email', 'cn', 'displayName', 'givenName', 'sn', 'sAMAccountName'] + }, + usernameField: 'username', + passwordField: 'password' + }, async (profile, done) => { try { - // Check if username is email format - const isEmail = username.includes('@'); - - let user; - if (isEmail) { - user = await storage.getUserByEmail(username); - } else { - user = await storage.getUserByUsername(username); - } - - if (!user || !(await comparePasswords(password, user.password))) { - return done(null, false); - } else { - return done(null, user); - } + const user = await findOrCreateExternalUser(profile, 'ldap'); + return done(null, user); } catch (error) { return done(error); } - }), - ); + })); + } + + // Initialize OpenID Connect strategy if enabled + if (CONFIG.OIDC_ENABLED) { + passport.use('oidc', new OpenIDConnectStrategy({ + issuer: CONFIG.OIDC_ISSUER, + authorizationURL: `${CONFIG.OIDC_ISSUER}/authorize`, + tokenURL: `${CONFIG.OIDC_ISSUER}/token`, + userInfoURL: `${CONFIG.OIDC_ISSUER}/userinfo`, + clientID: CONFIG.OIDC_CLIENT_ID, + clientSecret: CONFIG.OIDC_CLIENT_SECRET, + callbackURL: CONFIG.OIDC_CALLBACK_URL, + scope: CONFIG.OIDC_SCOPE.split(' ') + }, async (issuer, profile, context, idToken, accessToken, refreshToken, done) => { + try { + const user = await findOrCreateExternalUser(profile, 'oidc'); + return done(null, user); + } catch (error) { + return done(error); + } + })); + } passport.serializeUser((user, done) => done(null, user.id)); passport.deserializeUser(async (id: string, done) => { @@ -134,7 +289,13 @@ export function setupAuth(app: Express) { organizationId: z.string().uuid().optional() }); + // Registration endpoint app.post("/api/register", async (req, res, next) => { + // Check if registration is disabled + if (CONFIG.DISABLE_REGISTRATION) { + return res.status(403).json({ message: "User registration is disabled" }); + } + try { // Validate input const validatedData = registerSchema.parse(req.body); @@ -173,7 +334,12 @@ export function setupAuth(app: Express) { } }); + // Local auth login endpoint app.post("/api/login", (req, res, next) => { + if (!CONFIG.LOCAL_AUTH_ENABLED) { + return res.status(403).json({ message: "Local authentication is disabled" }); + } + passport.authenticate("local", (err, user, info) => { if (err) return next(err); if (!user) return res.status(401).json({ message: "Invalid credentials" }); @@ -188,6 +354,38 @@ export function setupAuth(app: Express) { })(req, res, next); }); + // LDAP login endpoint + app.post("/api/auth/ldap", (req, res, next) => { + if (!CONFIG.LDAP_ENABLED) { + return res.status(403).json({ message: "LDAP authentication is disabled" }); + } + + passport.authenticate("ldapauth", (err, user, info) => { + if (err) return next(err); + if (!user) return res.status(401).json({ message: "Invalid LDAP credentials" }); + + req.login(user, (err) => { + if (err) return next(err); + + // Remove password from response + const { password, ...userWithoutPassword } = user; + res.status(200).json(userWithoutPassword); + }); + })(req, res, next); + }); + + // OIDC auth endpoints + if (CONFIG.OIDC_ENABLED) { + app.get("/api/auth/oidc", passport.authenticate("oidc")); + + app.get("/api/auth/oidc/callback", + passport.authenticate("oidc", { failureRedirect: "/auth" }), + (req, res) => { + res.redirect("/"); + } + ); + } + app.post("/api/logout", (req, res, next) => { req.logout((err) => { if (err) return next(err); @@ -202,4 +400,18 @@ export function setupAuth(app: Express) { const { password, ...userWithoutPassword } = req.user; res.json(userWithoutPassword); }); + + // Authentication configuration endpoint - exposes auth options (but never secrets) + app.get("/api/auth/config", (req, res) => { + res.json({ + localAuthEnabled: CONFIG.LOCAL_AUTH_ENABLED, + registrationEnabled: !CONFIG.DISABLE_REGISTRATION, + ldapEnabled: CONFIG.LDAP_ENABLED, + oidcEnabled: CONFIG.OIDC_ENABLED, + oidcButtonText: process.env.OIDC_BUTTON_TEXT || "Sign in with OpenID Connect" + }); + }); + + // Ensure admin user exists on server start + ensureAdminUser(); }