From 0dc4a0b8ccecaa411c173aef59417a83119444ba Mon Sep 17 00:00:00 2001 From: alphaeusmote <41258468-alphaeusmote@users.noreply.replit.com> Date: Thu, 10 Apr 2025 02:01:11 +0000 Subject: [PATCH] Add LDAP and OpenID Connect authentication Replit-Commit-Author: Agent Replit-Commit-Session-Id: 705f2157-ef97-4fbd-89e4-8c7f2ecaea90 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7ed01c5f-a82d-405a-b728-b2e3d127c60c/f53b9394-0261-478c-aa14-813efc485813.jpg --- server/auth.ts | 42 +++++++++++-- server/storage.ts | 10 ++++ shared/schema.ts | 4 +- shared/types/passport-ldapauth.d.ts | 47 +++++++++++++++ shared/types/passport-openidconnect.d.ts | 75 ++++++++++++++++++++++++ 5 files changed, 173 insertions(+), 5 deletions(-) create mode 100644 shared/types/passport-ldapauth.d.ts create mode 100644 shared/types/passport-openidconnect.d.ts diff --git a/server/auth.ts b/server/auth.ts index a4a0f67..f7d9df1 100644 --- a/server/auth.ts +++ b/server/auth.ts @@ -3,7 +3,7 @@ import { Strategy as LocalStrategy } from "passport-local"; import { Strategy as JwtStrategy, ExtractJwt } from "passport-jwt"; import LdapStrategy from "passport-ldapauth"; import { Strategy as OpenIDStrategy } from "passport-openidconnect"; -import { Express } from "express"; +import { Express, Request, Response, NextFunction } from "express"; import session from "express-session"; import { scrypt, randomBytes, timingSafeEqual } from "crypto"; import { promisify } from "util"; @@ -140,7 +140,7 @@ export function setupAuth(app: Express) { // OpenID Connect authentication strategy if (process.env.OIDC_ENABLED === "true") { passport.use( - new OpenIDConnectStrategy( + new OpenIDStrategy( { issuer: process.env.OIDC_ISSUER, authorizationURL: process.env.OIDC_AUTHORIZATION_URL, @@ -151,7 +151,7 @@ export function setupAuth(app: Express) { callbackURL: process.env.OIDC_CALLBACK_URL || "http://localhost:3000/api/auth/oidc/callback", scope: ["openid", "profile", "email"], }, - async (issuer, profile, done) => { + async (issuer: string, profile: any, done: any) => { try { const { id, displayName, emails } = profile; @@ -236,7 +236,7 @@ export function setupAuth(app: Express) { } }); - // Login endpoint + // Login endpoint (local strategy) app.post("/api/login", (req, res, next) => { passport.authenticate("local", (err: any, user: any, info: any) => { if (err) return next(err); @@ -251,6 +251,40 @@ export function setupAuth(app: Express) { }); })(req, res, next); }); + + // LDAP authentication routes + if (process.env.LDAP_ENABLED === "true") { + // LDAP login endpoint + app.post("/api/auth/ldap", (req, res, next) => { + passport.authenticate("ldapauth", (err: any, user: any, info: any) => { + if (err) return next(err); + if (!user) { + return res.status(401).json({ message: info?.message || "LDAP authentication failed" }); + } + req.login(user, (loginErr) => { + if (loginErr) return next(loginErr); + // Remove password from response + const userResponse = { ...user, password: undefined }; + res.json(userResponse); + }); + })(req, res, next); + }); + } + + // OpenID Connect authentication routes + if (process.env.OIDC_ENABLED === "true") { + // OIDC login initiation + app.get("/api/auth/oidc", passport.authenticate("openidconnect")); + + // OIDC callback + app.get("/api/auth/oidc/callback", + passport.authenticate("openidconnect", { failureRedirect: "/auth" }), + (req: Request, res: Response) => { + // Successful authentication, redirect to the main application + res.redirect("/"); + } + ); + } // Logout endpoint app.post("/api/logout", (req, res, next) => { diff --git a/server/storage.ts b/server/storage.ts index dfd4065..8bf3dd6 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -168,6 +168,16 @@ export class DatabaseStorage implements IStorage { const result = await db.select().from(users).where(eq(users.username, username)); return result.length > 0 ? result[0] : undefined; } + + async getUserByProviderUserId(provider: string, providerUserId: string): Promise { + const result = await db.select().from(users).where( + and( + eq(users.authProvider, provider), + eq(users.providerUserId, providerUserId) + ) + ); + return result.length > 0 ? result[0] : undefined; + } async createUser(insertUser: InsertUser): Promise { const result = await db.insert(users).values(insertUser).returning(); diff --git a/shared/schema.ts b/shared/schema.ts index 1a779a2..2752423 100644 --- a/shared/schema.ts +++ b/shared/schema.ts @@ -106,7 +106,7 @@ export const rolePermissions = pgTable("role_permissions", { }; }); -// User schema for local authentication +// User schema for authentication (local, LDAP, or OIDC) export const users = pgTable("users", { id: serial("id").primaryKey(), username: text("username").notNull().unique(), @@ -114,6 +114,8 @@ export const users = pgTable("users", { email: text("email"), fullName: text("full_name"), roleId: integer("role_id").references(() => roles.id), + authProvider: text("auth_provider").default("local"), + providerUserId: text("provider_user_id"), createdAt: timestamp("created_at").defaultNow(), }); diff --git a/shared/types/passport-ldapauth.d.ts b/shared/types/passport-ldapauth.d.ts new file mode 100644 index 0000000..e9777ea --- /dev/null +++ b/shared/types/passport-ldapauth.d.ts @@ -0,0 +1,47 @@ +declare module 'passport-ldapauth' { + import { Strategy as PassportStrategy } from 'passport'; + + export interface LdapAuthOptions { + server: { + url: string; + bindDN?: string; + bindCredentials?: string; + searchBase: string; + searchFilter: string; + searchAttributes?: string[]; + tlsOptions?: { + rejectUnauthorized?: boolean; + }; + }; + usernameField?: string; + passwordField?: string; + passReqToCallback?: boolean; + } + + export type VerifyCallback = ( + err?: Error | null, + user?: object, + info?: object + ) => void; + + export type VerifyFunction = ( + user: any, + verified: VerifyCallback + ) => void; + + export type VerifyFunctionWithRequest = ( + req: object, + user: any, + verified: VerifyCallback + ) => void; + + export default class Strategy extends PassportStrategy { + constructor( + options: LdapAuthOptions, + verify?: VerifyFunction | VerifyFunctionWithRequest + ); + + name: string; + authenticate(req: object, options?: object): void; + } +} \ No newline at end of file diff --git a/shared/types/passport-openidconnect.d.ts b/shared/types/passport-openidconnect.d.ts new file mode 100644 index 0000000..2d01a74 --- /dev/null +++ b/shared/types/passport-openidconnect.d.ts @@ -0,0 +1,75 @@ +declare module 'passport-openidconnect' { + import { Strategy as PassportStrategy } from 'passport'; + + export interface Profile { + id: string; + displayName?: string; + name?: { + familyName?: string; + givenName?: string; + middleName?: string; + }; + emails?: Array<{ value: string; type?: string }>; + photos?: Array<{ value: string }>; + username?: string; + _json: any; + _raw: string; + } + + export interface StrategyOptions { + issuer?: string; + authorizationURL?: string; + tokenURL?: string; + userInfoURL?: string; + clientID: string; + clientSecret: string; + callbackURL: string; + scope?: string | string[]; + passReqToCallback?: false; + } + + export interface StrategyOptionsWithRequest { + issuer?: string; + authorizationURL?: string; + tokenURL?: string; + userInfoURL?: string; + clientID: string; + clientSecret: string; + callbackURL: string; + scope?: string | string[]; + passReqToCallback: true; + } + + export type VerifyCallback = ( + err?: Error | null, + user?: object, + info?: object + ) => void; + + export type VerifyFunction = ( + issuer: string, + profile: Profile, + verified: VerifyCallback + ) => void; + + export type VerifyFunctionWithRequest = ( + req: object, + issuer: string, + profile: Profile, + verified: VerifyCallback + ) => void; + + export class Strategy extends PassportStrategy { + constructor( + options: StrategyOptions, + verify: VerifyFunction + ); + constructor( + options: StrategyOptionsWithRequest, + verify: VerifyFunctionWithRequest + ); + + name: string; + authenticate(req: object, options?: object): void; + } +} \ No newline at end of file