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

112
README.md
View File

@ -14,6 +14,33 @@ This is a first implementation draft, which is currently only working with a nod
CONTRIBUTIONS WELCOME! If you are willing to help, just open a PR or contact me via bug system or simon.tretter@hokify.com. CONTRIBUTIONS WELCOME! If you are willing to help, just open a PR or contact me via bug system or simon.tretter@hokify.com.
## Motivation
### Why not Freeradius?
There are several reasons why I started implementing this radius server in node js. We are using
freeradius right now, but have several issues which are hard to tackle due to the reason that freeradius
is a complex software and supports many uses cases. It is also written in C++ and uses threads behind the scene.
Therefore it's not easy to extend or modify it, or even bring new feature in.
The idea of this project is to make a super simple node radius server, which is async by default. No complex
thread handling, no other fancy thing. The basic goal is to make WPA2 authenticiation easy again.
### 802.11x protocol in node
Another motivation is that it is very exciting to see how wireless protocols have evolved, and see
how a implementation like TTLS works.
### Few alternatives (only non-free ones like Jumpcloud...)
Furthermore there are few alternatives out there, e.g. jumpcloud is non-free and I couldn't find many others.
### Vision
As soon as I understood the TTLS PAP Tunnel approach, I had this vision of making Wlan Authentification easy
for everyone. Why limit it to something "complex" like LDAP and co. This library aims to make it easy for everyone
to implement either their own authentication mechanismus (e.g. against a database), or provides some mechansimns
out of the box (e.g. imap, static, ldap,..).
## Installation ## Installation
npm install npm install
@ -28,7 +55,7 @@ you need:
2. Optional: Create your own SSL certificate (e.g. self signed via npm run create-certificate) 2. Optional: Create your own SSL certificate (e.g. self signed via npm run create-certificate)
3. Check config.js and adapt to your needs 3. Check config.js and adapt to your needs
- configure authentication (passport config), e.g. for LDAP - configure authentication e.g. for LDAP
```js ```js
var config = { var config = {
@ -46,15 +73,86 @@ var config = {
4. Install und build server: npm install && npm run build 4. Install und build server: npm install && npm run build
5. Start server "npm run start" 5. Start server "npm run start"
## Authentications ## Configuration
right now only one simple ldap implementation is done, see config.js in root
the idea is though to use [passport.js](http://www.passportjs.org/) as authentication provider,
therefore it would be possible to use the radius server with your email provider authentication or any
other auth mechanismus you use (well everything with no 2factor or anything else that requries an extra step).
### Authentications
#### Google LDAP
google ldap optimized authenticiation implementaiton
#### LDAP
ldap authentication
```typescript
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;
}
```
#### IMAP
imap authenticiation
```typescript
interface IIMAPAuthOptions {
host: string;
port?: number;
useSecureTransport?: boolean;
validHosts?: string[];
}
```
#### SMTP
smtp authenticiation
```typescript
interface ISMTPAuthOptions {
host: string;
port?: number;
useSecureTransport?: boolean;
validHosts?: string[];
}
```
#### Static Auth
static authenticiation
```typescript
interface IStaticAuthOtions {
validCrentials: {
username: string;
password: string;
}[];
}
```
## Usage ## Usage
You need to specify at least a radius password and the base DN for LDAP: Ensure you have installed latest node version and run:
npm run start npm run start

View File

@ -0,0 +1,21 @@
import 'mocha';
import { expect } from 'chai';
import * as fs from 'fs';
import { GoogleLDAPAuth } from '../../src/auth/GoogleLDAPAuth';
describe('test google ldap auth', function() {
this.timeout(10000);
it('authenticate against ldap server', async () => {
const auth = new GoogleLDAPAuth({
base: 'dc=hokify,dc=com',
tlsOptions: {
key: fs.readFileSync('./ldap.gsuite.hokify.com.40567.key'),
cert: fs.readFileSync('./ldap.gsuite.hokify.com.40567.crt')
}
});
const result = await auth.authenticate('username', 'password');
expect(result).to.equal(true);
});
});

View File

@ -0,0 +1,18 @@
import 'mocha';
import { expect } from 'chai';
import { IMAPAuth } from '../../src/auth/IMAPAuth';
describe('test imap auth', () => {
it('authenticate against imap server', async () => {
const auth = new IMAPAuth({
host: 'imap.gmail.com',
port: 993,
useSecureTransport: true,
validHosts: ['gmail.com']
});
const result = await auth.authenticate('username', 'password');
expect(result).to.equal(true);
});
});

View File

@ -0,0 +1,23 @@
import 'mocha';
import { expect } from 'chai';
import * as fs from 'fs';
import { LDAPAuth } from '../../src/auth/LDAPAuth';
describe('test ldap auth', function() {
this.timeout(10000);
it('authenticate against ldap server', async () => {
const auth = new LDAPAuth({
url: 'ldaps://ldap.google.com:636',
base: 'dc=hokify,dc=com',
tlsOptions: {
servername: 'ldap.google.com',
key: fs.readFileSync('./ldap.gsuite.hokify.com.40567.key'),
cert: fs.readFileSync('./ldap.gsuite.hokify.com.40567.crt'),
}
});
const result = await auth.authenticate('username', 'password');
expect(result).to.equal(true);
});
});

View File

@ -0,0 +1,18 @@
import 'mocha';
import { expect } from 'chai';
import { SMTPAuth } from '../../src/auth/SMTPAuth';
describe('test smtp auth', () => {
it('authenticate against smtp server', async () => {
const auth = new SMTPAuth({
host: 'smtp.gmail.com',
port: 465,
useSecureTransport: true,
validHosts: ['gmail.com']
});
const result = await auth.authenticate('username', 'password');
expect(result).to.equal(true);
});
});

7
__tests__/tsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
},
"include": ["__tests__/*.ts", "*.ts", "../src/**/*.ts"]
}

View File

@ -19,20 +19,46 @@ module.exports = {
] ]
}, },
// authentication // GoogleLDAPAuth (optimized for google auth)
authentication: 'ldap', authentication: 'GoogleLDAPAuth',
authenticationOptions: { authenticationOptions: {
url: 'ldap://127.0.0.1:1636',
base: 'dc=hokify,dc=com', base: 'dc=hokify,dc=com',
tlsOptions2: { tlsOptions: {
key: fs.readFileSync('ldap.gsuite.hokify.com.40567.key'),
cert: fs.readFileSync('ldap.gsuite.hokify.com.40567.crt')
}
}
/** LDAP AUTH
authentication: 'LDAPAuth',
authenticationOptions: {
url: 'ldaps://ldap.google.com',
base: 'dc=hokify,dc=com',
tlsOptions: {
key: fs.readFileSync('ldap.gsuite.hokify.com.40567.key'), key: fs.readFileSync('ldap.gsuite.hokify.com.40567.key'),
cert: fs.readFileSync('ldap.gsuite.hokify.com.40567.crt'), cert: fs.readFileSync('ldap.gsuite.hokify.com.40567.crt'),
servername: 'ldap.google.com'
// This is necessary only if using the client certificate authentication.
requestCert: true,
// This is necessary only if the client uses the self-signed certificate.
ca: [fs.readFileSync('ldap.gsuite.hokify.com.40567.key')]
} }
} }
*/
/** IMAP AUTH
authentication: 'IMAPAuth',
authenticationOptions: {
host: 'imap.gmail.com',
port: 993,
useSecureTransport: true,
validHosts: ['hokify.com']
}
*/
/** SMTP AUTH
authentication: 'IMAPAuth',
authenticationOptions: {
host: 'smtp.gmail.com',
port: 465,
useSecureTransport: true,
validHosts: ['gmail.com']
}
*/
}; };

899
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,34 +1,41 @@
{ {
"name": "radius-server", "name": "radius-server",
"description": "radius server for google LDAP and TTLT", "description": "radius server for google LDAP and TTLS",
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"debug": "DEBUG=radius:* ../node/node dist/app.js", "debug": "DEBUG=radius:* ../node/node dist/app.js",
"start": "../node/node dist/app.js", "start": "../node/node dist/app.js",
"build": "tsc", "build": "tsc",
"dev": "ts-node src/app.ts", "dev": "ts-node src/app.ts",
"test-ttls-pap": "tests/eapol_test -c tests/ttls-pap.conf -s testing123", "test": "mocha -r ts-node/register __tests__/**/*.test.ts",
"test-ttls-pap": "__tests__/eapol_test -c __tests__/ttls-pap.conf -s testing123",
"test-radtest": "radtest -x user pwd localhost 1812 testing123", "test-radtest": "radtest -x user pwd localhost 1812 testing123",
"create-certificate": "sh ./ssl/create.sh && sh ./ssl/sign.sh" "create-certificate": "sh ./ssl/create.sh && sh ./ssl/sign.sh"
}, },
"dependencies": { "dependencies": {
"debug": "^4.1.1", "debug": "^4.1.1",
"imap-simple": "^4.3.0",
"ldapauth-fork": "^4.3.1",
"ldapjs": "^1.0.2", "ldapjs": "^1.0.2",
"md5": "^2.2.1", "md5": "^2.2.1",
"native-duplexpair": "^1.0.0", "native-duplexpair": "^1.0.0",
"node-cache": "^5.1.0", "node-cache": "^5.1.0",
"passport-ldapauth": "^2.1.3",
"radius": "~1.1.4", "radius": "~1.1.4",
"ts-node": "^8.6.2", "ts-node": "^8.6.2",
"type-cacheable": "^4.0.0", "type-cacheable": "^4.0.0",
"yargs": "~15.1.0" "yargs": "~15.1.0",
"smtp-client": "^0.3.1"
}, },
"license": "GPLv3", "license": "GPLv3",
"devDependencies": { "devDependencies": {
"@types/ldapjs": "^1.0.5",
"@types/radius": "0.0.28",
"@hokify/eslint-config": "^0.2.4", "@hokify/eslint-config": "^0.2.4",
"@types/chai": "^4.2.9",
"@types/ldapjs": "^1.0.5",
"@types/mocha": "^7.0.1",
"@types/radius": "0.0.28",
"chai": "^4.2.0",
"eslint": "^6.8.0", "eslint": "^6.8.0",
"mocha": "^7.0.1",
"prettier": "^1.19.1", "prettier": "^1.19.1",
"typescript": "^3.8.2" "typescript": "^3.8.2"
} }

View File

@ -1,24 +1,32 @@
import { GoogleLDAPAuth } from './auth/google-ldap';
import { UDPServer } from './server/UDPServer'; import { UDPServer } from './server/UDPServer';
import { RadiusService } from './radius/RadiusService'; import { RadiusService } from './radius/RadiusService';
import * as config from '../config'; import * as config from '../config';
import { Authentication } from './auth';
import { IAuthentication } from './types/Authentication';
console.log(`Listener Port: ${config.port || 1812}`); console.log(`Listener Port: ${config.port || 1812}`);
console.log(`RADIUS Secret: ${config.secret}`); console.log(`RADIUS Secret: ${config.secret}`);
console.log(`Auth Mode: ${config.authentication}`); console.log(`Auth Mode: ${config.authentication}`);
// const ldap = new LDAPAuth({url: 'ldap://ldap.google.com', base: 'dc=hokify,dc=com', uid: 'uid', tlsOptions}); (async () => {
/* configure auth mechansim */
const ldap = new GoogleLDAPAuth( let auth: IAuthentication;
config.authenticationOptions.url, try {
config.authenticationOptions.base 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 server = new UDPServer(config.port);
const radiusService = new RadiusService(config.secret, ldap); const radiusService = new RadiusService(config.secret, authentication);
(async () => {
server.on('message', async (msg, rinfo) => { server.on('message', async (msg, rinfo) => {
const response = await radiusService.handleMessage(msg); 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 { Client, createClient } from 'ldapjs';
import debug from 'debug'; import debug from 'debug';
import * as tls from 'tls';
import { IAuthentication } from '../types/Authentication'; import { IAuthentication } from '../types/Authentication';
const usernameFields = ['posixUid', 'mail']; const usernameFields = ['posixUid', 'mail'];
const log = debug('radius:auth:ldap'); const log = debug('radius:auth:google-ldap');
// TLS: // TLS:
// https://github.com/ldapjs/node-ldapjs/issues/307 // 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 { 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?) { constructor(config: IGoogleLDAPAuthOptions) {
this.ldap = createClient({ url, tlsOptions }).on('error', error => { 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); 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) { 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(); const cacheValidTime = new Date();
cacheValidTime.setHours(cacheValidTime.getHours() - 12); cacheValidTime.setHours(cacheValidTime.getHours() - 12);
@ -100,14 +113,14 @@ export class GoogleLDAPAuth implements IAuthentication {
if (!dnsFetched && !forceFetching) { if (!dnsFetched && !forceFetching) {
return this.authenticate(username, password, count, true); 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; return false;
} }
const authResult: boolean = await new Promise((resolve, reject) => { const authResult: boolean = await new Promise((resolve, reject) => {
this.ldap.bind(dn, password, (err, res) => { this.ldap.bind(dn, password, (err, res) => {
if (err) { 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++; count++;
// wait 1 second to give the ldap error handler time to reconnect // wait 1 second to give the ldap error handler time to reconnect
setTimeout(() => resolve(this.authenticate(dn, password)), 2000); 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 */ /* eslint-disable no-bitwise */
import * as tls from 'tls'; import * as tls from 'tls';
import * as NodeCache from 'node-cache'; 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, // retry up to MAX_RETRIES to send this message,
// we automatically retry if there is no confirmation (=any incoming message from client) // 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}`; const identifierForRetry = `${address}:${port}`;
if (expectAcknowledgment && retried < UDPServer.MAX_RETRIES) { if (expectAcknowledgment && retried < UDPServer.MAX_RETRIES) {
this.timeout[identifierForRetry] = setTimeout(sendResponse, 600 * (retried + 1)); this.timeout[identifierForRetry] = setTimeout(sendResponse, 600 * (retried + 1));

View File

@ -1,9 +1,7 @@
{ {
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"include": [ "include": ["src/**/*.ts", "*.js", "*.ts", "__tests__/**/*.ts"],
"src/**/*.ts", "exclude": ["node_modules"],
"*.js"
],
"compilerOptions": { "compilerOptions": {
"allowJs": true, "allowJs": true,
"checkJs": true "checkJs": true