@sync-in/server
Version:
The secure, open-source platform for file storage, sharing, collaboration, and sync
326 lines (325 loc) • 15.2 kB
JavaScript
/*
* Copyright (C) 2012-2025 Johan Legrand <johan.legrand@sync-in.com>
* This file is part of Sync-in | The open source file sync and share solution
* See the LICENSE file for licensing details
*/ "use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "AuthMethodLdapService", {
enumerable: true,
get: function() {
return AuthMethodLdapService;
}
});
const _common = require("@nestjs/common");
const _ldapts = require("ldapts");
const _appconstants = require("../../../app.constants");
const _user = require("../../../applications/users/constants/user");
const _adminusersmanagerservice = require("../../../applications/users/services/admin-users-manager.service");
const _usersmanagerservice = require("../../../applications/users/services/users-manager.service");
const _functions = require("../../../common/functions");
const _configenvironment = require("../../../configuration/config.environment");
const _authldap = require("../../constants/auth-ldap");
function _ts_decorate(decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
}
function _ts_metadata(k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
}
let AuthMethodLdapService = class AuthMethodLdapService {
async validateUser(login, password, ip, scope) {
// Find user from his login or email
let user = await this.usersManager.findUser(this.dbLogin(login), false);
if (user) {
if (user.isGuest) {
// Allow guests to be authenticated from db and check if the current user is defined as active
return this.usersManager.logUser(user, password, ip);
}
if (!user.isActive) {
this.logger.error(`${this.validateUser.name} - user *${user.login}* is locked`);
throw new _common.HttpException('Account locked', _common.HttpStatus.FORBIDDEN);
}
}
// If a user was found, use the stored login. This allows logging in with an email.
const entry = await this.checkAuth(user?.login || login, password);
if (entry === false) {
// LDAP auth failed
if (user) {
let authSuccess = false;
if (scope) {
// Try user app password
authSuccess = await this.usersManager.validateAppPassword(user, password, ip, scope);
}
this.usersManager.updateAccesses(user, ip, authSuccess).catch((e)=>this.logger.error(`${this.validateUser.name} : ${e}`));
if (authSuccess) {
// Logged with app password
return user;
}
}
return null;
} else if (!entry[this.ldapConfig.attributes.login] || !entry[this.ldapConfig.attributes.email]) {
this.logger.error(`${this.validateUser.name} - required ldap fields are missing :
[${this.ldapConfig.attributes.login}, ${this.ldapConfig.attributes.email}] =>
(${JSON.stringify(entry)})`);
return null;
}
const identity = this.createIdentity(entry, password);
user = await this.updateOrCreateUser(identity, user);
this.usersManager.updateAccesses(user, ip, true).catch((e)=>this.logger.error(`${this.validateUser.name} : ${e}`));
return user;
}
async checkAuth(login, password) {
const ldapLogin = this.buildLdapLogin(login);
// AD: bind directly with the user input (UPN or DOMAIN\user)
// Generic LDAP: build DN from login attribute + baseDN
const bindUserDN = this.isAD ? ldapLogin : `${this.ldapConfig.attributes.login}=${ldapLogin},${this.ldapConfig.baseDN}`;
let client;
let error;
for (const s of this.ldapConfig.servers){
client = new _ldapts.Client({
...this.clientOptions,
url: s
});
try {
await client.bind(bindUserDN, password);
return await this.checkAccess(ldapLogin, client);
} catch (e) {
if (e.errors?.length) {
for (const err of e.errors){
this.logger.warn(`${this.checkAuth.name} - ${ldapLogin} : ${err}`);
error = err;
}
} else {
error = e;
this.logger.warn(`${this.checkAuth.name} - ${ldapLogin} : ${e}`);
}
if (error instanceof _ldapts.InvalidCredentialsError) {
return false;
}
} finally{
await client.unbind();
}
}
if (error && _appconstants.CONNECT_ERROR_CODE.has(error.code)) {
throw new _common.HttpException('Authentication service error', _common.HttpStatus.INTERNAL_SERVER_ERROR);
}
return false;
}
async checkAccess(login, client) {
const searchFilter = this.buildUserFilter(login, this.ldapConfig.filter);
try {
const { searchEntries } = await client.search(this.ldapConfig.baseDN, {
scope: 'sub',
filter: searchFilter,
attributes: _authldap.ALL_LDAP_ATTRIBUTES
});
if (searchEntries.length === 0) {
this.logger.debug(`${this.checkAccess.name} - search filter : ${searchFilter}`);
this.logger.warn(`${this.checkAccess.name} - no LDAP entry found for : ${login}`);
return false;
}
if (searchEntries.length > 1) {
this.logger.warn(`${this.checkAccess.name} - multiple LDAP entries found for : ${login}, using first one`);
}
// Always return the first valid entry
return this.convertToLdapUserEntry(searchEntries[0]);
} catch (e) {
this.logger.debug(`${this.checkAccess.name} - search filter : ${searchFilter}`);
this.logger.error(`${this.checkAccess.name} - ${login} : ${e}`);
return false;
}
}
async updateOrCreateUser(identity, user) {
if (user === null) {
// Create
const createdUser = await this.adminUsersManager.createUserOrGuest(identity, identity.role);
const freshUser = await this.usersManager.fromUserId(createdUser.id);
if (!freshUser) {
this.logger.error(`${this.updateOrCreateUser.name} - user was not found : ${createdUser.login} (${createdUser.id})`);
throw new _common.HttpException('User not found', _common.HttpStatus.NOT_FOUND);
}
return freshUser;
}
if (identity.login !== user.login) {
this.logger.error(`${this.updateOrCreateUser.name} - user login mismatch : ${identity.login} !== ${user.login}`);
throw new _common.HttpException('Account matching error', _common.HttpStatus.FORBIDDEN);
}
// Update: check if user information has changed
const identityHasChanged = Object.fromEntries((await Promise.all(Object.keys(identity).map(async (key)=>{
if (key === 'password') {
const isSame = await (0, _functions.comparePassword)(identity[key], user.password);
return isSame ? null : [
key,
identity[key]
];
}
return identity[key] !== user[key] ? [
key,
identity[key]
] : null;
}))).filter(Boolean));
if (Object.keys(identityHasChanged).length > 0) {
try {
if (identityHasChanged?.role != null) {
if (user.role === _user.USER_ROLE.ADMINISTRATOR && !this.ldapConfig.adminGroup) {
// Prevent removing the admin role when adminGroup was removed or not defined
delete identityHasChanged.role;
}
}
// Update user properties
await this.adminUsersManager.updateUserOrGuest(user.id, identityHasChanged);
// Extra stuff
if (identityHasChanged?.password) {
delete identityHasChanged.password;
}
Object.assign(user, identityHasChanged);
if ('lastName' in identityHasChanged || 'firstName' in identityHasChanged) {
// Force fullName update in the current user model
user.setFullName(true);
}
} catch (e) {
this.logger.warn(`${this.updateOrCreateUser.name} - unable to update user *${user.login}* : ${e}`);
}
}
return user;
}
convertToLdapUserEntry(entry) {
for (const attr of _authldap.ALL_LDAP_ATTRIBUTES){
if (attr === _authldap.LDAP_COMMON_ATTR.MEMBER_OF && entry[attr]) {
entry[attr] = (Array.isArray(entry[attr]) ? entry[attr] : entry[attr] ? [
entry[attr]
] : []).filter((v)=>typeof v === 'string').map((v)=>v.match(/cn\s*=\s*([^,]+)/i)?.[1]?.trim()).filter(Boolean);
continue;
}
if (Array.isArray(entry[attr])) {
// Keep only the first value for all other attributes (e.g., email)
entry[attr] = entry[attr].length > 0 ? entry[attr][0] : null;
}
}
return entry;
}
createIdentity(entry, password) {
const isAdmin = typeof this.ldapConfig.adminGroup === 'string' && this.ldapConfig.adminGroup && entry[_authldap.LDAP_COMMON_ATTR.MEMBER_OF]?.includes(this.ldapConfig.adminGroup);
return {
login: this.dbLogin(entry[this.ldapConfig.attributes.login]),
email: entry[this.ldapConfig.attributes.email],
password: password,
role: isAdmin ? _user.USER_ROLE.ADMINISTRATOR : _user.USER_ROLE.USER,
...this.getFirstNameAndLastName(entry)
};
}
getFirstNameAndLastName(entry) {
// 1) Prefer structured attributes
if (entry.sn && entry.givenName) {
return {
firstName: entry.givenName,
lastName: entry.sn
};
}
// 2) Fallback to displayName if available
if (entry.displayName && entry.displayName.trim()) {
return (0, _functions.splitFullName)(entry.displayName);
}
// 3) Fallback to cn
if (entry.cn && entry.cn.trim()) {
return (0, _functions.splitFullName)(entry.cn);
}
// 4) Nothing usable
return {
firstName: '',
lastName: ''
};
}
dbLogin(login) {
if (login.includes('\\')) {
return login.split('\\').slice(-1)[0];
}
return login;
}
buildLdapLogin(login) {
if (this.ldapConfig.attributes.login === _authldap.LDAP_LOGIN_ATTR.UPN) {
if (this.ldapConfig.upnSuffix && !login.includes('@')) {
return `${login}@${this.ldapConfig.upnSuffix}`;
}
} else if (this.ldapConfig.attributes.login === _authldap.LDAP_LOGIN_ATTR.SAM) {
if (this.ldapConfig.netbiosName && !login.includes('\\')) {
return `${this.ldapConfig.netbiosName}\\${login}`;
}
}
return login;
}
buildUserFilter(login, extraFilter) {
// Build a safe LDAP filter to search for a user.
// Important: - Values passed to EqualityFilter are auto-escaped by ldapts
// - extraFilter is appended as-is (assumed trusted configuration)
// Output: (&(|(userPrincipalName=john.doe@sync-in.com)(sAMAccountName=john.doe)(cn=john.doe)(uid=john.doe)(mail=john.doe@sync-in.com))(*extraFilter*))
// Handle the case where the sAMAccountName is provided in domain-qualified format (e.g., SYNC_IN\\user)
// Note: sAMAccountName is always stored without the domain in Active Directory.
const dbLogin = this.dbLogin(login);
const or = new _ldapts.OrFilter({
filters: this.isAD ? [
new _ldapts.EqualityFilter({
attribute: _authldap.LDAP_LOGIN_ATTR.SAM,
value: dbLogin
}),
new _ldapts.EqualityFilter({
attribute: _authldap.LDAP_LOGIN_ATTR.UPN,
value: dbLogin
}),
new _ldapts.EqualityFilter({
attribute: _authldap.LDAP_LOGIN_ATTR.MAIL,
value: dbLogin
})
] : [
new _ldapts.EqualityFilter({
attribute: _authldap.LDAP_LOGIN_ATTR.UID,
value: dbLogin
}),
new _ldapts.EqualityFilter({
attribute: _authldap.LDAP_LOGIN_ATTR.CN,
value: dbLogin
}),
new _ldapts.EqualityFilter({
attribute: _authldap.LDAP_LOGIN_ATTR.MAIL,
value: dbLogin
})
]
});
// Convert to LDAP filter string
let filterString = new _ldapts.AndFilter({
filters: [
or
]
}).toString();
// Optionally append an extra filter from config (trusted source)
if (extraFilter && extraFilter.trim()) {
filterString = `(&${filterString}${extraFilter})`;
}
return filterString;
}
constructor(usersManager, adminUsersManager){
this.usersManager = usersManager;
this.adminUsersManager = adminUsersManager;
this.logger = new _common.Logger(AuthMethodLdapService.name);
this.ldapConfig = _configenvironment.configuration.auth.ldap;
this.isAD = this.ldapConfig.attributes.login === _authldap.LDAP_LOGIN_ATTR.SAM || this.ldapConfig.attributes.login === _authldap.LDAP_LOGIN_ATTR.UPN;
this.clientOptions = {
timeout: 6000,
connectTimeout: 6000,
url: ''
};
}
};
AuthMethodLdapService = _ts_decorate([
(0, _common.Injectable)(),
_ts_metadata("design:type", Function),
_ts_metadata("design:paramtypes", [
typeof _usersmanagerservice.UsersManager === "undefined" ? Object : _usersmanagerservice.UsersManager,
typeof _adminusersmanagerservice.AdminUsersManager === "undefined" ? Object : _adminusersmanagerservice.AdminUsersManager
])
], AuthMethodLdapService);
//# sourceMappingURL=auth-method-ldap.service.js.map