feat: add more auth providers and cleanup google auth

it's no longer needed to use stunnel ;-)
This commit is contained in:
simon
2020-02-23 20:42:36 +01:00
parent 5e5005cf6b
commit 3f600c664f
22 changed files with 1351 additions and 152 deletions

View File

@@ -1,26 +1,45 @@
import * as NodeCache from 'node-cache';
import { Client, createClient } from 'ldapjs';
import debug from 'debug';
import * as tls from 'tls';
import { IAuthentication } from '../types/Authentication';
const usernameFields = ['posixUid', 'mail'];
const log = debug('radius:auth:ldap');
const log = debug('radius:auth:google-ldap');
// 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 {
cache = new NodeCache();
private ldap: Client;
ldap: Client;
private lastDNsFetch: Date;
lastDNsFetch: Date;
private allValidDNsCache: { [key: string]: string };
allValidDNsCache: { [key: string]: string };
private base: string;
constructor(private url: string, private base: string, tlsOptions?) {
this.ldap = createClient({ url, tlsOptions }).on('error', error => {
constructor(config: IGoogleLDAPAuthOptions) {
this.base = config.base;
this.ldap = createClient({
url: 'ldaps://ldap.google.com:636',
tlsOptions: {
...config.tlsOptions,
servername: 'ldap.google.com'
}
}).on('error', error => {
console.error('Error in ldap', error);
});
@@ -74,12 +93,6 @@ export class GoogleLDAPAuth implements IAuthentication {
}
async authenticate(username: string, password: string, count = 0, forceFetching = false) {
const cacheKey = `usr:${username}|pwd:${password}`;
const fromCache = this.cache.get(cacheKey);
if (fromCache !== undefined) {
return fromCache;
}
const cacheValidTime = new Date();
cacheValidTime.setHours(cacheValidTime.getHours() - 12);
@@ -100,14 +113,14 @@ export class GoogleLDAPAuth implements IAuthentication {
if (!dnsFetched && !forceFetching) {
return this.authenticate(username, password, count, true);
}
console.error(`invalid username, not found in DN: ${username}`);
console.error(`invalid username, not found in DN: ${username}`, this.allValidDNsCache);
return false;
}
const authResult: boolean = await new Promise((resolve, reject) => {
this.ldap.bind(dn, password, (err, res) => {
if (err) {
if (err && (err as any).stack && (err as any).stack.includes(`${this.url} closed`)) {
if (err && (err as any).stack && (err as any).stack.includes(`ldap.google.com closed`)) {
count++;
// wait 1 second to give the ldap error handler time to reconnect
setTimeout(() => resolve(this.authenticate(dn, password)), 2000);
@@ -123,8 +136,6 @@ export class GoogleLDAPAuth implements IAuthentication {
});
});
this.cache.set(cacheKey, authResult, 86400);
return authResult;
return !!authResult;
}
}

64
src/auth/IMAPAuth.ts Normal file
View File

@@ -0,0 +1,64 @@
import * as imaps from 'imap-simple';
import { IAuthentication } from '../types/Authentication';
interface IIMAPAuthOptions {
host: string;
port?: number;
useSecureTransport?: boolean;
validHosts?: string[];
}
export class IMAPAuth implements IAuthentication {
private host: string;
private port = 143;
private useSecureTransport = false;
private validHosts?: string[];
constructor(config: IIMAPAuthOptions) {
this.host = config.host;
if (config.port !== undefined) {
this.port = config.port;
}
if (config.useSecureTransport !== undefined) {
this.useSecureTransport = config.useSecureTransport;
}
if (config.validHosts !== undefined) {
this.validHosts = config.validHosts;
}
}
async authenticate(username: string, password: string) {
if (this.validHosts) {
const domain = username.split('@').pop();
if (!domain || !this.validHosts.includes(domain)) {
console.info('invalid or no domain in username', username, domain);
return false;
}
}
let success = false;
try {
const connection = await imaps.connect({
imap: {
host: this.host,
port: this.port,
tls: this.useSecureTransport,
user: username,
password,
tlsOptions: {
servername: this.host // SNI (needs to be set for gmail)
}
}
});
success = true;
connection.end();
} catch (err) {
console.error('imap auth failed', err);
}
return success;
}
}

57
src/auth/LDAPAuth.ts Normal file
View File

@@ -0,0 +1,57 @@
import * as LdapAuth from 'ldapauth-fork';
import { IAuthentication } from '../types/Authentication';
interface ILDAPAuthOptions {
/** ldap url
* e.g. ldaps://ldap.google.com
*/
url: string;
/** 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'),
servername: 'ldap.google.com'
} */
tlsOptions?: any;
/**
* searchFilter
*/
searchFilter?: string;
}
export class LDAPAuth implements IAuthentication {
private ldap: LdapAuth;
constructor(options: ILDAPAuthOptions) {
this.ldap = new LdapAuth({
url: options.url,
searchBase: options.base,
tlsOptions: options.tlsOptions,
searchFilter: options.searchFilter || '(uid={{username}})',
reconnect: true
});
this.ldap.on('error', function(err) {
console.error('LdapAuth: ', err);
});
}
async authenticate(username: string, password: string) {
// console.log('AUTH', this.ldap);
const authResult: boolean = await new Promise((resolve, reject) => {
this.ldap.authenticate(username, password, function(err, user) {
if (err) {
resolve(false);
console.error('ldap error', err);
// reject(err);
}
if (user) resolve(user);
else reject();
});
});
return !!authResult;
}
}

68
src/auth/SMTPAuth.ts Normal file
View File

@@ -0,0 +1,68 @@
import { SMTPClient } from 'smtp-client';
import { IAuthentication } from '../types/Authentication';
interface ISMTPAuthOptions {
host: string;
port?: number;
useSecureTransport?: boolean;
validHosts?: string[];
}
export class SMTPAuth implements IAuthentication {
private host: string;
private port = 25;
private useSecureTransport = false;
private validHosts?: string[];
constructor(options: ISMTPAuthOptions) {
this.host = options.host;
if (options.port !== undefined) {
this.port = options.port;
}
if (options.useSecureTransport !== undefined) {
this.useSecureTransport = options.useSecureTransport;
}
if (options.validHosts !== undefined) {
this.validHosts = options.validHosts;
}
}
async authenticate(username: string, password: string) {
if (this.validHosts) {
const domain = username.split('@').pop();
if (!domain || !this.validHosts.includes(domain)) {
console.info('invalid or no domain in username', username, domain);
return false;
}
}
const s = new SMTPClient({
host: this.host,
port: this.port,
secure: this.useSecureTransport,
tlsOptions: {
servername: this.host // SNI (needs to be set for gmail)
}
});
let success = false;
try {
await s.connect();
await s.greet({ hostname: 'mx.domain.com' }); // runs EHLO command or HELO as a fallback
await s.authPlain({ username, password }); // authenticates a user
success = true;
s.close(); // runs QUIT command
} catch (err) {
console.error('imap auth failed', err);
}
return success;
}
}

22
src/auth/StaticAuth.ts Normal file
View File

@@ -0,0 +1,22 @@
import { IAuthentication } from '../types/Authentication';
interface IStaticAuthOtions {
validCrentials: {
username: string;
password: string;
}[];
}
export class StaticAuth implements IAuthentication {
private validCredentials: { username: string; password: string }[];
constructor(options: IStaticAuthOtions) {
this.validCredentials = options.validCrentials;
}
async authenticate(username: string, password: string) {
return !!this.validCredentials.find(
credential => credential.username === username && credential.password === password
);
}
}