Added user rebinding on reconnect in LDAP (#9339)

* Added reconnect handling code for LDAP

* Update api/src/auth/drivers/ldap.ts

* Improved error handling

* Updated re-bind so we can await it when required

* Added bind check before operations to ensure client has user

* Cleaned up reconnect handling in LDAP

* Minor cleanup

* Minor cleanup
This commit is contained in:
Aiden Foxx
2021-11-03 16:12:50 +01:00
committed by GitHub
parent 1bd43bd25f
commit 2f33f71a6a

View File

@@ -7,6 +7,7 @@ import ldap, {
InappropriateAuthenticationError,
InvalidCredentialsError,
InsufficientAccessRightsError,
OperationsError,
} from 'ldapjs';
import ms from 'ms';
import Joi from 'joi';
@@ -37,58 +38,75 @@ interface UserInfo {
const INVALID_ACCOUNT_FLAGS = 0x800012;
export class LDAPAuthDriver extends AuthDriver {
bindClient: Promise<Client>;
bindClient: Client;
usersService: UsersService;
config: Record<string, any>;
constructor(options: AuthDriverOptions, config: Record<string, any>) {
super(options, config);
const { bindDn, bindPassword, ...additionalConfig } = config;
const { bindDn, bindPassword, userDn, provider, clientUrl } = config;
if (
!bindDn ||
!bindPassword ||
!additionalConfig.userDn ||
!additionalConfig.provider ||
(!additionalConfig.clientUrl && !additionalConfig.client?.socketPath)
) {
throw new InvalidConfigException('Invalid provider config', { provider: additionalConfig.provider });
if (!bindDn || !bindPassword || !userDn || !provider || (!clientUrl && !config.client?.socketPath)) {
throw new InvalidConfigException('Invalid provider config', { provider });
}
this.bindClient = new Promise((resolve, reject) => {
const clientConfig = typeof additionalConfig.client === 'object' ? additionalConfig.client : {};
const client = ldap.createClient({ url: additionalConfig.clientUrl, reconnect: true, ...clientConfig });
const clientConfig = typeof config.client === 'object' ? config.client : {};
client.on('error', (err: Error) => {
logger.error(err);
});
client.bind(bindDn, bindPassword, (err: Error | null) => {
if (err) {
const error = handleError(err);
if (error instanceof InvalidCredentialsException) {
reject(new InvalidConfigException('Invalid bind user', { provider: additionalConfig.provider }));
} else {
reject(error);
}
return;
}
resolve(client);
});
this.bindClient = ldap.createClient({ url: clientUrl, reconnect: true, ...clientConfig });
this.bindClient.on('error', (err: Error) => {
logger.warn(err);
});
this.usersService = new UsersService({ knex: this.knex, schema: this.schema });
this.config = additionalConfig;
this.config = config;
}
private async validateBindClient(): Promise<void> {
const { bindDn, bindPassword, provider } = this.config;
return new Promise((resolve, reject) => {
// Healthcheck bind user
this.bindClient.search(bindDn, {}, (err: Error | null, res: SearchCallbackResponse) => {
if (err) {
reject(handleError(err));
return;
}
res.on('searchEntry', () => {
resolve();
});
res.on('error', (err: Error) => {
if (!(err instanceof OperationsError)) {
reject(handleError(err));
return;
}
// Rebind on OperationsError
this.bindClient.bind(bindDn, bindPassword, (err: Error | null) => {
if (err) {
const error = handleError(err);
if (error instanceof InvalidCredentialsException) {
reject(new InvalidConfigException('Invalid bind user', { provider }));
} else {
reject(error);
}
} else {
resolve();
}
});
});
});
});
}
private async fetchUserDn(identifier: string): Promise<string | undefined> {
const { userDn, userAttribute } = this.config;
const client = await this.bindClient;
return new Promise((resolve, reject) => {
// Search for the user in LDAP by attribute
client.search(
this.bindClient.search(
userDn,
{
attributes: ['cn'],
@@ -119,12 +137,11 @@ export class LDAPAuthDriver extends AuthDriver {
}
private async fetchUserInfo(userDn: string): Promise<UserInfo | undefined> {
const client = await this.bindClient;
const { mailAttribute } = this.config;
return new Promise((resolve, reject) => {
// Fetch user info in LDAP by domain component
client.search(
this.bindClient.search(
userDn,
{ attributes: ['givenName', 'sn', mailAttribute ?? 'mail', 'userAccountControl'] },
(err: Error | null, res: SearchCallbackResponse) => {
@@ -166,13 +183,11 @@ export class LDAPAuthDriver extends AuthDriver {
return Promise.resolve([]);
}
const client = await this.bindClient;
return new Promise((resolve, reject) => {
let userGroups: string[] = [];
// Search for the user info in LDAP by group attribute
client.search(
this.bindClient.search(
groupDn,
{
attributes: ['cn'],
@@ -220,6 +235,8 @@ export class LDAPAuthDriver extends AuthDriver {
throw new InvalidCredentialsException();
}
await this.validateBindClient();
const userDn = await this.fetchUserDn(payload.identifier);
if (!userDn) {
@@ -299,6 +316,8 @@ export class LDAPAuthDriver extends AuthDriver {
}
async refresh(user: User): Promise<SessionData> {
await this.validateBindClient();
const userInfo = await this.fetchUserInfo(user.external_identifier!);
if (userInfo?.userAccountControl && userInfo.userAccountControl & INVALID_ACCOUNT_FLAGS) {