mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-15 22:01:37 +00:00
Added LDAP Auth Implementation
This commit is contained in:
parent
c2793fe29b
commit
464a1ea121
6 changed files with 459 additions and 25 deletions
112
server/Auth.js
112
server/Auth.js
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
124
server/authProviders/ldap.js
Normal file
124
server/authProviders/ldap.js
Normal 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue