my own indieAuth provider!
indiko.dunkirk.sh/docs
indieauth
oauth2-server
1import { authenticate } from "ldap-authentication";
2import { db } from "./db";
3
4interface LdapUser {
5 username: string;
6 id: number;
7 status: string;
8 created_at: number;
9}
10
11interface AuditResult {
12 total: number;
13 active: number;
14 orphaned: number;
15 errors: number;
16 orphanedUsers: Array<{
17 username: string;
18 id: number;
19 status: string;
20 createdAt: number;
21 }>;
22}
23
24export async function checkLdapUser(username: string): Promise<boolean> {
25 try {
26 const user = await authenticate({
27 ldapOpts: {
28 url: process.env.LDAP_URL || "ldap://localhost:389",
29 },
30 adminDn: process.env.LDAP_ADMIN_DN || "",
31 adminPassword: process.env.LDAP_ADMIN_PASSWORD || "",
32 userSearchBase: process.env.LDAP_USER_SEARCH_BASE || "dc=example,dc=com",
33 usernameAttribute: process.env.LDAP_USERNAME_ATTRIBUTE || "uid",
34 username: username,
35 verifyUserExists: true,
36 });
37 return !!user;
38 } catch (error) {
39 // User not found or invalid credentials (expected for non-existence check)
40 return false;
41 }
42}
43
44export async function checkLdapGroupMembership(
45 username: string,
46 userDn: string,
47): Promise<boolean> {
48 if (!process.env.LDAP_GROUP_DN) {
49 return true; // No group restriction configured
50 }
51
52 try {
53 const groupDn = process.env.LDAP_GROUP_DN;
54 const groupClass = process.env.LDAP_GROUP_CLASS || "groupOfUniqueNames";
55 const memberAttribute =
56 process.env.LDAP_GROUP_MEMBER_ATTRIBUTE || "uniqueMember";
57
58 const user = await authenticate({
59 ldapOpts: {
60 url: process.env.LDAP_URL || "ldap://localhost:389",
61 },
62 userDn: userDn,
63 userSearchBase: process.env.LDAP_USER_SEARCH_BASE || "dc=example,dc=com",
64 usernameAttribute: process.env.LDAP_USERNAME_ATTRIBUTE || "uid",
65 username: username,
66 verifyUserExists: true,
67 groupsSearchBase: groupDn,
68 groupClass: groupClass,
69 groupMemberAttribute: memberAttribute,
70 });
71
72 // If user was found and authenticate returns it, groups are available
73 return !!user;
74 } catch (error) {
75 console.error("LDAP group membership check failed:", error);
76 return false;
77 }
78}
79
80export async function getLdapAccounts(): Promise<AuditResult> {
81 console.log("🔍 Starting LDAP orphan account audit...\n");
82
83 // Get all LDAP-provisioned users
84 const ldapUsers = db
85 .query(
86 "SELECT id, username, status, created_at FROM users WHERE provisioned_via_ldap = 1",
87 )
88 .all() as LdapUser[];
89
90 const result: AuditResult = {
91 total: ldapUsers.length,
92 active: 0,
93 orphaned: 0,
94 errors: 0,
95 orphanedUsers: [],
96 };
97
98 console.log(`Found ${result.total} LDAP-provisioned accounts\n`);
99
100 // Check each user against LDAP
101 for (const user of ldapUsers) {
102 process.stdout.write(`Checking ${user.username}... `);
103
104 try {
105 const existsInLdap = await checkLdapUser(user.username);
106
107 if (existsInLdap) {
108 console.log("✅ Found in LDAP");
109 result.active++;
110 } else {
111 console.log("❌ NOT FOUND in LDAP");
112 result.orphaned++;
113 result.orphanedUsers.push({
114 username: user.username,
115 id: user.id,
116 status: user.status,
117 createdAt: user.created_at,
118 });
119 }
120 } catch (error) {
121 console.log("⚠️ Error checking LDAP");
122 result.errors++;
123 console.error(
124 ` Error: ${error instanceof Error ? error.message : String(error)}`,
125 );
126 }
127 }
128
129 return result;
130}
131
132export async function updateOrphanedAccounts(
133 result: AuditResult,
134 action: "suspend" | "deactivate" | "remove",
135): Promise<void> {
136 const newStatus = action === "suspend" ? "suspended" : "inactive";
137
138 console.log(
139 `\n📝 Updating ${result.orphaned} orphaned account(s) to status: '${newStatus}'`,
140 );
141
142 for (const user of result.orphanedUsers) {
143 if (action === "remove") {
144 db.query("DELETE FROM users WHERE id = ?").run(user.id);
145 console.log(` Removed: ${user.username}`);
146 continue;
147 }
148 db.query("UPDATE users SET status = ? WHERE id = ?").run(
149 newStatus,
150 user.id,
151 );
152 console.log(` Updated: ${user.username}`);
153 }
154
155 console.log(`\n✅ Updated ${result.orphaned} account(s)`);
156}