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,24 +1,32 @@
import { GoogleLDAPAuth } from './auth/google-ldap';
import { UDPServer } from './server/UDPServer';
import { RadiusService } from './radius/RadiusService';
import * as config from '../config';
import { Authentication } from './auth';
import { IAuthentication } from './types/Authentication';
console.log(`Listener Port: ${config.port || 1812}`);
console.log(`RADIUS Secret: ${config.secret}`);
console.log(`Auth Mode: ${config.authentication}`);
// const ldap = new LDAPAuth({url: 'ldap://ldap.google.com', base: 'dc=hokify,dc=com', uid: 'uid', tlsOptions});
const ldap = new GoogleLDAPAuth(
config.authenticationOptions.url,
config.authenticationOptions.base
);
const server = new UDPServer(config.port);
const radiusService = new RadiusService(config.secret, ldap);
(async () => {
/* configure auth mechansim */
let auth: IAuthentication;
try {
const AuthMechanismus = (await import(`./auth/${config.authentication}`))[
config.authentication
];
auth = new AuthMechanismus(config.authenticationOptions);
} catch (err) {
console.error('cannot load auth mechanismus', config.authentication);
throw err;
}
// start radius server
const authentication = new Authentication(auth);
const server = new UDPServer(config.port);
const radiusService = new RadiusService(config.secret, authentication);
server.on('message', async (msg, rinfo) => {
const response = await radiusService.handleMessage(msg);

25
src/auth.ts Normal file
View File

@@ -0,0 +1,25 @@
import * as NodeCache from 'node-cache';
import { IAuthentication } from './types/Authentication';
/**
* this is just a simple abstraction to provide
* an application layer for caching credentials
*/
export class Authentication implements IAuthentication {
cache = new NodeCache();
constructor(private authenticator: IAuthentication) {}
async authenticate(username: string, password: string): Promise<boolean> {
const cacheKey = `usr:${username}|pwd:${password}`;
const fromCache = this.cache.get(cacheKey) as undefined | boolean;
if (fromCache !== undefined) {
return fromCache;
}
const authResult = await this.authenticator.authenticate(username, password);
this.cache.set(cacheKey, authResult, 86400); // cache for one day
return authResult;
}
}

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
);
}
}

View File

@@ -1,3 +1,6 @@
// https://tools.ietf.org/html/rfc5281 TTLS v0
// https://tools.ietf.org/html/draft-funk-eap-ttls-v1-00 TTLS v1 (not implemented)
/* eslint-disable no-bitwise */
import * as tls from 'tls';
import * as NodeCache from 'node-cache';

View File

@@ -38,7 +38,7 @@ export class UDPServer extends events.EventEmitter implements IServer {
// retry up to MAX_RETRIES to send this message,
// we automatically retry if there is no confirmation (=any incoming message from client)
// if expectAcknowledgment (e.g. Access-Accept or Access-Reject) is set, we do not retry
// if expectAcknowledgment is false (e.g. Access-Accept or Access-Reject), we do not retry
const identifierForRetry = `${address}:${port}`;
if (expectAcknowledgment && retried < UDPServer.MAX_RETRIES) {
this.timeout[identifierForRetry] = setTimeout(sendResponse, 600 * (retried + 1));