feat: add more auth providers and cleanup google auth
it's no longer needed to use stunnel ;-)
This commit is contained in:
30
src/app.ts
30
src/app.ts
@@ -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
25
src/auth.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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
64
src/auth/IMAPAuth.ts
Normal 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
57
src/auth/LDAPAuth.ts
Normal 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
68
src/auth/SMTPAuth.ts
Normal 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
22
src/auth/StaticAuth.ts
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user