You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

142 lines
3.4 KiB

import { Client, createClient } from 'ldapjs';
import debug from 'debug';
import * as tls from 'tls';
import { IAuthentication } from '../types/Authentication';
4 years ago
const usernameFields = ['posixUid', 'mail'];
const log = debug('radius:auth:google-ldap');
4 years ago
// TLS:
// https://github.com/ldapjs/node-ldapjs/issues/307
interface IGoogleLDAPAuthOptions {
/** base DN
* e.g. 'dc=hokify,dc=com', */
base: string;
/** tls options
* e.g. {
key: fs.readFileSync('ldap.gsuite.hokify.com.40567.key'),
cert: fs.readFileSync('ldap.gsuite.hokify.com.40567.crt')
} */
tlsOptions: tls.TlsOptions;
}
export class GoogleLDAPAuth implements IAuthentication {
private ldap: Client;
private lastDNsFetch: Date;
4 years ago
private allValidDNsCache: { [key: string]: string };
4 years ago
private base: string;
4 years ago
constructor(config: IGoogleLDAPAuthOptions) {
this.base = config.base;
4 years ago
this.ldap = createClient({
url: 'ldaps://ldap.google.com:636',
tlsOptions: {
...config.tlsOptions,
servername: 'ldap.google.com'
}
}).on('error', error => {
4 years ago
console.error('Error in ldap', error);
});
this.fetchDNs();
}
private async fetchDNs() {
4 years ago
const dns: { [key: string]: string } = {};
await new Promise((resolve, reject) => {
this.ldap.search(
this.base,
{
scope: 'sub'
},
(err, res) => {
if (err) {
reject(err);
return;
}
res.on('searchEntry', function(entry) {
// log('entry: ' + JSON.stringify(entry.object));
4 years ago
usernameFields.forEach(field => {
const index = entry.object[field] as string;
dns[index] = entry.object.dn;
});
});
res.on('searchReference', function(referral) {
log(`referral: ${referral.uris.join()}`);
4 years ago
});
res.on('error', function(ldapErr) {
console.error(`error: ${ldapErr.message}`);
reject();
});
res.on('end', result => {
log(`ldap status: ${result?.status}`);
4 years ago
// replace with new dns
this.allValidDNsCache = dns;
// log('allValidDNsCache', this.allValidDNsCache);
4 years ago
resolve();
});
}
);
});
this.lastDNsFetch = new Date();
}
async authenticate(username: string, password: string, count = 0, forceFetching = false) {
const cacheValidTime = new Date();
cacheValidTime.setHours(cacheValidTime.getHours() - 12);
let dnsFetched = false;
if (!this.lastDNsFetch || this.lastDNsFetch < cacheValidTime || forceFetching) {
log('fetching dns');
4 years ago
await this.fetchDNs();
dnsFetched = true;
}
if (count > 5) {
throw new Error('Failed to authenticate with LDAP!');
}
// const dn = ;
const dn = this.allValidDNsCache[username];
if (!dn) {
if (!dnsFetched && !forceFetching) {
return this.authenticate(username, password, count, true);
}
console.error(`invalid username, not found in DN: ${username}`, this.allValidDNsCache);
return false;
4 years ago
}
const authResult: boolean = await new Promise((resolve, reject) => {
4 years ago
this.ldap.bind(dn, password, (err, res) => {
if (err) {
if (err && (err as any).stack && (err as any).stack.includes(`ldap.google.com closed`)) {
4 years ago
count++;
// wait 1 second to give the ldap error handler time to reconnect
setTimeout(() => resolve(this.authenticate(dn, password)), 2000);
return;
}
resolve(false);
// console.error('ldap error', err);
// reject(err);
4 years ago
}
if (res) resolve(res);
else reject();
});
});
return !!authResult;
4 years ago
}
}