This commit is contained in:
Simon Hartcher 2026-02-12 00:18:47 +11:00 committed by GitHub
commit 9b9232802a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 307 additions and 1 deletions

View file

@ -2,7 +2,7 @@ import ldap, {SearchOptions} from "ldapjs";
import colors from "chalk";
import log from "../../log";
import Config from "../../config";
import Config, {ConfigType} from "../../config";
import type {AuthHandler} from "../auth";
function ldapAuthCommon(
@ -236,6 +236,186 @@ function isLdapEnabled() {
return !Config.values.public && Config.values.ldap.enable;
}
export type LdapConfig = ConfigType["ldap"];
import type {
AuthProvider,
AuthenticateParams,
AuthResult,
AuthStartParams,
AuthStartInfo,
DisconnectParams,
GetValidUsersParams,
LogoutParams,
LogoutInfo,
} from "./types";
export class SimpleLDAPAuthProvider implements AuthProvider {
name = "ldap";
canChangePassword = false;
async init(): Promise<void> {}
getValidUsers({users}: GetValidUsersParams): Promise<string[]> {
// Simple LDAP can't test for user existence without the user's
// unhashed password, so return all users unchanged
return Promise.resolve(users);
}
authStart(_params: AuthStartParams): AuthStartInfo {
return {};
}
async authenticate({
manager,
client,
username,
password,
}: AuthenticateParams): Promise<AuthResult> {
return new Promise((resolve) => {
simpleLdapAuth(username, password, (valid) => {
if (valid && !client) {
manager.addUser(username, null, true);
}
if (valid) {
resolve({success: true, username});
} else {
resolve({success: false});
}
});
});
}
logout(_params: LogoutParams): LogoutInfo {
return {};
}
disconnect(_params: DisconnectParams): void {}
}
export class AdvancedLDAPAuthProvider implements AuthProvider {
name = "ldap";
canChangePassword = false;
config: LdapConfig;
constructor(config: LdapConfig) {
this.config = config;
}
async init(): Promise<void> {}
async getValidUsers({users}: GetValidUsersParams): Promise<string[]> {
return new Promise((resolve) => {
const validUsers: string[] = [];
const ldapclient = ldap.createClient({
url: this.config.url,
tlsOptions: this.config.tlsOptions,
});
const base = this.config.searchDN.base;
ldapclient.on("error", function (err: Error) {
log.error(`Unable to connect to LDAP server: ${err.toString()}`);
resolve(users);
});
ldapclient.bind(
this.config.searchDN.rootDN,
this.config.searchDN.rootPassword,
(err) => {
if (err) {
log.error("Invalid LDAP root credentials");
resolve(users);
return;
}
const userSet = new Set(users);
const searchOptions: SearchOptions = {
scope: this.config.searchDN.scope,
filter: `${this.config.searchDN.filter}`,
attributes: [this.config.primaryKey],
paged: true,
};
ldapclient.search(base, searchOptions, function (err2, res) {
if (err2) {
log.error(`LDAP search error: ${err2?.toString()}`);
resolve(users);
return;
}
res.on("searchEntry", function (entry) {
const user = entry.attributes[0].vals[0].toString();
if (userSet.has(user)) {
validUsers.push(user);
}
});
res.on("error", function (err3) {
log.error(`LDAP error: ${err3.toString()}`);
ldapclient.unbind();
resolve(users);
});
res.on("end", function () {
const validSet = new Set(validUsers);
users.forEach((user) => {
if (!validSet.has(user)) {
log.warn(
`No account info in LDAP for ${colors.bold(
user
)} but user config file exists`
);
}
});
ldapclient.unbind();
resolve(validUsers);
});
});
}
);
});
}
authStart(_params: AuthStartParams): AuthStartInfo {
return {};
}
async authenticate({
manager,
client,
username,
password,
}: AuthenticateParams): Promise<AuthResult> {
return new Promise((resolve) => {
advancedLdapAuth(username, password, (valid) => {
if (valid && !client) {
manager.addUser(username, null, true);
}
if (valid) {
resolve({success: true, username});
} else {
resolve({success: false});
}
});
});
}
logout(_params: LogoutParams): LogoutInfo {
return {};
}
disconnect(_params: DisconnectParams): void {}
}
export default {
moduleName: "ldap",
auth: ldapAuth,

View file

@ -2,6 +2,17 @@ import colors from "chalk";
import log from "../../log";
import Helper from "../../helper";
import type {AuthHandler} from "../auth";
import type {
AuthProvider,
AuthenticateParams,
AuthResult,
AuthStartParams,
AuthStartInfo,
DisconnectParams,
GetValidUsersParams,
LogoutParams,
LogoutInfo,
} from "./types";
const localAuth: AuthHandler = (_manager, client, user, password, callback) => {
// If no user is found, or if the client has not provided a password,
@ -44,6 +55,71 @@ const localAuth: AuthHandler = (_manager, client, user, password, callback) => {
});
};
export class LocalAuthProvider implements AuthProvider {
name = "local";
canChangePassword = true;
async init(): Promise<void> {}
async getValidUsers({users}: GetValidUsersParams): Promise<string[]> {
return users;
}
authStart(_params: AuthStartParams): AuthStartInfo {
return {};
}
async authenticate({client, username, password}: AuthenticateParams): Promise<AuthResult> {
if (!client || !password) {
return {success: false};
}
if (!client.config.password) {
log.error(
`User ${colors.bold(
username
)} with no local password set tried to sign in. (Probably a LDAP user)`
);
return {success: false};
}
let matching: boolean;
try {
matching = await Helper.password.compare(password, client.config.password);
} catch (error) {
log.error(`Error while checking users password. Error: ${error}`);
return {success: false};
}
if (!matching) {
return {success: false};
}
if (Helper.password.requiresUpdate(client.config.password)) {
const hash = Helper.password.hash(password);
client.setPassword(hash, (success) => {
if (success) {
log.info(
`User ${colors.bold(
client.name
)} logged in and their hashed password has been updated to match new security requirements`
);
}
});
}
return {success: true, username};
}
logout(_params: LogoutParams): LogoutInfo {
return {};
}
disconnect(_params: DisconnectParams): void {}
}
export default {
moduleName: "local",
auth: localAuth,

View file

@ -0,0 +1,50 @@
import type ClientManager from "../../clientManager";
import type Client from "../../client";
export interface GetValidUsersParams {
users: string[];
}
export interface AuthStartParams {
socketId: string;
}
export interface AuthenticateParams {
manager: ClientManager;
client: Client | null;
username: string;
password: string;
}
export interface LogoutParams {
sessionData?: Record<string, unknown>;
}
export interface DisconnectParams {
socketId: string;
}
export interface AuthStartInfo {
authUrl?: string;
}
export type AuthResult =
| {success: false}
| {success: true; username: string; sessionData?: Record<string, unknown>};
export interface LogoutInfo {
logoutUrl?: string;
}
export interface AuthProvider {
name: string;
canChangePassword: boolean;
init(): Promise<void>;
getValidUsers(params: GetValidUsersParams): Promise<string[]>;
authStart(params: AuthStartParams): AuthStartInfo;
authenticate(params: AuthenticateParams): Promise<AuthResult>;
logout(params: LogoutParams): LogoutInfo;
disconnect(params: DisconnectParams): void;
}