Added LDAP Auth Implementation

This commit is contained in:
Brian C. Arnold 2022-12-21 12:12:51 -05:00 committed by Brian C. Arnold
parent c2793fe29b
commit 464a1ea121
6 changed files with 459 additions and 25 deletions

View file

@ -1,11 +1,28 @@
const bcrypt = require('./libs/bcryptjs')
const ldap = require('./authProviders/ldap')
const jwt = require('./libs/jsonwebtoken')
const requestIp = require('./libs/requestIp')
const User = require('./objects/user/User')
const { getId } = require('./utils')
const Logger = require('./Logger')
const Database = require('./Database')
class Auth {
constructor() { }
constructor() {
if (global.ldapEnabled) {
this.ldap = ldap
}
this.user = null
}
get username() {
return this.user ? this.user.username : 'nobody'
}
get users() {
return Database.users
}
cors(req, res, next) {
res.header('Access-Control-Allow-Origin', '*')
@ -34,15 +51,18 @@ class Auth {
// New token secret creation added in v2.1.0 so generate new API tokens for each user
if (Database.users.length) {
for (const user of Database.users) {
user.token = await this.generateAccessToken({ userId: user.id, username: user.username })
Logger.warn(`[Auth] User ${user.username} api token has been updated using new token secret`)
if (user.token) {
delete user.token
}
//Tokens should never be stored on the server, the entire point is that the server can cryptographically verify the token without storing it
Logger.warn(`[Auth] User ${user.username} api token security hole has been removed`)
}
await Database.updateBulkUsers(Database.users)
}
}
async authMiddleware(req, res, next) {
var token = null
let token = null
// If using a get request, the token can be passed as a query string
if (req.method === 'GET' && req.query && req.query.token) {
@ -104,7 +124,16 @@ class Auth {
})
}
getUserLoginResponsePayload(user) {
async getLdapUsers() {
if (this.ldap) {
const ldapUsers = await this.ldap.findAllUsers();
return ldapUsers;
} else {
return [];
}
}
async getUserLoginResponsePayload(user) {
user.token = await this.generateAccessToken({ userId: user.id, username: user.username });
return {
user: user.toJSONForBrowser(),
userDefaultLibraryId: user.getDefaultLibraryId(Database.libraries),
@ -119,7 +148,28 @@ class Auth {
const username = (req.body.username || '').toLowerCase()
const password = req.body.password || ''
const user = Database.users.find(u => u.username.toLowerCase() === username)
let user = Database.users.find(u => u.username.toLowerCase() === username)
if (!user && this.ldap) {
Logger.warn(`[Auth] Could not find user in database from ${ipAddress}, checking LDAP...`)
const ldapUsers = await this.getLdapUsers()
Logger.debug(ldapUsers);
if (ldapUsers.filter(u => u[global.ldapUsernameAttribute].toLowerCase() == username.toLowerCase())) {
const ldapUser = ldapUsers.find(v => v[global.ldapUsernameAttribute].toLowerCase() == username.toLowerCase())
const newUser = new User({
username: ldapUser[global.ldapUsernameAttribute],
id: getId('usr'),
isActive: true,
isLocked: false,
librariesAccessible: [],
itemTagsAccessible: [],
type: 'ldap'
})
const success = await Database.insertEntity('user', newUser)
if (success) {
user = Database.users.find(u => u.username.toLowerCase() === username)
}
}
}
if (!user?.isActive) {
Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`)
@ -136,22 +186,46 @@ class Auth {
return res.status(401).send('Invalid root password (hint: there is none)')
} else {
Logger.info(`[Auth] ${user.username} logged in from ${ipAddress}`)
return res.json(this.getUserLoginResponsePayload(user))
return res.json(await this.getUserLoginResponsePayload(user))
}
}
// Check password match
const compare = await bcrypt.compare(password, user.pash)
if (compare) {
Logger.info(`[Auth] ${user.username} logged in from ${ipAddress}`)
res.json(this.getUserLoginResponsePayload(user))
} else {
Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`)
if (req.rateLimit.remaining <= 2) {
Logger.error(`[Auth] Failed login attempt for user ${user.username} from ip ${ipAddress}. Attempts: ${req.rateLimit.current}`)
return res.status(401).send(`Invalid user or password (${req.rateLimit.remaining === 0 ? '1 attempt remaining' : `${req.rateLimit.remaining + 1} attempts remaining`})`)
if (!user.pash || user.pash === '') {
// Check LDAP for user.
const ldapUsers = await this.getLdapUsers()
const ldapUser = ldapUsers.find(v => v[global.ldapUsernameAttribute].toLowerCase() == username.toLowerCase())
if (ldapUser != null) {
const adUserName = ldapUser.dn
const authResult = await this.ldap.authenticateUser(adUserName, password);
if (authResult) {
res.json(await this.getUserLoginResponsePayload(user))
} else {
Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`)
if (req.rateLimit.remaining <= 2) {
Logger.error(`[Auth] Failed login attempt for user ${user.username} from ip ${ipAddress}. Attempts: ${req.rateLimit.current}`)
return res.status(401).send(`Invalid user or password (${req.rateLimit.remaining === 0 ? '1 attempt remaining' : `${req.rateLimit.remaining + 1} attempts remaining`})`)
}
return res.status(401).send('Invalid user or password')
}
} else {
return res.status(401).send('Invalid user or password')
}
} else {
// Check password match
const compare = await bcrypt.compare(password, user.pash)
if (compare) {
Logger.info(`[Auth] ${user.username} logged in from ${ipAddress}`)
res.json(await this.getUserLoginResponsePayload(user))
} else {
Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`)
if (req.rateLimit.remaining <= 2) {
Logger.error(`[Auth] Failed login attempt for user ${user.username} from ip ${ipAddress}. Attempts: ${req.rateLimit.current}`)
return res.status(401).send(`Invalid user or password (${req.rateLimit.remaining === 0 ? '1 attempt remaining' : `${req.rateLimit.remaining + 1} attempts remaining`})`)
}
return res.status(401).send('Invalid user or password')
}
return res.status(401).send('Invalid user or password')
}
}
@ -162,7 +236,7 @@ class Auth {
}
async userChangePassword(req, res) {
var { password, newPassword } = req.body
const { password, newPassword } = req.body
newPassword = newPassword || ''
const matchingUser = Database.users.find(u => u.id === req.user.id)

View file

@ -38,7 +38,7 @@ const CronManager = require('./managers/CronManager')
const TaskManager = require('./managers/TaskManager')
class Server {
constructor(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) {
constructor(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH, LDAP_ENABLED, LDAP_URL, LDAP_BASE_DN, LDAP_BIND_USER, LDAP_BIND_PASS, LDAP_SEARCH_FILTER, LDAP_USERNAME_ATTRIBUTE) {
this.Port = PORT
this.Host = HOST
global.Source = SOURCE
@ -49,6 +49,19 @@ class Server {
global.MetadataPath = fileUtils.filePathToPOSIX(Path.normalize(METADATA_PATH))
global.RouterBasePath = ROUTER_BASE_PATH
global.XAccel = process.env.USE_X_ACCEL
global.ldapUrl = LDAP_URL
global.ldapBaseDn = LDAP_BASE_DN
global.ldapBindUser = LDAP_BIND_USER
global.ldapBindPass = LDAP_BIND_PASS
global.ldapSearchFilter = LDAP_SEARCH_FILTER
global.ldapUsernameAttribute = LDAP_USERNAME_ATTRIBUTE
global.ldapEnabled = LDAP_ENABLED
// Fix backslash if not on Windows
if (process.platform !== 'win32') {
global.ConfigPath = global.ConfigPath.replace(/\\/g, '/')
global.MetadataPath = global.MetadataPath.replace(/\\/g, '/')
}
if (!fs.pathExistsSync(global.ConfigPath)) {
fs.mkdirSync(global.ConfigPath)

View file

@ -0,0 +1,124 @@
const ldap = require('ldapjs');
const Logger = require('../Logger');
const { res } = require('../libs/dateAndTime');
const { integer } = require('../libs/requestIp/isJs');
class LdapAuth {
constructor() {}
async getClient() {
return await new Promise((resolve, reject) => {
Logger.debug('Connecting...')
const client = ldap.createClient({
url: global.ldapUrl,
bindDN: global.ldapBindUser,
bindCredentials: global.ldapBindPass
});
let onError, onConnect, onTimeout;
onError = err => {
Logger.error('Error Connecting.')
client.removeListener('connect', onConnect);
reject(err);
};
onConnect = () => {
Logger.debug('Connected.')
Logger.debug('Binding...')
client.removeListener('error', onError);
client.bind(global.ldapBindUser, global.ldapBindPass, err => {
if (err) {
Logger.error('Error Binding.')
reject(err);
}
else {
Logger.debug('Bound.')
resolve(client);
}
// client.search
})
};
onTimeout = () => {
Logger.error('Timeout Connecting.')
reject(new Error('LDAP connection timeout'));
}
client.once('connect', onConnect)
client.once('error', onError)
client.once('connectTimeout', onTimeout)
})
}
async findAllUsers() {
const client = await this.getClient();
return await new Promise((resolve, reject) => {
Logger.info('Searching...')
Logger.debug(`global.ldapBaseDn: ${global.ldapBaseDn}`)
Logger.debug(`global.ldapSearchFilter: ${global.ldapSearchFilter}`)
client.search(global.ldapBaseDn, {
scope: 'sub',
filter: global.ldapSearchFilter
}, (err, res) => {
Logger.info('Search returned.')
const items = []
Logger.debug('Attaching to entry events...')
res.on('searchEntry', entry => {
Logger.debug('adding entry...')
items.push(entry.object)
})
Logger.debug('Attaching to error events...')
res.on('end', result => {
Logger.debug('end of entries.')
Logger.info(`Search complete, returning ${items.length} items.`)
resolve(items);
})
})
})
}
authenticateUser(username, password) {
Logger.info('Authenticating LDAP User...');
return new Promise((resolve, reject) => {
const tempClient = ldap.createClient({
url: global.ldapUrl,
bindDN: username,
bindCredentials: password
})
let onError, onConnect, onTimeout;
onError = err => {
Logger.error('Error Connecting.')
tempClient.removeListener('connect', onConnect);
resolve(false);
};
onConnect = () => {
Logger.debug('Connected.')
Logger.debug('Binding...')
tempClient.removeListener('error', onError);
tempClient.bind(username, password, err => {
if (err) {
Logger.error('Error Binding.')
resolve(false);
}
else {
Logger.debug('Bound.')
tempClient.unbind(err => {
if (err) {
Logger.error('Error Unbinding.')
resolve(false);
} else {
Logger.info('User Authenticated.')
resolve(true);
}
})
}
// client.search
})
};
onTimeout = () => {
Logger.error('Timeout Connecting.')
resolve(false);
}
tempClient.once('connect', onConnect)
tempClient.once('error', onError)
tempClient.once('connectTimeout', onTimeout)
});
}
}
module.exports = new LdapAuth()