├── ssl ├── db │ ├── index.txt.old │ ├── serial │ ├── serial.old │ ├── index.txt.attr │ └── index.txt ├── README.md ├── create.sh ├── xpextensions ├── cert │ ├── server.csr │ ├── ca.pem │ ├── ca.key │ ├── server.key │ ├── 00.pem │ └── server.crt ├── server.cnf └── ca.cnf ├── __tests__ ├── .gitignore ├── eapol_test ├── tsconfig.json ├── ttls-pap.conf └── auth │ ├── imap.test.ts │ ├── smtp.test.ts │ ├── google-ldap.test.ts │ └── ldap.test.ts ├── bin └── radius-server ├── .prettierrc ├── src ├── types │ ├── Authentication.ts │ ├── EAPMethod.ts │ ├── PacketHandler.ts │ └── Server.ts ├── auth │ ├── StaticAuth.ts │ ├── LDAPAuth.ts │ ├── IMAPAuth.ts │ ├── SMTPAuth.ts │ ├── README.md │ └── GoogleLDAPAuth.ts ├── radius │ ├── handler │ │ ├── eap │ │ │ ├── eapMethods │ │ │ │ ├── EAP-MD5.ts │ │ │ │ ├── EAP-GTC.ts │ │ │ │ └── EAP-TTLS.ts │ │ │ └── EAPHelper.ts │ │ ├── UserPasswordPacketHandler.ts │ │ └── EAPPacketHandler.ts │ ├── PacketHandler.ts │ └── RadiusService.ts ├── helpers.ts ├── auth.ts ├── app.ts ├── server │ └── UDPServer.ts └── tls │ └── crypt.ts ├── notes ├── .eslintrc.js ├── .gitignore ├── tsconfig.eslint.json ├── tsconfig.json ├── config.js ├── package.json ├── README.md └── CHANGELOG.md /ssl/db/index.txt.old: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ssl/db/serial: -------------------------------------------------------------------------------- 1 | 01 2 | -------------------------------------------------------------------------------- /ssl/db/serial.old: -------------------------------------------------------------------------------- 1 | 00 2 | -------------------------------------------------------------------------------- /__tests__/.gitignore: -------------------------------------------------------------------------------- 1 | *hokify* 2 | -------------------------------------------------------------------------------- /ssl/db/index.txt.attr: -------------------------------------------------------------------------------- 1 | unique_subject = yes 2 | -------------------------------------------------------------------------------- /bin/radius-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | require('../dist/app.js'); 6 | -------------------------------------------------------------------------------- /__tests__/eapol_test: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afgarcia86/node-radius-server/master/__tests__/eapol_test -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "useTabs": true, 5 | "endOfLine": "lf" 6 | } 7 | -------------------------------------------------------------------------------- /ssl/db/index.txt: -------------------------------------------------------------------------------- 1 | V 360727170538Z 00 unknown /C=AT/ST=Radius/O=Example Inc./CN=Example Certificate Authority/emailAddress=admin@example.org 2 | -------------------------------------------------------------------------------- /src/types/Authentication.ts: -------------------------------------------------------------------------------- 1 | export interface IAuthentication { 2 | authenticate(username: string, password: string): Promise; 3 | } 4 | -------------------------------------------------------------------------------- /notes: -------------------------------------------------------------------------------- 1 | https://github.com/retailnext/node-radius/issues/29 2 | 3 | https://stackoverflow.com/questions/60232165/ssl-export-keying-material-in-node-js 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['@hokify/eslint-config'], 4 | parserOptions: { 5 | project: './tsconfig.eslint.json' 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | .DS_Store 3 | 4 | # build files 5 | dist 6 | 7 | # ts 8 | tsconfig.tsbuildinfo 9 | 10 | # custom certificates 11 | /ssl-*/ 12 | -------------------------------------------------------------------------------- /__tests__/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | 5 | }, 6 | "include": ["__tests__/*.ts", "*.ts", "../src/**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*.ts", "*.js", "*.ts", "__tests__/**/*.ts"], 4 | "exclude": ["node_modules"], 5 | "compilerOptions": { 6 | "allowJs": true, 7 | "checkJs": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ssl/README.md: -------------------------------------------------------------------------------- 1 | this is based on freeradius cert directory :) 2 | 3 | there is a default certificate already present, if you want to create your own, follow these steps: 4 | 5 | 1. edit ca.cnf 6 | 2. edit server.cnf 7 | 3. replace your choosen pwd (deafult is whatever2020) in create.sh 8 | 4. run ./create.sh 9 | 5. set your choosen pwd in ~/config.js 10 | 11 | -------------------------------------------------------------------------------- /__tests__/ttls-pap.conf: -------------------------------------------------------------------------------- 1 | # 2 | # eapol_test -c ttls-pap.conf -s testing123 3 | # 4 | network={ 5 | ssid="example" 6 | key_mgmt=WPA-EAP 7 | eap=TTLS 8 | identity="user" 9 | anonymous_identity="anonymous" 10 | password="pwd" 11 | phase2="auth=PAP" 12 | 13 | # 14 | # Uncomment the following to perform server certificate validation. 15 | ca_cert="./ssl/cert/ca.pem" 16 | } 17 | -------------------------------------------------------------------------------- /src/types/EAPMethod.ts: -------------------------------------------------------------------------------- 1 | import { IPacket, IPacketHandlerResult } from './PacketHandler'; 2 | 3 | export interface IEAPMethod { 4 | getEAPType(): number; 5 | 6 | identify(identifier: number, stateID: string, msg?: Buffer): IPacketHandlerResult; 7 | 8 | handleMessage( 9 | identifier: number, 10 | stateID: string, 11 | msg: Buffer, 12 | packet?: IPacket, 13 | identity?: string 14 | ): Promise; 15 | } 16 | -------------------------------------------------------------------------------- /__tests__/auth/imap.test.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | import { expect } from 'chai'; 3 | import { IMAPAuth } from '../../src/auth/IMAPAuth'; 4 | 5 | describe('test imap auth', () => { 6 | it('authenticate against imap server', async () => { 7 | const auth = new IMAPAuth({ 8 | host: 'imap.gmail.com', 9 | port: 993, 10 | useSecureTransport: true, 11 | validHosts: ['gmail.com'], 12 | }); 13 | 14 | const result = await auth.authenticate('username', 'password'); 15 | 16 | expect(result).to.equal(true); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /__tests__/auth/smtp.test.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | import { expect } from 'chai'; 3 | import { SMTPAuth } from '../../src/auth/SMTPAuth'; 4 | 5 | describe('test smtp auth', () => { 6 | it('authenticate against smtp server', async () => { 7 | const auth = new SMTPAuth({ 8 | host: 'smtp.gmail.com', 9 | port: 465, 10 | useSecureTransport: true, 11 | validHosts: ['gmail.com'], 12 | }); 13 | 14 | const result = await auth.authenticate('username', 'password'); 15 | 16 | expect(result).to.equal(true); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /__tests__/auth/google-ldap.test.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | import { expect } from 'chai'; 3 | import { GoogleLDAPAuth } from '../../src/auth/GoogleLDAPAuth'; 4 | 5 | describe('test google ldap auth', function () { 6 | this.timeout(10000); 7 | it('authenticate against ldap server', async () => { 8 | const auth = new GoogleLDAPAuth({ 9 | base: 'dc=hokify,dc=com', 10 | tls: { 11 | keyFile: './ldap.gsuite.key', 12 | certFile: './ldap.gsuite.crt', 13 | }, 14 | }); 15 | 16 | const result = await auth.authenticate('username', 'password'); 17 | 18 | expect(result).to.equal(true); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /ssl/create.sh: -------------------------------------------------------------------------------- 1 | # generate private key 2 | # openssl genrsa -out csr.key 2048 3 | 4 | # CA 5 | openssl req -new -x509 -keyout cert/ca.key -out cert/ca.pem -days 3600 -config ./ca.cnf 6 | 7 | # server 8 | openssl req -new -out cert/server.csr -keyout cert/server.key -config ./server.cnf 9 | 10 | # sign it 11 | # -key $(PASSWORD_CA) (default pwd is whatever2020) 12 | openssl ca -batch -keyfile cert/ca.key -cert cert/ca.pem -in cert/server.csr -key whatever2020 -out cert/server.crt -extensions xpserver_ext -extfile xpextensions -config ./server.cnf 13 | 14 | # sign it 15 | # openssl x509 -req -in csr.pem -signkey private-key.pem -out public-cert.pem 16 | -------------------------------------------------------------------------------- /src/auth/StaticAuth.ts: -------------------------------------------------------------------------------- 1 | import { IAuthentication } from '../types/Authentication'; 2 | 3 | interface IStaticAuthOtions { 4 | validCrentials: { 5 | username: string; 6 | password: string; 7 | }[]; 8 | } 9 | 10 | export class StaticAuth implements IAuthentication { 11 | private validCredentials: { username: string; password: string }[]; 12 | 13 | constructor(options: IStaticAuthOtions) { 14 | this.validCredentials = options.validCrentials; 15 | } 16 | 17 | async authenticate(username: string, password: string) { 18 | return !!this.validCredentials.find( 19 | (credential) => credential.username === username && credential.password === password 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /__tests__/auth/ldap.test.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | import { expect } from 'chai'; 3 | import { LDAPAuth } from '../../src/auth/LDAPAuth'; 4 | 5 | describe('test ldap auth', function () { 6 | this.timeout(10000); 7 | it('authenticate against ldap server', async () => { 8 | const auth = new LDAPAuth({ 9 | url: 'ldaps://ldap.google.com:636', 10 | base: 'dc=hokify,dc=com', 11 | tls: { 12 | keyFile: './ldap.gsuite.key', 13 | certFile: './ldap.gsuite.crt', 14 | }, 15 | tlsOptions: { 16 | servername: 'ldap.google.com', 17 | }, 18 | }); 19 | 20 | const result = await auth.authenticate('username', 'password'); 21 | 22 | expect(result).to.equal(true); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/types/PacketHandler.ts: -------------------------------------------------------------------------------- 1 | export enum PacketResponseCode { 2 | AccessChallenge = 'Access-Challenge', 3 | AccessAccept = 'Access-Accept', 4 | AccessReject = 'Access-Reject', 5 | } 6 | 7 | export interface IPacketHandlerResult { 8 | code?: PacketResponseCode; 9 | attributes?: [string, Buffer | string][]; 10 | } 11 | 12 | export interface IPacketAttributes { 13 | [key: string]: string | Buffer; 14 | } 15 | 16 | export interface IPacket { 17 | attributes: { [key: string]: string | Buffer }; 18 | authenticator?: Buffer; 19 | } 20 | 21 | export interface IPacketHandler { 22 | /** handlingType is the attreibute ID of the currently processing type (e.g. TTLS, GTC, MD5,..) */ 23 | handlePacket(packet: IPacket, handlingType?: number): Promise; 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "rootDir": "./src", 5 | 6 | // target settings for node js 7 | "module": "commonjs", 8 | "target": "es2018", 9 | "lib": ["es2018"], 10 | 11 | // other best practice configs 12 | "moduleResolution": "node", 13 | "strict": true, 14 | "noImplicitAny": false, // <-- get rid of this! 15 | "removeComments": false, // <-- do not remove comments (needed for @deprecated notices etc) 16 | "emitDecoratorMetadata": true, 17 | "composite": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization": false, 20 | "resolveJsonModule": true, 21 | "sourceMap": true, 22 | "isolatedModules": false, 23 | "declaration": true 24 | }, 25 | "exclude": ["node_modules", "**/__tests__"], 26 | "include": ["./src"] 27 | } 28 | -------------------------------------------------------------------------------- /src/types/Server.ts: -------------------------------------------------------------------------------- 1 | import { RemoteInfo } from 'dgram'; 2 | 3 | /** 4 | * @fires IServer#message 5 | */ 6 | export interface IServer { 7 | /** 8 | * 9 | * @param msg 10 | * @param port 11 | * @param address 12 | * @param callback 13 | * @param expectAcknowledgment: if set to false, message is not retried to send again if there is no confirmation 14 | */ 15 | sendToClient( 16 | msg: string | Uint8Array, 17 | port?: number, 18 | address?: string, 19 | callback?: (error: Error | null, bytes: number) => void, 20 | expectAcknowledgment?: boolean 21 | ): void; 22 | 23 | /** 24 | * Message event. 25 | * 26 | * @event IServer#message 27 | * @type {object} 28 | * @property {message} data - the data of the incoming message 29 | * @property {rinfo} optionally remote information 30 | */ 31 | on(event: 'message', listener: (msg: Buffer, rinfo?: RemoteInfo) => void): this; 32 | } 33 | -------------------------------------------------------------------------------- /ssl/xpextensions: -------------------------------------------------------------------------------- 1 | # 2 | # File containing the OIDs required for Windows. 3 | # 4 | # http://support.microsoft.com/kb/814394/en-us 5 | # 6 | [ xpclient_ext] 7 | extendedKeyUsage = 1.3.6.1.5.5.7.3.2 8 | crlDistributionPoints = URI:http://www.example.com/example_ca.crl 9 | 10 | [ xpserver_ext] 11 | extendedKeyUsage = 1.3.6.1.5.5.7.3.1 12 | crlDistributionPoints = URI:http://www.example.com/example_ca.crl 13 | 14 | # This is the 'Trust Override Disabled - STRICT' policy. 15 | #certificatePolicies = 1.3.6.1.4.1.40808.1.3.1 16 | # This is the 'Trust Override Disabled - TOFU' policy. 17 | 18 | certificatePolicies = 1.3.6.1.4.1.40808.1.3.2 19 | 20 | # 21 | # Add this to the PKCS#7 keybag attributes holding the client's private key 22 | # for machine authentication. 23 | # 24 | # the presence of this OID tells Windows XP that the cert is intended 25 | # for use by the computer itself, and not by an end-user. 26 | # 27 | # The other solution is to use Microsoft's web certificate server 28 | # to generate these certs. 29 | # 30 | # 1.3.6.1.4.1.311.17.2 31 | -------------------------------------------------------------------------------- /src/radius/handler/eap/eapMethods/EAP-MD5.ts: -------------------------------------------------------------------------------- 1 | // https://tools.ietf.org/html/rfc5281 TTLS v0 2 | // https://tools.ietf.org/html/draft-funk-eap-ttls-v1-00 TTLS v1 (not implemented) 3 | /* eslint-disable no-bitwise */ 4 | import { RadiusPacket } from 'radius'; 5 | import debug from 'debug'; 6 | import { IPacketHandlerResult } from '../../../../types/PacketHandler'; 7 | import { IEAPMethod } from '../../../../types/EAPMethod'; 8 | import { IAuthentication } from '../../../../types/Authentication'; 9 | 10 | export class EAPMD5 implements IEAPMethod { 11 | getEAPType(): number { 12 | return 4; 13 | } 14 | 15 | identify(_identifier: number, _stateID: string): IPacketHandlerResult { 16 | // NOT IMPLEMENTED 17 | return {}; 18 | } 19 | 20 | constructor(private authentication: IAuthentication) {} 21 | 22 | async handleMessage( 23 | _identifier: number, 24 | _stateID: string, 25 | _msg: Buffer, 26 | _orgRadiusPacket: RadiusPacket 27 | ): Promise { 28 | // not implemented 29 | 30 | debug('eap md5 not implemented...'); 31 | 32 | return {}; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ssl/cert/server.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIC2TCCAcECAQAwgZMxCzAJBgNVBAYTAkFUMQ8wDQYDVQQIDAZSYWRpdXMxEjAQ 3 | BgNVBAcMCVNvbWV3aGVyZTEVMBMGA1UECgwMRXhhbXBsZSBJbmMuMSAwHgYJKoZI 4 | hvcNAQkBFhFhZG1pbkBleGFtcGxlLm9yZzEmMCQGA1UEAwwdRXhhbXBsZSBDZXJ0 5 | aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB 6 | AQCgMtR/dFP+1t8Vm6SU56GatjKJMuS8rhdewmlinTjppHnjjzR0ijdsL8kVtzmK 7 | CXlNJEwMYhxQQcyQPsWE1i5YgOkrNdJn9zmtioIgNhczE/HNh1w2moZv/HfPGJel 8 | 65qcGbGg0OkySsp8tYMLjxDtiXq8HQH6YQ4vnZrGK3FMPeFWSF7U3i+svQOlRoFV 9 | 9xYpOI39d8TuWqsC6hn3fvEiQxZbENQrgmbQyktV0lbSFJLbMzUsyL14agbadX9P 10 | NqsWWuArHMprD+WXOw5/tuyGOhwQVXTAaCysYOJY34o6rRtxCgPiYgak1gnliyfB 11 | YQUbkp7/9CszInkkR0ANKiVxAgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAQEAInel 12 | TdZPJ++4rhbUVjNXDcJYq5BTL6IEJGE0voh+n57LxpZBQGmP6+mDRMs+6aacBzfY 13 | T1hOrsXkY1QTh5Up9A8MTkKd0jOVN2wNvRuESA3i6WMG8JCgsqxXrp3F6MuQS9sV 14 | GII4IagorXfKijydzbRU1DULaj+rdrD8vv+mIwMcHMmFNqmymuZfyfkMoXosLU+r 15 | Ysjc4aHIoQzC8m+WZIWiMKjU4FyktsWnfzwBB0HuDj45e94Z7mKuTyOR/+GJgMuS 16 | yh1KFLJba+t5YNhIN4fxagwn4SKTeIurelr1kvXa+Q2X6Y4RLufo5ghww5+G4WiP 17 | 9dMJjSlC1+zbTarsGA== 18 | -----END CERTIFICATE REQUEST----- 19 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | export function makeid(length) { 2 | let result = ''; 3 | const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 4 | const charactersLength = characters.length; 5 | for (let i = 0; i < length; i++) { 6 | result += characters.charAt(Math.floor(Math.random() * charactersLength)); 7 | } 8 | return result; 9 | } 10 | 11 | // by RFC Radius attributes have a max length 12 | // https://tools.ietf.org/html/rfc6929#section-1.2 13 | export const MAX_RADIUS_ATTRIBUTE_SIZE = 253; 14 | 15 | export interface IDeferredPromise { 16 | promise: Promise; 17 | resolve: (value?: unknown) => Promise; 18 | reject: (reason?: any) => Promise; 19 | } 20 | 21 | export const newDeferredPromise = (): IDeferredPromise => { 22 | if (Promise && !('deferred' in Promise)) { 23 | let fResolve; 24 | let fReject; 25 | 26 | const P = new Promise((resolve, reject) => { 27 | fResolve = resolve; 28 | fReject = reject; 29 | }); 30 | return { 31 | promise: P, 32 | resolve: fResolve, 33 | reject: fReject, 34 | }; 35 | } 36 | 37 | return (Promise as any).deferred; 38 | }; 39 | 40 | export const delay = (timeout: number) => 41 | new Promise((resolve) => setTimeout(() => resolve(), timeout)); 42 | -------------------------------------------------------------------------------- /src/auth.ts: -------------------------------------------------------------------------------- 1 | import * as NodeCache from 'node-cache'; 2 | import { Cache, ExpirationStrategy, MemoryStorage } from '@hokify/node-ts-cache'; 3 | import { IAuthentication } from './types/Authentication'; 4 | 5 | const cacheStrategy = new ExpirationStrategy(new MemoryStorage()); 6 | /** 7 | * this is just a simple abstraction to provide 8 | * an application layer for caching credentials 9 | */ 10 | export class Authentication implements IAuthentication { 11 | cache = new NodeCache(); 12 | 13 | constructor(private authenticator: IAuthentication) {} 14 | 15 | @Cache(cacheStrategy, { ttl: 60000 }) 16 | async authenticate(username: string, password: string): Promise { 17 | const cacheKey = `usr:${username}|pwd:${password}`; 18 | const fromCache = this.cache.get(cacheKey) as undefined | boolean; 19 | if (fromCache !== undefined) { 20 | console.log(`Cached Auth Result for user ${username}`, fromCache ? 'SUCCESS' : 'Failure'); 21 | return fromCache; 22 | } 23 | 24 | const authResult = await this.authenticator.authenticate(username, password); 25 | console.log(`Auth Result for user ${username}`, authResult ? 'SUCCESS' : 'Failure'); 26 | this.cache.set(cacheKey, authResult, authResult ? 86400 : 60); // cache for one day on success, otherwise just for 60 seconds 27 | 28 | return authResult; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ssl/server.cnf: -------------------------------------------------------------------------------- 1 | [ ca ] 2 | default_ca = CA_default 3 | 4 | [ CA_default ] 5 | dir = ./cert/ 6 | certs = $dir 7 | crl_dir = $dir/crl 8 | database = db/index.txt 9 | new_certs_dir = $dir 10 | certificate = $dir/server.pem 11 | serial = db/serial 12 | crl = $dir/crl.pem 13 | private_key = $dir/server.key 14 | RANDFILE = $dir/.rand 15 | name_opt = ca_default 16 | cert_opt = ca_default 17 | default_days = 6000 18 | default_crl_days = 30 19 | default_md = sha256 20 | preserve = no 21 | policy = policy_match 22 | 23 | [ policy_match ] 24 | countryName = match 25 | stateOrProvinceName = match 26 | organizationName = match 27 | organizationalUnitName = optional 28 | commonName = supplied 29 | emailAddress = optional 30 | 31 | [ policy_anything ] 32 | countryName = optional 33 | stateOrProvinceName = optional 34 | localityName = optional 35 | organizationName = optional 36 | organizationalUnitName = optional 37 | commonName = supplied 38 | emailAddress = optional 39 | 40 | [ req ] 41 | prompt = no 42 | distinguished_name = server 43 | default_bits = 2048 44 | input_password = whatever2020 45 | output_password = whatever2020 46 | 47 | [server] 48 | countryName = AT 49 | stateOrProvinceName = Radius 50 | localityName = Somewhere 51 | organizationName = Example Inc. 52 | emailAddress = admin@example.org 53 | commonName = "Example Certificate Authority" 54 | 55 | -------------------------------------------------------------------------------- /src/radius/handler/UserPasswordPacketHandler.ts: -------------------------------------------------------------------------------- 1 | import debug from 'debug'; 2 | import { IAuthentication } from '../../types/Authentication'; 3 | import { 4 | IPacket, 5 | IPacketHandler, 6 | IPacketHandlerResult, 7 | PacketResponseCode, 8 | } from '../../types/PacketHandler'; 9 | 10 | const log = debug('radius:user-pwd'); 11 | 12 | export class UserPasswordPacketHandler implements IPacketHandler { 13 | constructor(private authentication: IAuthentication) {} 14 | 15 | async handlePacket(packet: IPacket): Promise { 16 | const username = packet.attributes['User-Name']; 17 | let password = packet.attributes['User-Password']; 18 | if (Buffer.isBuffer(password)) { 19 | password = password.slice(0, password.indexOf('\0')); 20 | } 21 | 22 | if (!username || !password) { 23 | // params missing, this handler cannot continue... 24 | return {}; 25 | } 26 | 27 | log('username', username, username.toString()); 28 | log('token', password, password.toString()); 29 | 30 | const authenticated = await this.authentication.authenticate( 31 | username.toString(), 32 | password.toString() 33 | ); 34 | if (authenticated) { 35 | // success 36 | return { 37 | code: PacketResponseCode.AccessAccept, 38 | attributes: [['User-Name', username]], 39 | }; 40 | } 41 | 42 | // Failed 43 | console.error('decoding of UserPassword package failed'); 44 | return { 45 | code: PacketResponseCode.AccessReject, 46 | }; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/radius/PacketHandler.ts: -------------------------------------------------------------------------------- 1 | import { IPacket, IPacketHandler, IPacketHandlerResult } from '../types/PacketHandler'; 2 | import { IAuthentication } from '../types/Authentication'; 3 | import { EAPPacketHandler } from './handler/EAPPacketHandler'; 4 | import { EAPTTLS } from './handler/eap/eapMethods/EAP-TTLS'; 5 | import { EAPGTC } from './handler/eap/eapMethods/EAP-GTC'; 6 | import { EAPMD5 } from './handler/eap/eapMethods/EAP-MD5'; 7 | import { UserPasswordPacketHandler } from './handler/UserPasswordPacketHandler'; 8 | 9 | export class PacketHandler implements IPacketHandler { 10 | packetHandlers: IPacketHandler[] = []; 11 | 12 | constructor(authentication: IAuthentication) { 13 | this.packetHandlers.push( 14 | new EAPPacketHandler([ 15 | new EAPTTLS(authentication, this), 16 | new EAPGTC(authentication), 17 | new EAPMD5(authentication), 18 | ]) 19 | ); 20 | this.packetHandlers.push(new UserPasswordPacketHandler(authentication)); 21 | } 22 | 23 | async handlePacket(packet: IPacket, handlingType?: number) { 24 | let response: IPacketHandlerResult; 25 | 26 | let i = 0; 27 | if (!this.packetHandlers[i]) { 28 | throw new Error('no packet handlers registered'); 29 | } 30 | 31 | // process packet handlers until we get a response from one 32 | do { 33 | /* response is of type IPacketHandlerResult */ 34 | response = await this.packetHandlers[i].handlePacket(packet, handlingType); 35 | i++; 36 | } while (this.packetHandlers[i] && (!response || !response.code)); 37 | 38 | return response; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/radius/RadiusService.ts: -------------------------------------------------------------------------------- 1 | import * as radius from 'radius'; 2 | import { IAuthentication } from '../types/Authentication'; 3 | import { IPacketHandlerResult, PacketResponseCode } from '../types/PacketHandler'; 4 | 5 | import { PacketHandler } from './PacketHandler'; 6 | 7 | export class RadiusService { 8 | private packetHandler: PacketHandler; 9 | 10 | constructor(private secret: string, authentication: IAuthentication) { 11 | this.packetHandler = new PacketHandler(authentication); 12 | } 13 | 14 | async handleMessage( 15 | msg: Buffer 16 | ): Promise<{ data: Buffer; expectAcknowledgment?: boolean } | undefined> { 17 | const packet = radius.decode({ packet: msg, secret: this.secret }); 18 | 19 | if (packet.code !== 'Access-Request') { 20 | console.error('unknown packet type: ', packet.code); 21 | return undefined; 22 | } 23 | 24 | const response: IPacketHandlerResult = await this.packetHandler.handlePacket(packet); 25 | 26 | // still no response, we are done here 27 | if (!response || !response.code) { 28 | return undefined; 29 | } 30 | 31 | // all fine, return radius encoded response 32 | return { 33 | data: radius.encode_response({ 34 | packet, 35 | code: response.code, 36 | secret: this.secret, 37 | attributes: response.attributes, 38 | }), 39 | // if message is accept or reject, we conside this as final message 40 | // this means we do not expect a reponse from the client again (acknowledgement for package) 41 | expectAcknowledgment: response.code === PacketResponseCode.AccessChallenge, 42 | }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/auth/LDAPAuth.ts: -------------------------------------------------------------------------------- 1 | import * as LdapAuth from 'ldapauth-fork'; 2 | import * as fs from 'fs'; 3 | import { IAuthentication } from '../types/Authentication'; 4 | 5 | interface ILDAPAuthOptions { 6 | /** ldap url 7 | * e.g. ldaps://ldap.google.com 8 | */ 9 | url: string; 10 | /** base DN 11 | * e.g. 'dc=hokify,dc=com', */ 12 | base: string; 13 | 14 | tls: { 15 | keyFile: string; 16 | certFile: string; 17 | }; 18 | /** tls options 19 | * e.g. { 20 | servername: 'ldap.google.com' 21 | } */ 22 | tlsOptions?: any; 23 | /** 24 | * searchFilter 25 | */ 26 | searchFilter?: string; 27 | } 28 | 29 | export class LDAPAuth implements IAuthentication { 30 | private ldap: LdapAuth; 31 | 32 | constructor(config: ILDAPAuthOptions) { 33 | const tlsOptions = { 34 | key: fs.readFileSync(config.tls.keyFile), 35 | cert: fs.readFileSync(config.tls.certFile), 36 | ...config.tlsOptions, 37 | }; 38 | 39 | this.ldap = new LdapAuth({ 40 | url: config.url, 41 | searchBase: config.base, 42 | tlsOptions, 43 | searchFilter: config.searchFilter || '(uid={{username}})', 44 | reconnect: true, 45 | }); 46 | this.ldap.on('error', function (err) { 47 | console.error('LdapAuth: ', err); 48 | }); 49 | } 50 | 51 | async authenticate(username: string, password: string) { 52 | const authResult: boolean = await new Promise((resolve, reject) => { 53 | this.ldap.authenticate(username, password, function (err, user) { 54 | if (err) { 55 | resolve(false); 56 | console.error('ldap error', err); 57 | // reject(err); 58 | } 59 | if (user) resolve(user); 60 | else reject(); 61 | }); 62 | }); 63 | 64 | return !!authResult; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/auth/IMAPAuth.ts: -------------------------------------------------------------------------------- 1 | import * as imaps from 'imap-simple'; 2 | import { IAuthentication } from '../types/Authentication'; 3 | 4 | interface IIMAPAuthOptions { 5 | host: string; 6 | port?: number; 7 | useSecureTransport?: boolean; 8 | validHosts?: string[]; 9 | } 10 | 11 | export class IMAPAuth implements IAuthentication { 12 | private host: string; 13 | 14 | private port = 143; 15 | 16 | private useSecureTransport = false; 17 | 18 | private validHosts?: string[]; 19 | 20 | constructor(config: IIMAPAuthOptions) { 21 | this.host = config.host; 22 | if (config.port !== undefined) { 23 | this.port = config.port; 24 | } 25 | if (config.useSecureTransport !== undefined) { 26 | this.useSecureTransport = config.useSecureTransport; 27 | } 28 | if (config.validHosts !== undefined) { 29 | this.validHosts = config.validHosts; 30 | } 31 | } 32 | 33 | async authenticate(username: string, password: string) { 34 | if (this.validHosts) { 35 | const domain = username.split('@').pop(); 36 | if (!domain || !this.validHosts.includes(domain)) { 37 | console.info('invalid or no domain in username', username, domain); 38 | return false; 39 | } 40 | } 41 | let success = false; 42 | try { 43 | const connection = await imaps.connect({ 44 | imap: { 45 | host: this.host, 46 | port: this.port, 47 | tls: this.useSecureTransport, 48 | user: username, 49 | password, 50 | tlsOptions: { 51 | servername: this.host, // SNI (needs to be set for gmail) 52 | }, 53 | }, 54 | }); 55 | 56 | success = true; 57 | 58 | connection.end(); 59 | } catch (err) { 60 | console.error('imap auth failed', err); 61 | } 62 | return success; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | const SSL_CERT_DIRECTORY = path.join(__dirname, './ssl/cert'); 6 | 7 | module.exports = { 8 | port: 1812, 9 | // radius secret 10 | secret: 'testing123', 11 | 12 | certificate: { 13 | cert: fs.readFileSync(path.join(SSL_CERT_DIRECTORY, '/server.crt')), 14 | key: [ 15 | { 16 | pem: fs.readFileSync(path.join(SSL_CERT_DIRECTORY, '/server.key')), 17 | passphrase: 'whatever2020', 18 | }, 19 | ], 20 | // sessionTimeout: 3600, 21 | // sesionIdContext: 'meiasdfkljasdft!', 22 | // ticketKeys: Buffer.from('123456789012345678901234567890123456789012345678'), 23 | }, 24 | 25 | // GoogleLDAPAuth (optimized for google auth) 26 | authentication: 'GoogleLDAPAuth', 27 | authenticationOptions: { 28 | base: 'dc=hokify,dc=com', 29 | // get your keys from http://admin.google.com/ -> Apps -> LDAP -> Client 30 | tls: { 31 | keyFile: 'ldap.gsuite.key', 32 | certFile: 'ldap.gsuite.crt', 33 | }, 34 | }, 35 | 36 | /** LDAP AUTH 37 | authentication: 'LDAPAuth', 38 | authenticationOptions: { 39 | url: 'ldaps://ldap.google.com', 40 | base: 'dc=hokify,dc=com', 41 | tls: { 42 | keyFile: 'ldap.gsuite.key', 43 | certFile: 'ldap.gsuite.crt' 44 | }, 45 | tlsOptions: { 46 | servername: 'ldap.google.com' 47 | } 48 | } 49 | */ 50 | 51 | /** IMAP AUTH 52 | authentication: 'IMAPAuth', 53 | authenticationOptions: { 54 | host: 'imap.gmail.com', 55 | port: 993, 56 | useSecureTransport: true, 57 | validHosts: ['hokify.com'] 58 | } 59 | */ 60 | 61 | /** SMTP AUTH 62 | authentication: 'IMAPAuth', 63 | authenticationOptions: { 64 | host: 'smtp.gmail.com', 65 | port: 465, 66 | useSecureTransport: true, 67 | validHosts: ['gmail.com'] 68 | } 69 | */ 70 | }; 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "radius-server", 3 | "description": "radius server for google LDAP and TTLS", 4 | "version": "1.1.10", 5 | "engines": { 6 | "node": ">13.10.1" 7 | }, 8 | "bin": { 9 | "radius-server": "bin/radius-server" 10 | }, 11 | "files": [ 12 | "bin", 13 | "config.js", 14 | "dist", 15 | "ssl" 16 | ], 17 | "homepage": "https://github.com/simllll/node-radius-server", 18 | "scripts": { 19 | "release": "npm run build && standard-version", 20 | "debug": "DEBUG=radius:* node --tls-min-v1.0 dist/app.js", 21 | "start": "node --tls-min-v1.0 dist/app.js", 22 | "build": "tsc", 23 | "dev": "ts-node src/app.ts", 24 | "test": "mocha -r ts-node/register __tests__/**/*.test.ts", 25 | "test-ttls-pap": "__tests__/eapol_test -c __tests__/ttls-pap.conf -s testing123", 26 | "test-radtest": "radtest -x user pwd localhost 1812 testing123", 27 | "create-certificate": "sh ./ssl/create.sh && sh ./ssl/sign.sh" 28 | }, 29 | "author": "Simon Tretter ", 30 | "preferGlobal": true, 31 | "main": "dist/app.js", 32 | "dependencies": { 33 | "@hokify/node-ts-cache": "^5.4.1", 34 | "debug": "^4.3.1", 35 | "imap-simple": "^5.0.0", 36 | "ldapauth-fork": "^5.0.1", 37 | "ldapjs": "^2.2.2", 38 | "native-duplexpair": "^1.0.0", 39 | "node-cache": "^5.1.2", 40 | "radius": "~1.1.4", 41 | "smtp-client": "^0.3.3", 42 | "yargs": "~16.1.1" 43 | }, 44 | "license": "GPLv3", 45 | "devDependencies": { 46 | "@hokify/eslint-config": "^1.0.5", 47 | "@types/chai": "^4.2.14", 48 | "@types/ldapjs": "^1.0.9", 49 | "@types/mocha": "^8.0.4", 50 | "@types/radius": "0.0.28", 51 | "@types/yargs": "^15.0.10", 52 | "chai": "^4.2.0", 53 | "eslint": "^7.14.0", 54 | "mocha": "^8.2.1", 55 | "prettier": "^2.2.1", 56 | "standard-version": "^9.0.0", 57 | "ts-node": "^9.0.0", 58 | "typescript": "^4.1.2" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/auth/SMTPAuth.ts: -------------------------------------------------------------------------------- 1 | import { SMTPClient } from 'smtp-client'; 2 | import { IAuthentication } from '../types/Authentication'; 3 | 4 | interface ISMTPAuthOptions { 5 | host: string; 6 | port?: number; 7 | useSecureTransport?: boolean; 8 | validHosts?: string[]; 9 | } 10 | 11 | export class SMTPAuth implements IAuthentication { 12 | private host: string; 13 | 14 | private port = 25; 15 | 16 | private useSecureTransport = false; 17 | 18 | private validHosts?: string[]; 19 | 20 | constructor(options: ISMTPAuthOptions) { 21 | this.host = options.host; 22 | 23 | if (options.port !== undefined) { 24 | this.port = options.port; 25 | } 26 | 27 | if (options.useSecureTransport !== undefined) { 28 | this.useSecureTransport = options.useSecureTransport; 29 | } 30 | 31 | if (options.validHosts !== undefined) { 32 | this.validHosts = options.validHosts; 33 | } 34 | } 35 | 36 | async authenticate(username: string, password: string) { 37 | if (this.validHosts) { 38 | const domain = username.split('@').pop(); 39 | if (!domain || !this.validHosts.includes(domain)) { 40 | console.info('invalid or no domain in username', username, domain); 41 | return false; 42 | } 43 | } 44 | 45 | const s = new SMTPClient({ 46 | host: this.host, 47 | port: this.port, 48 | secure: this.useSecureTransport, 49 | tlsOptions: { 50 | servername: this.host, // SNI (needs to be set for gmail) 51 | }, 52 | }); 53 | 54 | let success = false; 55 | try { 56 | await s.connect(); 57 | await s.greet({ hostname: 'mx.domain.com' }); // runs EHLO command or HELO as a fallback 58 | await s.authPlain({ username, password }); // authenticates a user 59 | 60 | success = true; 61 | 62 | s.close(); // runs QUIT command 63 | } catch (err) { 64 | console.error('imap auth failed', err); 65 | } 66 | return success; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /ssl/cert/ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIE+jCCA+KgAwIBAgIUJozlGRrKBVim0N6T59U+lvduEJ4wDQYJKoZIhvcNAQEL 3 | BQAwgZMxCzAJBgNVBAYTAkFUMQ8wDQYDVQQIDAZSYWRpdXMxEjAQBgNVBAcMCVNv 4 | bWV3aGVyZTEVMBMGA1UECgwMRXhhbXBsZSBJbmMuMSAwHgYJKoZIhvcNAQkBFhFh 5 | ZG1pbkBleGFtcGxlLm9yZzEmMCQGA1UEAwwdRXhhbXBsZSBDZXJ0aWZpY2F0ZSBB 6 | dXRob3JpdHkwHhcNMjAxMjEwMTUwMTE1WhcNMzAxMDE5MTUwMTE1WjCBkzELMAkG 7 | A1UEBhMCQVQxDzANBgNVBAgMBlJhZGl1czESMBAGA1UEBwwJU29tZXdoZXJlMRUw 8 | EwYDVQQKDAxFeGFtcGxlIEluYy4xIDAeBgkqhkiG9w0BCQEWEWFkbWluQGV4YW1w 9 | bGUub3JnMSYwJAYDVQQDDB1FeGFtcGxlIENlcnRpZmljYXRlIEF1dGhvcml0eTCC 10 | ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM8+/p/9144ekvZMZrPv13qf 11 | pCuEnBl2GcvW+5r33e0UhhXcTBpFrcA/agz7bvGDuD2Z989/XVl2W05PfPcH9YBx 12 | CQBs0tJxQljrx2p9pPhoWljSNjXYg57iNcPYvEyVFAXIprYzSK7yH4fzYkqWrRHF 13 | HwfWdJPUoS6rQu+g3tM9uBTvMXZHDmgQxwg0zvekeTpmi67dL/mz9hG3zDuAvriQ 14 | mUWSQBkUVb/A0+wtFXG2LqdFlBt6MFnW/icv1YwdzpGxyRYXJ2aH272IqTj+0va7 15 | 8kmt7jPZQR3Sms9SM76XwVuWqbv3V4AosOuCVYbRRQN0k1in8lzT/44Om+VMLGsC 16 | AwEAAaOCAUIwggE+MB0GA1UdDgQWBBRgu2UDIkMWlgANske+OYj5CFD09zCB0wYD 17 | VR0jBIHLMIHIgBRgu2UDIkMWlgANske+OYj5CFD096GBmaSBljCBkzELMAkGA1UE 18 | BhMCQVQxDzANBgNVBAgMBlJhZGl1czESMBAGA1UEBwwJU29tZXdoZXJlMRUwEwYD 19 | VQQKDAxFeGFtcGxlIEluYy4xIDAeBgkqhkiG9w0BCQEWEWFkbWluQGV4YW1wbGUu 20 | b3JnMSYwJAYDVQQDDB1FeGFtcGxlIENlcnRpZmljYXRlIEF1dGhvcml0eYIUJozl 21 | GRrKBVim0N6T59U+lvduEJ4wDwYDVR0TAQH/BAUwAwEB/zA2BgNVHR8ELzAtMCug 22 | KaAnhiVodHRwOi8vd3d3LmV4YW1wbGUub3JnL2V4YW1wbGVfY2EuY3JsMA0GCSqG 23 | SIb3DQEBCwUAA4IBAQA7b/Gm3HAGmidOLoSuYMXv/rjRXxoU3olyXoP/t3JhcMcH 24 | DyymjWHN8vlAt5GvukoWzj7sx0rpbCfyYBcapBKXr/ghn2Cvis0Eua2oN5MgiPr1 25 | ksHxC5xkFxbE4GCMzdwMF++iQSd1iUopsHWDniZdhu0wJCWF+SNZQYjvs47qYop8 26 | t4CWOc4559Jzt239TBxQCTRU0rWxrAiu83QElSmKSkvgxP+qBotn+mch2PkW9GLW 27 | S10FpU3rKbR84AJ2kTy/OFT3R1UqOnXcIuQFeYmfSfrWXkHQkASIJRnqDbPahmrA 28 | Txw9l6AmKWQAaWuFXaNbNDIZFFgwtPI6Viy4hN/E 29 | -----END CERTIFICATE----- 30 | -------------------------------------------------------------------------------- /ssl/cert/ca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED PRIVATE KEY----- 2 | MIIFHDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQIkXpE+A344AwCAggA 3 | MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECPshBsOyAxluBIIEyPtMjHKMbrdW 4 | HnTg+7twIMMh3cjG5SnbSnimQXv/z15pWuuo633GiHaJVN0CcYT2A8wNGGEm0uoE 5 | 0U0GoaMD6/rxH3O8PJVUMo/qYfWxh4qEzEofxGcRKFeQiQ3ZE4QNrXyC1Hjr4Stn 6 | XqrXylT6v+hdfNWDaX42f7tS2gXQirpfiadps7CEgp4PWX7/HyVsZgZvGNg4BEE8 7 | bjDcUC+6mKMPOzPlZEeNO7EUYxEyuLKTBMN15PKwlHk9pMJ/rbQHtnqbDboS2p1k 8 | kntPV7jmeEz5nRiVb4IXRkEj7pM28CWsEQFtzEJ9XretE1YPIjPl5wlXQzrfEIJ/ 9 | 9HWbzsUOLAaTDi8w+TEzOP7gWUPwP4SV656mEDhjxTcoC69z0CjF5ZLBir315MR0 10 | 4ltRzXnBPBEiO7FswO/1reFANV+HgB1vwo2+o/rmxyZvUmb0+XSQJykNHOAJj8MG 11 | iIp/WyaRA9oa8Q9eh6I9Bmjw6ax7O3ErkKCwynKONYlJEIgtnoljDtTPKSe/Xc+q 12 | Uo6egVNvleUif1ZGXmpQqUdDwAZuuADHo15cmk+aF/wtpjLmmoucfwW32xQObxUJ 13 | vSpbbvr7qFWDrylvLfaiIx70cy94efENLMV4+gtOBCfnEjIfq8MEQ0sX1/6zFjt2 14 | oCVkcxsfyhVzWUnkcurJ3jTkXbrd+pDQc43HJ/BCznmbbHK87UQTgw4Bh1X0A8i1 15 | TKcZ0zru0QNaIlmtVyFIpzh1hUqg27jYpJEOVWAbjX6u5PTts6jMRIrqNablUcjh 16 | sAm07+cupuPCMOVPX1OgI71+g81lKzPGCoq7buYTw17oVymnV9Y1eaEvJv5oM45d 17 | x7GGQsX8LumlmfpjOi+gWYBq+va1j6rK73qtEzyFdsYtDgjWNqtfNtSPR9vPF4xv 18 | irchJA77TlO+acvqUOdMkYjzYr7uDyou7Y9YxzFksBdb8IIwKP1Nmy/ACMjGXnCQ 19 | y4+u2fI7GxIgBOR0a2n6Sn+vA7qhdUYTWLTCER1drEWM5F6PJGD5kFQUotNg+9aE 20 | 7+JUjJXkXzEnieXpWEnbOkQ5TbB9xHWHupYxnJBKqVbOMwKKu6EBc3KikLA7mpkM 21 | GKG551tmPm5xY6NZ/RcS/DSypF8hj8PSaWrBJ+UG2VGq1Vl5SyQ4fWeeF3j0jGml 22 | t+n4f5g3T6NaufFKiw+5YiTeU9EvLMePCJS5Ahm+2beLlssxnCFdR+ZhHu6bd+8p 23 | jZS1yqamDdEDeTXSTrRUNFCdGrbCB/VZphZHd0kJBTy+T7g3p8RxPnR/mY7SBysc 24 | 5y5rFxfkBcpjP0rVBVUc9WvdIVS74Y0I+CiqodN8iX8DK2cz990y+aMpl6GF8ECW 25 | B1M8nAphlypBrETFNW5SPrcTtnxhtL7Z7vZhhZz/rVSq73150F3QEKK8X4KbcYDA 26 | 4/nKl2dKGYIkMPp2+dagI73ZxFVeS6OCaW9Y2c+S3tBn71wFBHdDM7TpH5EwPlOl 27 | khU9CGTshdfxU3l4hjsDOehCHyetLFpc5B6ulyRIFG2OPWUTGA20GM2nlLvcgQpk 28 | SMYiZ9Q9GWqyUEZR5RmDnFVdMt080BGgr1BALzQcBSxybZBdcgVe6GofiwacEg/r 29 | YBwYND9ADIXIBhOHW6b84A== 30 | -----END ENCRYPTED PRIVATE KEY----- 31 | -------------------------------------------------------------------------------- /ssl/cert/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED PRIVATE KEY----- 2 | MIIFHDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQIjPVEHCF4Q9gCAggA 3 | MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECKlCLUtCbbGGBIIEyOB2zZc677sj 4 | szzd9WBfyzJyW1FaZ417cijF3YBhB1wJ/t2G+DzWKtwRkCVIiIbolVZ1IeocN3C8 5 | 5nZzfSLSdNb/hQTDNWd1K56S5hvmV2WAJDvre2G8V8KlYa4iSAcaLC/M74ITWBqS 6 | A9Rkhdzd5EQyQzKodDMJksTw/LFFI36pJ7CGgYaNynco1H+RuuRjclaSyuMaijMg 7 | ABhbRFKVOp7A47AiNcu1vaFTl7YMYZv6b/uvIk+DnGCKYBD4Na0/GoBelanFcN0L 8 | icQrrutEj5a5YvdcQvA0bcCDyCS2s8PbOcnJRYgTr/04X01rsFH181k+53J2Ph82 9 | bHcO0AHfNqHTDFv5AM7U65UCu/yNdHt6I0yy8I3zqfCPR3p8FB0ELJ0Hs8ujN1hS 10 | p0O7YO3uVvxQ4JEQaPNkYXrp1xirq9xAfLdWgplMuTMFXzSi2+ZwRyPnZztZd+uk 11 | cgFRlOqCw0WcPy9xm+NckxBGe4sHBiDOYo8D+ewlhFR0C96cJqIxcOProsfWsoq/ 12 | VzzGustx7hGXVD233tNkX1qh5tHGlBEeUAOnPjCW0KepdxWVBmk+Uarjlo5mxL2P 13 | nQMZeuhPvGm24Ersh70GzMjygFkOCaNokP9rcYD+vqgKE6uwGYkOoA2hR//PWSGU 14 | LxGZoxVmLoXhnWhGFSzirsOfTNxs1+7EZOTSO3+/0kVni0K7z1Na6eduek5zo1iz 15 | vNnH2kF8ocGCc3pbJj+eaOBd4LTVjG3o5yIABkgr9W6lErz45zqOJ5H6KmaM/DU9 16 | RIqMox2kWUflt9eTqyGOfSlhkFFGrNyVGEoAxWhkRW9bAyiLH0WhCjIlZJngeZ0/ 17 | VotpXC2ljC28loWzYZ3Bs40Ph7YwYhaFcPTwOp0p708LJjcwGJ925dwbbs5EDDEh 18 | d+wEAGLRj9uj2pgewdvgMOypY2QHvy4VNq1K8hHhW83G/V+GgrcrmbrwKYKn6QF3 19 | EPufPCTIQFbW9eWnur9BnXNmt7/emW5b61haCGp28lWN0RdqpUn4RamZY+Pi6qrU 20 | ADY/KIrittKVgLCQlbXxr/DktaRUnE9CBnoCzvEQa+gGE/qthLhqNGNfS4vNCQAg 21 | NxnI9zu9R/Di2F77SWS5K0C9sNrx8Zm3UIvjqrrw5kP3+5WbKe8yQ1mzRwAJwch1 22 | 58kVemq+GzKUGV0LJsjIADy4k3eveZ69RSPgr+Pb0pDVxE2EJxvryYcM3FJD97tP 23 | 8wAeuHXAKuMQecchhWBJmCUihPDDuaDlVBPzf2n2q+9jDQQATaFUjq7U14FzHzPy 24 | KUVqUF8tMF+jnc6sYmAgJ6tv7fntvdtHlQp8cBmBopvzIbXDpJhy4GlQrEF4yhx+ 25 | WgL66qgkakNnGHOgBowHVjUSo9/cNj1UYKIBTs+AOp48sRIVgLI/34osjXswgVql 26 | PM7WnfrVtjuWrV0RhlwMjHSibF1JV7JWBUDLT4ovybY+J+qwaN9DdOyXkd/bQ3Dj 27 | 82yr1frpKdh3dK4oDHyDOu8Gk0yG9IOiehh3ZST8k67KRHBdSYbY7PHwXNtsJUYv 28 | GrVJYu1H581zkhUsfJOO9Hi8ko1A/dXyNPUUItIr5mvw56m8JzhzNmk+48vsGhtL 29 | wq08mfnrJxDpU7Z9royQ8A== 30 | -----END ENCRYPTED PRIVATE KEY----- 31 | -------------------------------------------------------------------------------- /ssl/ca.cnf: -------------------------------------------------------------------------------- 1 | [ ca ] 2 | default_ca = CA_default 3 | 4 | [ CA_default ] 5 | dir = ./cert/ 6 | certs = $dir 7 | crl_dir = $dir/crl 8 | database = db/index.txt 9 | new_certs_dir = $dir 10 | certificate = $dir/ca.pem 11 | serial = db/serial 12 | crl = $dir/crl.pem 13 | private_key = $dir/ca.key 14 | RANDFILE = $dir/.rand 15 | name_opt = ca_default 16 | cert_opt = ca_default 17 | default_days = 60 18 | default_crl_days = 30 19 | default_md = sha256 20 | preserve = no 21 | policy = policy_match 22 | crlDistributionPoints = URI:http://www.example.org/example_ca.crl 23 | 24 | [ policy_match ] 25 | countryName = match 26 | stateOrProvinceName = match 27 | organizationName = match 28 | organizationalUnitName = optional 29 | commonName = supplied 30 | emailAddress = optional 31 | 32 | [ policy_anything ] 33 | countryName = optional 34 | stateOrProvinceName = optional 35 | localityName = optional 36 | organizationName = optional 37 | organizationalUnitName = optional 38 | commonName = supplied 39 | emailAddress = optional 40 | 41 | [ req ] 42 | prompt = no 43 | distinguished_name = certificate_authority 44 | default_bits = 2048 45 | input_password = whatever2020 46 | output_password = whatever2020 47 | x509_extensions = v3_ca 48 | 49 | [certificate_authority] 50 | countryName = AT 51 | stateOrProvinceName = Radius 52 | localityName = Somewhere 53 | organizationName = Example Inc. 54 | emailAddress = admin@example.org 55 | commonName = "Example Certificate Authority" 56 | 57 | [v3_ca] 58 | subjectKeyIdentifier = hash 59 | authorityKeyIdentifier = keyid:always,issuer:always 60 | basicConstraints = critical,CA:true 61 | crlDistributionPoints = URI:http://www.example.org/example_ca.crl 62 | -------------------------------------------------------------------------------- /src/radius/handler/eap/eapMethods/EAP-GTC.ts: -------------------------------------------------------------------------------- 1 | // https://tools.ietf.org/html/rfc5281 TTLS v0 2 | // https://tools.ietf.org/html/draft-funk-eap-ttls-v1-00 TTLS v1 (not implemented) 3 | /* eslint-disable no-bitwise */ 4 | import debug from 'debug'; 5 | import { IPacketHandlerResult, PacketResponseCode } from '../../../../types/PacketHandler'; 6 | import { IEAPMethod } from '../../../../types/EAPMethod'; 7 | import { IAuthentication } from '../../../../types/Authentication'; 8 | import { buildEAPResponse, decodeEAPHeader } from '../EAPHelper'; 9 | 10 | const log = debug('radius:eap:gtc'); 11 | 12 | export class EAPGTC implements IEAPMethod { 13 | getEAPType(): number { 14 | return 6; 15 | } 16 | 17 | extractValue(msg: Buffer) { 18 | let tillBinary0 = msg.findIndex((v) => v === 0) || msg.length; 19 | if (tillBinary0 < 0) { 20 | tillBinary0 = msg.length - 1; 21 | } 22 | return msg.slice(0, tillBinary0 + 1); // use token til binary 0. 23 | } 24 | 25 | identify(identifier: number, _stateID: string): IPacketHandlerResult { 26 | return buildEAPResponse(identifier, 6, Buffer.from('Password: ')); 27 | } 28 | 29 | constructor(private authentication: IAuthentication) {} 30 | 31 | async handleMessage( 32 | _identifier: number, 33 | _stateID: string, 34 | msg: Buffer, 35 | _, 36 | identity?: string 37 | ): Promise { 38 | const username = identity; // this.loginData.get(stateID) as Buffer | undefined; 39 | 40 | try { 41 | const { data } = decodeEAPHeader(msg); 42 | 43 | const token = this.extractValue(data); 44 | 45 | if (!username) { 46 | throw new Error('no username'); 47 | } 48 | 49 | log('username', username, username.toString()); 50 | log('token', token, token.toString()); 51 | 52 | const success = await this.authentication.authenticate(username.toString(), token.toString()); 53 | 54 | return { 55 | code: success ? PacketResponseCode.AccessAccept : PacketResponseCode.AccessReject, 56 | attributes: (success && [['User-Name', username]]) || undefined, 57 | }; 58 | } catch (err) { 59 | console.error('decoding of EAP-GTC package failed', msg, err); 60 | return { 61 | code: PacketResponseCode.AccessReject, 62 | }; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/radius/handler/eap/EAPHelper.ts: -------------------------------------------------------------------------------- 1 | import { IPacketHandlerResult, PacketResponseCode } from '../../../types/PacketHandler'; 2 | 3 | export function buildEAP(identifier: number, msgType: number, data?: Buffer) { 4 | /** build a package according to this: 5 | 0 1 2 3 6 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 7 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 8 | | Code | Identifier | Length | 9 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 10 | | Type | Type-Data ... 11 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- 12 | */ 13 | const buffer = Buffer.from([ 14 | 1, // request 15 | identifier, 16 | 0, // length (1/2) 17 | 0, // length (2/2) 18 | msgType, // 1 = identity, 21 = EAP-TTLS, 2 = notificaiton, 4 = md5-challenge, 3 = NAK 19 | ]); 20 | 21 | const resBuffer = data ? Buffer.concat([buffer, data]) : buffer; 22 | 23 | // set EAP length header 24 | resBuffer.writeUInt16BE(resBuffer.byteLength, 2); 25 | 26 | return resBuffer; 27 | } 28 | 29 | /** 30 | * 31 | * @param data 32 | * @param msgType 1 = identity, 21 = EAP-TTLS, 2 = notification, 4 = md5-challenge, 3 = NAK 33 | */ 34 | export function buildEAPResponse( 35 | identifier: number, 36 | msgType: number, 37 | data?: Buffer 38 | ): IPacketHandlerResult { 39 | return { 40 | code: PacketResponseCode.AccessChallenge, 41 | attributes: [['EAP-Message', buildEAP(identifier, msgType, data)]], 42 | }; 43 | } 44 | 45 | export function decodeEAPHeader(msg: Buffer) { 46 | /** 47 | * parse msg according to this: 48 | 0 1 2 3 49 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 50 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 51 | | Code | Identifier | Length | 52 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 53 | | Type | Type-Data ... 54 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- 55 | */ 56 | 57 | /* 58 | code: 59 | 1 Request 60 | 2 Response 61 | 3 Success 62 | 4 Failure 63 | */ 64 | const code = msg.slice(0, 1).readUInt8(0); 65 | /* identifier is a number */ 66 | const identifier = msg.slice(1, 2).readUInt8(0); 67 | const length = msg.slice(2, 4).readUInt16BE(0); 68 | /* EAP type */ 69 | const type = msg.slice(4, 5).readUInt8(0); 70 | const data = msg.slice(5); 71 | 72 | return { 73 | code, 74 | identifier, 75 | length, 76 | type, 77 | data, 78 | }; 79 | } 80 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import * as yargs from 'yargs'; 2 | import { UDPServer } from './server/UDPServer'; 3 | import { RadiusService } from './radius/RadiusService'; 4 | 5 | import * as config from '../config'; 6 | import { Authentication } from './auth'; 7 | import { IAuthentication } from './types/Authentication'; 8 | import { startTLSServer } from './tls/crypt'; 9 | 10 | /* test node version */ 11 | const testSocket = startTLSServer(); 12 | if (typeof (testSocket.tls as any).exportKeyingMaterial !== 'function') { 13 | console.error(`UNSUPPORTED NODE VERSION (${process.version}) FOUND!!`); 14 | 15 | console.log('min version supported is node js 14. run "sudo npx n 14"'); 16 | process.exit(-1); 17 | } 18 | 19 | const { argv } = yargs 20 | .usage('NODE RADIUS Server\nUsage: radius-server') 21 | .example('radius-server --port 1812 -s radiussecret', 'start on port 1812 with a secret') 22 | .default({ 23 | port: config.port || 1812, 24 | s: config.secret || 'testing123', 25 | authentication: config.authentication, 26 | authenticationOptions: config.authenticationOptions, 27 | }) 28 | .describe('port', 'RADIUS server listener port') 29 | .alias('s', 'secret') 30 | .describe('secret', 'RADIUS secret') 31 | .number('port') 32 | .string(['secret', 'authentication']); 33 | 34 | console.log(`Listener Port: ${argv.port || 1812}`); 35 | console.log(`RADIUS Secret: ${argv.secret}`); 36 | console.log(`Auth ${argv.authentication}`); 37 | console.log(`Auth Config: ${JSON.stringify(argv.authenticationOptions, undefined, 3)}`); 38 | 39 | (async () => { 40 | /* configure auth mechansim */ 41 | let auth: IAuthentication; 42 | try { 43 | const AuthMechanismus = (await import(`./auth/${config.authentication}`))[ 44 | config.authentication 45 | ]; 46 | auth = new AuthMechanismus(config.authenticationOptions); 47 | } catch (err) { 48 | console.error('cannot load auth mechanismus', config.authentication); 49 | throw err; 50 | } 51 | // start radius server 52 | const authentication = new Authentication(auth); 53 | 54 | const server = new UDPServer(config.port); 55 | const radiusService = new RadiusService(config.secret, authentication); 56 | 57 | server.on('message', async (msg, rinfo) => { 58 | const response = await radiusService.handleMessage(msg); 59 | 60 | if (response) { 61 | server.sendToClient( 62 | response.data, 63 | rinfo.port, 64 | rinfo.address, 65 | (err, _bytes) => { 66 | if (err) { 67 | console.log('Error sending response to ', rinfo); 68 | } 69 | }, 70 | response.expectAcknowledgment 71 | ); 72 | } 73 | }); 74 | 75 | // start server 76 | await server.start(); 77 | })(); 78 | -------------------------------------------------------------------------------- /src/server/UDPServer.ts: -------------------------------------------------------------------------------- 1 | import * as dgram from 'dgram'; 2 | import { SocketType } from 'dgram'; 3 | import * as events from 'events'; 4 | import { EventEmitter } from 'events'; 5 | import { newDeferredPromise } from '../helpers'; 6 | import { IServer } from '../types/Server'; 7 | 8 | export class UDPServer extends events.EventEmitter implements IServer { 9 | static MAX_RETRIES = 3; 10 | 11 | private timeout: { [key: string]: NodeJS.Timeout } = {}; 12 | 13 | private server: dgram.Socket; 14 | 15 | constructor(private port: number, type: SocketType = 'udp4') { 16 | super(); 17 | this.server = dgram.createSocket(type); 18 | } 19 | 20 | sendToClient( 21 | msg: string | Uint8Array, 22 | port?: number, 23 | address?: string, 24 | callback?: (error: Error | null, bytes: number) => void, 25 | expectAcknowledgment = true 26 | ): void { 27 | let retried = 0; 28 | 29 | const sendResponse = (): void => { 30 | if (retried > 0) { 31 | console.warn( 32 | `no confirmation of last message from ${address}:${port}, re-sending response... (bytes: ${msg.length}, try: ${retried}/${UDPServer.MAX_RETRIES})` 33 | ); 34 | } 35 | 36 | // send message to client 37 | this.server.send(msg, 0, msg.length, port, address, callback); 38 | 39 | // retry up to MAX_RETRIES to send this message, 40 | // we automatically retry if there is no confirmation (=any incoming message from client) 41 | // if expectAcknowledgment is false (e.g. Access-Accept or Access-Reject), we do not retry 42 | const identifierForRetry = `${address}:${port}`; 43 | if (expectAcknowledgment && retried < UDPServer.MAX_RETRIES) { 44 | this.timeout[identifierForRetry] = setTimeout(() => sendResponse(), 1600 * (retried + 1)); 45 | } 46 | retried += 1; 47 | }; 48 | 49 | sendResponse(); 50 | } 51 | 52 | async start(): Promise { 53 | const startServer = newDeferredPromise(); 54 | this.server.on('listening', () => { 55 | const address = this.server.address(); 56 | console.log(`radius server listening ${address.address}:${address.port}`); 57 | 58 | this.setupListeners(); 59 | startServer.resolve(); 60 | }); 61 | 62 | this.server.on('message', (_msg, rinfo) => { 63 | // message retrieved, reset timeout handler 64 | const identifierForRetry = `${rinfo.address}:${rinfo.port}`; 65 | if (this.timeout[identifierForRetry]) { 66 | clearTimeout(this.timeout[identifierForRetry]); 67 | } 68 | }); 69 | 70 | this.server.bind(this.port); 71 | 72 | return startServer.promise; 73 | } 74 | 75 | private setupListeners() { 76 | this.server.on('message', (message, rinfo) => this.emit('message', message, rinfo)); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/auth/README.md: -------------------------------------------------------------------------------- 1 | # Authentications 2 | 3 | ## Google LDAP 4 | 5 | google ldap optimized authenticiation implementaiton 6 | 7 | ```typescript 8 | interface IGoogleLDAPAuthOptions { 9 | /** base DN 10 | * e.g. 'dc=hokify,dc=com', */ 11 | base: string; 12 | tls: { 13 | keyFile: string; 14 | certFile: string; 15 | }; 16 | /** tls options 17 | * e.g. { 18 | key: fs.readFileSync('ldap.gsuite.key'), 19 | cert: fs.readFileSync('ldap.gsuite.crt') 20 | } */ 21 | tlsOptions?: tls.TlsOptions; 22 | } 23 | ``` 24 | 25 | Example 26 | 27 | ```js 28 | c = { 29 | // GoogleLDAPAuth (optimized for google auth) 30 | authentication: 'GoogleLDAPAuth', 31 | authenticationOptions: { 32 | base: 'dc=hokify,dc=com', 33 | tls: { 34 | keyFile: 'ldap.gsuite.key', 35 | certFile: 'ldap.gsuite.crt' 36 | } 37 | } 38 | }; 39 | ``` 40 | 41 | ## LDAP 42 | 43 | ldap authentication 44 | 45 | ```typescript 46 | interface ILDAPAuthOptions { 47 | /** ldap url 48 | * e.g. ldaps://ldap.google.com 49 | */ 50 | url: string; 51 | /** base DN 52 | * e.g. 'dc=hokify,dc=com', */ 53 | base: string; 54 | 55 | tls: { 56 | keyFile: string; 57 | certFile: string; 58 | }; 59 | /** tls options 60 | * e.g. { 61 | servername: 'ldap.google.com' 62 | } */ 63 | tlsOptions?: any; 64 | /** 65 | * searchFilter 66 | */ 67 | searchFilter?: string; 68 | } 69 | ``` 70 | 71 | Example 72 | 73 | ```js 74 | c = { 75 | authentication: 'LDAPAuth', 76 | authenticationOptions: { 77 | url: 'ldaps://ldap.google.com', 78 | base: 'dc=hokify,dc=com', 79 | tlsOptions: { 80 | servername: 'ldap.google.com' 81 | }, 82 | tls: { 83 | keyFile: 'ldap.gsuite.key', 84 | certFile: 'ldap.gsuite.crt' 85 | } 86 | } 87 | }; 88 | ``` 89 | 90 | ## IMAP 91 | 92 | imap authenticiation 93 | 94 | ```typescript 95 | interface IIMAPAuthOptions { 96 | host: string; 97 | port?: number; 98 | useSecureTransport?: boolean; 99 | validHosts?: string[]; 100 | } 101 | ``` 102 | 103 | Example 104 | 105 | ```js 106 | c = { 107 | authentication: 'IMAPAuth', 108 | authenticationOptions: { 109 | host: 'imap.gmail.com', 110 | port: 993, 111 | useSecureTransport: true, 112 | validHosts: ['hokify.com'] 113 | } 114 | }; 115 | ``` 116 | 117 | ## SMTP 118 | 119 | smtp authenticiation 120 | 121 | ```typescript 122 | interface ISMTPAuthOptions { 123 | host: string; 124 | port?: number; 125 | useSecureTransport?: boolean; 126 | validHosts?: string[]; 127 | } 128 | ``` 129 | 130 | Example 131 | 132 | ```js 133 | c = { 134 | authentication: 'IMAPAuth', 135 | authenticationOptions: { 136 | host: 'smtp.gmail.com', 137 | port: 465, 138 | useSecureTransport: true, 139 | validHosts: ['gmail.com'] 140 | } 141 | }; 142 | ``` 143 | 144 | ## Static Auth 145 | 146 | static authenticiation 147 | 148 | ```typescript 149 | interface IStaticAuthOtions { 150 | validCrentials: { 151 | username: string; 152 | password: string; 153 | }[]; 154 | } 155 | ``` 156 | 157 | Example 158 | 159 | ```js 160 | c = { 161 | authentication: 'StaticAuth', 162 | authenticationOptions: { 163 | validCredentials: [ 164 | { username: 'test', password: 'pwd' }, 165 | { username: 'user1', password: 'password' }, 166 | { username: 'admin', password: 'cool' } 167 | ] 168 | } 169 | }; 170 | ``` 171 | -------------------------------------------------------------------------------- /ssl/cert/00.pem: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 0 (0x0) 5 | Signature Algorithm: sha256WithRSAEncryption 6 | Issuer: C=AT, ST=Radius, L=Somewhere, O=Example Inc./emailAddress=admin@example.org, CN=Example Certificate Authority 7 | Validity 8 | Not Before: Feb 22 17:05:38 2020 GMT 9 | Not After : Jul 27 17:05:38 2036 GMT 10 | Subject: C=AT, ST=Radius, O=Example Inc., CN=Example Certificate Authority/emailAddress=admin@example.org 11 | Subject Public Key Info: 12 | Public Key Algorithm: rsaEncryption 13 | RSA Public-Key: (2048 bit) 14 | Modulus: 15 | 00:b5:ad:78:5d:89:14:28:92:55:e7:c1:e0:15:46: 16 | e7:ea:da:ea:98:bb:ec:33:17:ee:d6:e3:37:1c:34: 17 | ea:4e:87:a1:c9:d3:19:de:95:6f:c1:76:13:e0:b4: 18 | f5:08:3e:fe:5a:76:b8:4a:e4:94:07:8b:67:4b:b5: 19 | d1:97:c1:d3:e1:b1:cd:84:5b:16:aa:dc:56:5b:ff: 20 | 0a:e7:f3:79:a3:47:4d:5b:6d:e4:d6:21:a0:eb:e1: 21 | 54:83:87:1c:c5:18:de:d8:52:40:93:7e:fb:c5:df: 22 | 73:3c:d6:0b:a3:1d:9f:36:c7:6f:59:90:d7:5f:82: 23 | 80:fe:82:05:1b:1b:77:92:04:c9:25:88:8f:9c:35: 24 | 91:cc:86:a0:8f:8f:9b:2e:2c:62:af:f3:8a:16:75: 25 | 30:61:47:8d:24:ea:35:84:43:33:01:ed:d7:33:2c: 26 | 29:4f:2c:8d:f5:b7:b3:bd:74:4f:2e:83:2d:e1:d6: 27 | 3d:5e:d9:14:c3:a1:89:38:4d:29:4b:ae:39:c8:a2: 28 | dd:ad:f1:42:c8:0d:6c:29:eb:70:c8:dc:e7:41:59: 29 | 47:1c:89:52:62:53:9c:35:58:8a:39:16:87:61:f2: 30 | 11:26:3a:2b:a2:19:29:c2:77:31:de:1c:74:c6:57: 31 | 3a:15:8b:2f:29:61:a7:45:b4:d8:70:a4:d2:ef:da: 32 | 5d:a1 33 | Exponent: 65537 (0x10001) 34 | X509v3 extensions: 35 | X509v3 Extended Key Usage: 36 | TLS Web Server Authentication 37 | X509v3 CRL Distribution Points: 38 | 39 | Full Name: 40 | URI:http://www.example.com/example_ca.crl 41 | 42 | Signature Algorithm: sha256WithRSAEncryption 43 | 60:49:5c:0c:1d:1a:c3:86:16:b7:d8:63:b7:b9:9b:4d:74:49: 44 | 25:05:c0:28:f4:c6:95:ce:63:95:b7:99:41:54:7f:64:5d:9a: 45 | 8f:f5:a7:96:09:8c:67:42:61:5d:c5:af:e4:d9:33:28:20:2a: 46 | 1f:76:5b:a7:1f:54:47:c4:94:4f:de:bb:6d:ea:36:51:44:bf: 47 | 90:82:b3:7c:91:28:6a:1e:dd:76:66:38:2c:0f:ed:8a:fd:b6: 48 | 91:28:3b:59:f0:b3:42:4b:bb:fb:88:d9:d8:6f:e2:a9:79:14: 49 | df:0b:53:76:be:23:c1:0e:96:85:aa:d9:3f:d6:60:7a:a5:a9: 50 | 5a:1d:17:05:7e:84:7a:36:0b:a0:eb:96:8d:75:08:ab:ea:e2: 51 | 9b:4c:dc:92:05:b0:bc:2a:c0:ff:21:89:02:c2:cd:07:ee:85: 52 | 55:5b:cf:bd:bc:d7:b1:8a:54:51:5a:58:f6:77:e5:76:85:26: 53 | fd:dd:e8:ec:aa:45:c8:cf:d1:10:16:68:97:e4:e7:06:e3:22: 54 | 9c:4d:f5:85:bb:37:26:d4:fc:47:af:26:03:f3:e1:17:cc:20: 55 | 33:c1:c4:e2:b3:b7:1e:d3:0a:81:85:ff:e6:79:70:07:a7:8b: 56 | 3c:1e:9e:1b:e8:f5:a1:3e:69:c5:90:f4:7f:49:a7:19:93:fa: 57 | 65:fc:0e:43 58 | -----BEGIN CERTIFICATE----- 59 | MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBkzELMAkGA1UEBhMCQVQx 60 | DzANBgNVBAgMBlJhZGl1czESMBAGA1UEBwwJU29tZXdoZXJlMRUwEwYDVQQKDAxF 61 | eGFtcGxlIEluYy4xIDAeBgkqhkiG9w0BCQEWEWFkbWluQGV4YW1wbGUub3JnMSYw 62 | JAYDVQQDDB1FeGFtcGxlIENlcnRpZmljYXRlIEF1dGhvcml0eTAeFw0yMDAyMjIx 63 | NzA1MzhaFw0zNjA3MjcxNzA1MzhaMH8xCzAJBgNVBAYTAkFUMQ8wDQYDVQQIDAZS 64 | YWRpdXMxFTATBgNVBAoMDEV4YW1wbGUgSW5jLjEmMCQGA1UEAwwdRXhhbXBsZSBD 65 | ZXJ0aWZpY2F0ZSBBdXRob3JpdHkxIDAeBgkqhkiG9w0BCQEWEWFkbWluQGV4YW1w 66 | bGUub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAta14XYkUKJJV 67 | 58HgFUbn6trqmLvsMxfu1uM3HDTqToehydMZ3pVvwXYT4LT1CD7+Wna4SuSUB4tn 68 | S7XRl8HT4bHNhFsWqtxWW/8K5/N5o0dNW23k1iGg6+FUg4ccxRje2FJAk377xd9z 69 | PNYLox2fNsdvWZDXX4KA/oIFGxt3kgTJJYiPnDWRzIagj4+bLixir/OKFnUwYUeN 70 | JOo1hEMzAe3XMywpTyyN9bezvXRPLoMt4dY9XtkUw6GJOE0pS645yKLdrfFCyA1s 71 | KetwyNznQVlHHIlSYlOcNViKORaHYfIRJjorohkpwncx3hx0xlc6FYsvKWGnRbTY 72 | cKTS79pdoQIDAQABo08wTTATBgNVHSUEDDAKBggrBgEFBQcDATA2BgNVHR8ELzAt 73 | MCugKaAnhiVodHRwOi8vd3d3LmV4YW1wbGUuY29tL2V4YW1wbGVfY2EuY3JsMA0G 74 | CSqGSIb3DQEBCwUAA4IBAQBgSVwMHRrDhha32GO3uZtNdEklBcAo9MaVzmOVt5lB 75 | VH9kXZqP9aeWCYxnQmFdxa/k2TMoICofdlunH1RHxJRP3rtt6jZRRL+QgrN8kShq 76 | Ht12ZjgsD+2K/baRKDtZ8LNCS7v7iNnYb+KpeRTfC1N2viPBDpaFqtk/1mB6pala 77 | HRcFfoR6Ngug65aNdQir6uKbTNySBbC8KsD/IYkCws0H7oVVW8+9vNexilRRWlj2 78 | d+V2hSb93ejsqkXIz9EQFmiX5OcG4yKcTfWFuzcm1PxHryYD8+EXzCAzwcTis7ce 79 | 0wqBhf/meXAHp4s8Hp4b6PWhPmnFkPR/SacZk/pl/A5D 80 | -----END CERTIFICATE----- 81 | -------------------------------------------------------------------------------- /ssl/cert/server.crt: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 0 (0x0) 5 | Signature Algorithm: sha256WithRSAEncryption 6 | Issuer: C=AT, ST=Radius, L=Somewhere, O=Example Inc./emailAddress=admin@example.org, CN=Example Certificate Authority 7 | Validity 8 | Not Before: Feb 22 17:05:38 2020 GMT 9 | Not After : Jul 27 17:05:38 2036 GMT 10 | Subject: C=AT, ST=Radius, O=Example Inc., CN=Example Certificate Authority/emailAddress=admin@example.org 11 | Subject Public Key Info: 12 | Public Key Algorithm: rsaEncryption 13 | RSA Public-Key: (2048 bit) 14 | Modulus: 15 | 00:b5:ad:78:5d:89:14:28:92:55:e7:c1:e0:15:46: 16 | e7:ea:da:ea:98:bb:ec:33:17:ee:d6:e3:37:1c:34: 17 | ea:4e:87:a1:c9:d3:19:de:95:6f:c1:76:13:e0:b4: 18 | f5:08:3e:fe:5a:76:b8:4a:e4:94:07:8b:67:4b:b5: 19 | d1:97:c1:d3:e1:b1:cd:84:5b:16:aa:dc:56:5b:ff: 20 | 0a:e7:f3:79:a3:47:4d:5b:6d:e4:d6:21:a0:eb:e1: 21 | 54:83:87:1c:c5:18:de:d8:52:40:93:7e:fb:c5:df: 22 | 73:3c:d6:0b:a3:1d:9f:36:c7:6f:59:90:d7:5f:82: 23 | 80:fe:82:05:1b:1b:77:92:04:c9:25:88:8f:9c:35: 24 | 91:cc:86:a0:8f:8f:9b:2e:2c:62:af:f3:8a:16:75: 25 | 30:61:47:8d:24:ea:35:84:43:33:01:ed:d7:33:2c: 26 | 29:4f:2c:8d:f5:b7:b3:bd:74:4f:2e:83:2d:e1:d6: 27 | 3d:5e:d9:14:c3:a1:89:38:4d:29:4b:ae:39:c8:a2: 28 | dd:ad:f1:42:c8:0d:6c:29:eb:70:c8:dc:e7:41:59: 29 | 47:1c:89:52:62:53:9c:35:58:8a:39:16:87:61:f2: 30 | 11:26:3a:2b:a2:19:29:c2:77:31:de:1c:74:c6:57: 31 | 3a:15:8b:2f:29:61:a7:45:b4:d8:70:a4:d2:ef:da: 32 | 5d:a1 33 | Exponent: 65537 (0x10001) 34 | X509v3 extensions: 35 | X509v3 Extended Key Usage: 36 | TLS Web Server Authentication 37 | X509v3 CRL Distribution Points: 38 | 39 | Full Name: 40 | URI:http://www.example.com/example_ca.crl 41 | 42 | Signature Algorithm: sha256WithRSAEncryption 43 | 60:49:5c:0c:1d:1a:c3:86:16:b7:d8:63:b7:b9:9b:4d:74:49: 44 | 25:05:c0:28:f4:c6:95:ce:63:95:b7:99:41:54:7f:64:5d:9a: 45 | 8f:f5:a7:96:09:8c:67:42:61:5d:c5:af:e4:d9:33:28:20:2a: 46 | 1f:76:5b:a7:1f:54:47:c4:94:4f:de:bb:6d:ea:36:51:44:bf: 47 | 90:82:b3:7c:91:28:6a:1e:dd:76:66:38:2c:0f:ed:8a:fd:b6: 48 | 91:28:3b:59:f0:b3:42:4b:bb:fb:88:d9:d8:6f:e2:a9:79:14: 49 | df:0b:53:76:be:23:c1:0e:96:85:aa:d9:3f:d6:60:7a:a5:a9: 50 | 5a:1d:17:05:7e:84:7a:36:0b:a0:eb:96:8d:75:08:ab:ea:e2: 51 | 9b:4c:dc:92:05:b0:bc:2a:c0:ff:21:89:02:c2:cd:07:ee:85: 52 | 55:5b:cf:bd:bc:d7:b1:8a:54:51:5a:58:f6:77:e5:76:85:26: 53 | fd:dd:e8:ec:aa:45:c8:cf:d1:10:16:68:97:e4:e7:06:e3:22: 54 | 9c:4d:f5:85:bb:37:26:d4:fc:47:af:26:03:f3:e1:17:cc:20: 55 | 33:c1:c4:e2:b3:b7:1e:d3:0a:81:85:ff:e6:79:70:07:a7:8b: 56 | 3c:1e:9e:1b:e8:f5:a1:3e:69:c5:90:f4:7f:49:a7:19:93:fa: 57 | 65:fc:0e:43 58 | -----BEGIN CERTIFICATE----- 59 | MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBkzELMAkGA1UEBhMCQVQx 60 | DzANBgNVBAgMBlJhZGl1czESMBAGA1UEBwwJU29tZXdoZXJlMRUwEwYDVQQKDAxF 61 | eGFtcGxlIEluYy4xIDAeBgkqhkiG9w0BCQEWEWFkbWluQGV4YW1wbGUub3JnMSYw 62 | JAYDVQQDDB1FeGFtcGxlIENlcnRpZmljYXRlIEF1dGhvcml0eTAeFw0yMDAyMjIx 63 | NzA1MzhaFw0zNjA3MjcxNzA1MzhaMH8xCzAJBgNVBAYTAkFUMQ8wDQYDVQQIDAZS 64 | YWRpdXMxFTATBgNVBAoMDEV4YW1wbGUgSW5jLjEmMCQGA1UEAwwdRXhhbXBsZSBD 65 | ZXJ0aWZpY2F0ZSBBdXRob3JpdHkxIDAeBgkqhkiG9w0BCQEWEWFkbWluQGV4YW1w 66 | bGUub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAta14XYkUKJJV 67 | 58HgFUbn6trqmLvsMxfu1uM3HDTqToehydMZ3pVvwXYT4LT1CD7+Wna4SuSUB4tn 68 | S7XRl8HT4bHNhFsWqtxWW/8K5/N5o0dNW23k1iGg6+FUg4ccxRje2FJAk377xd9z 69 | PNYLox2fNsdvWZDXX4KA/oIFGxt3kgTJJYiPnDWRzIagj4+bLixir/OKFnUwYUeN 70 | JOo1hEMzAe3XMywpTyyN9bezvXRPLoMt4dY9XtkUw6GJOE0pS645yKLdrfFCyA1s 71 | KetwyNznQVlHHIlSYlOcNViKORaHYfIRJjorohkpwncx3hx0xlc6FYsvKWGnRbTY 72 | cKTS79pdoQIDAQABo08wTTATBgNVHSUEDDAKBggrBgEFBQcDATA2BgNVHR8ELzAt 73 | MCugKaAnhiVodHRwOi8vd3d3LmV4YW1wbGUuY29tL2V4YW1wbGVfY2EuY3JsMA0G 74 | CSqGSIb3DQEBCwUAA4IBAQBgSVwMHRrDhha32GO3uZtNdEklBcAo9MaVzmOVt5lB 75 | VH9kXZqP9aeWCYxnQmFdxa/k2TMoICofdlunH1RHxJRP3rtt6jZRRL+QgrN8kShq 76 | Ht12ZjgsD+2K/baRKDtZ8LNCS7v7iNnYb+KpeRTfC1N2viPBDpaFqtk/1mB6pala 77 | HRcFfoR6Ngug65aNdQir6uKbTNySBbC8KsD/IYkCws0H7oVVW8+9vNexilRRWlj2 78 | d+V2hSb93ejsqkXIz9EQFmiX5OcG4yKcTfWFuzcm1PxHryYD8+EXzCAzwcTis7ce 79 | 0wqBhf/meXAHp4s8Hp4b6PWhPmnFkPR/SacZk/pl/A5D 80 | -----END CERTIFICATE----- 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Basic RADIUS Server for node.js for Google LDAP Service and WPA2 Enterprise WLAN Authentification. 2 | 3 | - supports LDAP Authentification Backend 4 | - supports WPA2 Enterprise (TTLS over PAP) 5 | 6 | Protect your WIFI access with a username and password by a credential provider you already use! 7 | 8 | Authentication tested with Windows, Linux, Android and Apple devices. 9 | 10 | # Quick start 11 | 12 | 1. Install node js => 13.10.1 13 | - easiest way is to install a node js version from nodejs.org or run "npx n latest" to install latest version. 14 | 2. Check out the config options, e.g. for google ldap, download your certificates from http://admin.google.com/ -> Apps -> LDAP -> Client 15 | download the files and name them "ldap.gsuite.key" and "ldap.gsuite.crt" accordingly (Ensure you have activated your newly created LDAP Client in Google Admin). 16 | 3. Switch to this directory and run "npx radius-server -s YourRadiusSecret" 17 | 4. Log into your WLAN Controller and configure the radius server to your newly running radius 18 | 5. On your clients, just connect to the WLAN, the clients should figure out the correct method by their own, 19 | if they don't use: WPA2-Enterprise -> EAP-TTLS -> PAP / CHAP 20 | 6. Log in with your google credentials (email + password, ... e.g. youremail@yourcompany.com) 21 | 22 | ## Known Issues / Disclaimer 23 | 24 | Support for this has landed in node 13.10.1, therefore ensure you have installed at least this node version. 25 | 26 | - MD5 Challenge not implemented, but RFC says this is mandatory ;-) (no worries, it isn't) 27 | - Inner Tunnel does not act differently, even though spec says that EAP-message are not allowed to get fragmented, 28 | this is not a problem right now, as the messages of the inner tunnel are small enough, but it could be a bug in the future. 29 | ways to approach this: refactor that the inner tunnel can set max fragment size, or rebuild eap fragments in ttls after inner tunnel response 30 | - minor security issues regarding session resumption. It could theoretically be possible to hijack when the auth is actually rejected, but the session is resumed 31 | in the same time frame (sessions are currently not explicitly killed on rejected auths). 32 | 33 | CONTRIBUTIONS WELCOME! If you are willing to help, just open a PR or contact me via bug system or simon.tretter@hokify.com. 34 | 35 | ## Motivation 36 | 37 | ### Why not Freeradius? 38 | 39 | There are several reasons why I started implementing this radius server in node js. We are using 40 | freeradius right now, but have several issues which are hard to tackle due to the reason that freeradius 41 | is a complex software and supports many uses cases. It is also written in C++ and uses threads behind the scene. 42 | Therefore it's not easy to extend or modify it, or even bring new feature in. 43 | The idea of this project is to make a super simple node radius server, which is async by default. No complex 44 | thread handling, no other fancy thing. The basic goal is to make WPA2 authenticiation easy again. 45 | 46 | ### 802.1x protocol in node 47 | 48 | Another motivation is that it is very exciting to see how wireless protocols have evolved, and see 49 | how a implementation like TTLS works. 50 | 51 | ### Few alternatives (only non-free ones like Jumpcloud...) 52 | 53 | Furthermore there are few alternatives out there, e.g. jumpcloud is non-free and I couldn't find many others. 54 | 55 | ### Vision 56 | 57 | As soon as I understood the TTLS PAP Tunnel approach, I had this vision of making Wlan Authentification easy 58 | for everyone. Why limit it to something "complex" like LDAP and co. This library aims to make it easy for everyone 59 | to implement either their own authentication mechanismus (e.g. against a database), or provides some mechansimns 60 | out of the box (e.g. imap, static, ldap,..). 61 | 62 | ## Installation 63 | 64 | npm install 65 | npm run build 66 | 67 | ## Introduction 68 | 69 | This app provides a radius server to authenticate against google's SLDAP service. To get this running 70 | you need: 71 | 72 | 1. Running LDAP Service (E.g. Google Suite Enterprise or Gloud Identity Premium) 73 | 2. Optional: Create your own SSL certificate (e.g. self signed via npm run create-certificate) 74 | 3. Check config.js and adapt to your needs 75 | 76 | - configure authentication: 77 | set authenticaiton to one of the provided authenticators. 78 | 79 | ```js 80 | var config = { 81 | // .... 82 | authentication: 'GoogleLDAPAuth', 83 | authenticationOptions: { 84 | base: 'dc=hokify,dc=com' 85 | } 86 | }; 87 | ``` 88 | 89 | - set radius secret 90 | 91 | 4. Install und build server: npm install && npm run build 92 | 5. Start server "npm run start" 93 | 94 | ## Configuration 95 | 96 | For authentication see [Authentication Details](src/auth/README.md). 97 | For general config options run with --help or see see [config.js](config.js) in root. 98 | 99 | ## Usage 100 | 101 | Ensure you have installed latest node version (>= 13.10.1) and run: 102 | 103 | npm run start 104 | -------------------------------------------------------------------------------- /src/auth/GoogleLDAPAuth.ts: -------------------------------------------------------------------------------- 1 | import { ClientOptions, createClient } from 'ldapjs'; 2 | import debug from 'debug'; 3 | import * as tls from 'tls'; 4 | import * as fs from 'fs'; 5 | import { IAuthentication } from '../types/Authentication'; 6 | 7 | const usernameFields = ['posixUid', 'mail']; 8 | 9 | const log = debug('radius:auth:google-ldap'); 10 | // TLS: 11 | // https://github.com/ldapjs/node-ldapjs/issues/307 12 | 13 | interface IGoogleLDAPAuthOptions { 14 | /** base DN 15 | * e.g. 'dc=hokify,dc=com', */ 16 | base: string; 17 | searchBase?: string; // default ou=users,{{base}} 18 | tls: { 19 | keyFile: string; 20 | certFile: string; 21 | }; 22 | /** tls options 23 | * e.g. { 24 | key: fs.readFileSync('ldap.gsuite.key'), 25 | cert: fs.readFileSync('ldap.gsuite.crt') 26 | } */ 27 | tlsOptions?: tls.TlsOptions; 28 | } 29 | 30 | export class GoogleLDAPAuth implements IAuthentication { 31 | private lastDNsFetch: Date; 32 | 33 | private allValidDNsCache: { [key: string]: string }; 34 | 35 | private base: string; 36 | 37 | private config: ClientOptions; 38 | 39 | searchBase: string; 40 | 41 | constructor(config: IGoogleLDAPAuthOptions) { 42 | this.base = config.base; 43 | this.searchBase = config.searchBase || `ou=users,${this.base}`; 44 | 45 | const tlsOptions = { 46 | key: fs.readFileSync(config.tls.keyFile), 47 | cert: fs.readFileSync(config.tls.certFile), 48 | servername: 'ldap.google.com', 49 | ...config.tlsOptions, 50 | }; 51 | 52 | this.config = { 53 | url: 'ldaps://ldap.google.com:636', 54 | tlsOptions, 55 | }; 56 | 57 | this.fetchDNs().catch((err) => { 58 | console.error('fatal error google ldap auth, cannot fetch DNs', err); 59 | }); 60 | } 61 | 62 | private async fetchDNs() { 63 | const dns: { [key: string]: string } = {}; 64 | 65 | await new Promise((resolve, reject) => { 66 | const ldapDNClient = createClient(this.config).on('error', (error) => { 67 | console.error('Error in ldap', error); 68 | reject(error); 69 | }); 70 | 71 | ldapDNClient.search( 72 | this.searchBase, 73 | { 74 | scope: 'sub', 75 | }, 76 | (err, res) => { 77 | if (err) { 78 | reject(err); 79 | return; 80 | } 81 | 82 | res.on('searchEntry', function (entry) { 83 | // log('entry: ' + JSON.stringify(entry.object)); 84 | usernameFields.forEach((field) => { 85 | const index = entry.object[field] as string; 86 | dns[index] = entry.object.dn; 87 | }); 88 | }); 89 | 90 | res.on('searchReference', function (referral) { 91 | log(`referral: ${referral.uris.join()}`); 92 | }); 93 | 94 | res.on('error', function (ldapErr) { 95 | console.error(`error: ${JSON.stringify(ldapErr)}`); 96 | reject(ldapErr); 97 | }); 98 | 99 | res.on('end', (result) => { 100 | log(`ldap status: ${result?.status}`); 101 | 102 | // replace with new dns 103 | this.allValidDNsCache = dns; 104 | // log('allValidDNsCache', this.allValidDNsCache); 105 | resolve(); 106 | }); 107 | } 108 | ); 109 | }); 110 | this.lastDNsFetch = new Date(); 111 | } 112 | 113 | async authenticate(username: string, password: string, count = 0, forceFetching = false) { 114 | const cacheValidTime = new Date(); 115 | cacheValidTime.setHours(cacheValidTime.getHours() - 12); 116 | 117 | /* 118 | just a test for super slow google responses 119 | await new Promise((resolve, reject) => { 120 | setTimeout(resolve, 10000); // wait 10 seconds 121 | }) 122 | */ 123 | 124 | let dnsFetched = false; 125 | 126 | if (!this.lastDNsFetch || this.lastDNsFetch < cacheValidTime || forceFetching) { 127 | log('fetching dns'); 128 | await this.fetchDNs(); 129 | dnsFetched = true; 130 | } 131 | 132 | if (count > 5) { 133 | throw new Error('Failed to authenticate with LDAP!'); 134 | } 135 | // const dn = ; 136 | const dn = this.allValidDNsCache[username]; 137 | if (!dn) { 138 | if (!dnsFetched && !forceFetching) { 139 | return this.authenticate(username, password, count, true); 140 | } 141 | // console.log('this.allValidDNsCache', this.allValidDNsCache); 142 | console.error(`invalid username, not found in DN: ${username}`); // , this.allValidDNsCache); 143 | return false; 144 | } 145 | 146 | const authResult: boolean = await new Promise((resolve, reject) => { 147 | // we never unbding a client, therefore create a new client every time 148 | const authClient = createClient(this.config); 149 | 150 | authClient.bind(dn, password, (err, res) => { 151 | if (err) { 152 | if (err && (err as any).stack && (err as any).stack.includes(`ldap.google.com closed`)) { 153 | count++; 154 | // wait 1 second to give the ldap error handler time to reconnect 155 | setTimeout(() => resolve(this.authenticate(dn, password)), 2000); 156 | return; 157 | } 158 | 159 | resolve(false); 160 | // console.error('ldap error', err); 161 | // reject(err); 162 | } 163 | if (res) resolve(res); 164 | else reject(); 165 | 166 | authClient.unbind(); 167 | }); 168 | }); 169 | 170 | return !!authResult; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/radius/handler/EAPPacketHandler.ts: -------------------------------------------------------------------------------- 1 | // https://tools.ietf.org/html/rfc3748#section-4.1 2 | 3 | import * as NodeCache from 'node-cache'; 4 | import debug from 'debug'; 5 | import { makeid } from '../../helpers'; 6 | import { IPacket, IPacketHandler, IPacketHandlerResult } from '../../types/PacketHandler'; 7 | import { IEAPMethod } from '../../types/EAPMethod'; 8 | import { buildEAPResponse, decodeEAPHeader } from './eap/EAPHelper'; 9 | 10 | const log = debug('radius:eap'); 11 | 12 | export class EAPPacketHandler implements IPacketHandler { 13 | private identities = new NodeCache({ useClones: false, stdTTL: 60 }); // queue data maximum for 60 seconds 14 | 15 | // private eapConnectionStates: { [key: string]: { validMethods: IEAPMethod[] } } = {}; 16 | private eapConnectionStates = new NodeCache({ useClones: false, stdTTL: 3600 }); // max for one hour 17 | 18 | constructor(private eapMethods: IEAPMethod[]) {} 19 | 20 | async handlePacket(packet: IPacket, handlingType?: number): Promise { 21 | if (!packet.attributes['EAP-Message']) { 22 | // not an EAP message 23 | return {}; 24 | } 25 | 26 | const stateID = (packet.attributes.State && packet.attributes.State.toString()) || makeid(16); 27 | 28 | if (!this.eapConnectionStates.get(stateID)) { 29 | this.eapConnectionStates.set(stateID, { 30 | validMethods: this.eapMethods.filter((eap) => eap.getEAPType() !== handlingType), // on init all registered eap methods are valid, we kick them out in case we get a NAK response 31 | }); 32 | } 33 | 34 | // EAP MESSAGE 35 | let msg = packet.attributes['EAP-Message'] as Buffer; 36 | 37 | if (Array.isArray(msg) && !(packet.attributes['EAP-Message'] instanceof Buffer)) { 38 | // log('multiple EAP Messages received, concat', msg.length); 39 | const allMsgs = msg as Buffer[]; 40 | msg = Buffer.concat(allMsgs); 41 | // log('final EAP Message', msg); 42 | } 43 | 44 | try { 45 | const { code, type, identifier, data } = decodeEAPHeader(msg); 46 | 47 | const currentState = this.eapConnectionStates.get(stateID) as { validMethods: IEAPMethod[] }; 48 | 49 | switch (code) { 50 | case 1: // for request 51 | case 2: // for response 52 | switch (type) { 53 | case 1: // identifiy 54 | log('>>>>>>>>>>>> REQUEST FROM CLIENT: IDENTIFY', stateID, data.toString()); 55 | if (data) { 56 | this.identities.set(stateID, data); // use token til binary 0.); 57 | } else { 58 | log('no msg'); 59 | } 60 | 61 | // start identify 62 | if (currentState.validMethods.length > 0) { 63 | return currentState.validMethods[0].identify(identifier, stateID, data); 64 | } 65 | 66 | return buildEAPResponse(identifier, 3); // NAK 67 | case 2: // notification 68 | log('>>>>>>>>>>>> REQUEST FROM CLIENT: notification', {}); 69 | console.info('notification'); 70 | break; 71 | case 4: // md5-challenge 72 | log('>>>>>>>>>>>> REQUEST FROM CLIENT: md5-challenge', {}); 73 | 74 | console.info('md5-challenge'); 75 | break; 76 | case 254: // expanded type 77 | console.error('not implemented type', type); 78 | break; 79 | case 3: // nak 80 | // console.log('got NAK', data); 81 | if (data) { 82 | // if there is data, each data octect reprsents a eap method the clients supports, 83 | // kick out all unsupported ones 84 | const supportedEAPMethods: number[] = []; 85 | for (const supportedMethod of data) { 86 | supportedEAPMethods.push(supportedMethod); 87 | } 88 | 89 | currentState.validMethods = currentState.validMethods.filter((method) => { 90 | return supportedEAPMethods.includes(method.getEAPType()); // kick it out? 91 | }); 92 | // save 93 | this.eapConnectionStates.set(stateID, currentState); 94 | 95 | // new identidy request 96 | // start identify 97 | if (currentState.validMethods.length > 0) { 98 | return currentState.validMethods[0].identify(identifier, stateID, data); 99 | } 100 | } 101 | // continue with responding a NAK and add rest of supported methods 102 | // eslint-disable-next-line no-fallthrough 103 | default: { 104 | const eapMethod = this.eapMethods.find((method) => { 105 | return type === method.getEAPType(); 106 | }); 107 | 108 | if (eapMethod) { 109 | return eapMethod.handleMessage( 110 | identifier, 111 | stateID, 112 | msg, 113 | packet, 114 | this.identities.get(stateID) 115 | ); 116 | } 117 | 118 | // we do not support this auth type, ask for something we support 119 | const serverSupportedMethods = currentState.validMethods.map((method) => 120 | method.getEAPType() 121 | ); 122 | 123 | console.error('unsupported type', type, `requesting: ${serverSupportedMethods}`); 124 | 125 | return buildEAPResponse(identifier, 3, Buffer.from(serverSupportedMethods)); 126 | } 127 | } 128 | break; 129 | case 3: 130 | log('Client Auth Success'); 131 | break; 132 | case 4: 133 | log('Client Auth FAILURE'); 134 | break; 135 | default: 136 | } 137 | // silently ignore; 138 | return {}; 139 | } catch (err) { 140 | console.error( 141 | 'decoding of (generic) EAP package failed', 142 | msg, 143 | err, 144 | this.eapConnectionStates.get(stateID) 145 | ); 146 | return {}; 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [1.1.10](https://github.com/simllll/node-radius-server/compare/v1.1.9...v1.1.10) (2020-12-01) 6 | 7 | ### [1.1.9](https://github.com/simllll/node-radius-server/compare/v1.1.8...v1.1.9) (2020-09-03) 8 | 9 | ### [1.1.8](https://github.com/simllll/node-radius-server/compare/v1.1.7...v1.1.8) (2020-09-03) 10 | 11 | 12 | ### Bug Fixes 13 | 14 | * **tls:** allow tls v1.0 ([55e6dc4](https://github.com/simllll/node-radius-server/commit/55e6dc4b0b2ddc2d24704a0ef57c56b8905e7aa4)) 15 | 16 | ### [1.1.7](https://github.com/simllll/node-radius-server/compare/v1.1.5...v1.1.7) (2020-08-05) 17 | 18 | 19 | ### Bug Fixes 20 | 21 | * session resumptions + google ldap auth ([66fbcf4](https://github.com/simllll/node-radius-server/commit/66fbcf4ca83cc6c3813b0015c0e2d8f69c8db6e6)) 22 | 23 | ### [1.1.6](https://github.com/simllll/node-radius-server/compare/v1.1.5...v1.1.6) (2020-08-05) 24 | 25 | 26 | ### Bug Fixes 27 | 28 | * **google-auth:** search base must include ou ([a3ab393](https://github.com/simllll/node-radius-server/commit/a3ab393379be7f1b8b2f82347bbc4300b8db409d)) 29 | 30 | ### [1.1.5](https://github.com/simllll/node-radius-server/compare/v1.1.4...v1.1.5) (2020-06-26) 31 | 32 | 33 | ### Bug Fixes 34 | 35 | * **eap:** catch decoding errors ([97ea3fa](https://github.com/simllll/node-radius-server/commit/97ea3fad1d8d1d79eab38ee5e45e17ef6ed20caa)) 36 | * **eap:** concat buffers if they are an array ([3d03658](https://github.com/simllll/node-radius-server/commit/3d03658a43a924017ad5da84599f57a21b3ae27e)) 37 | * **eap:** output state on error ([e71f0b3](https://github.com/simllll/node-radius-server/commit/e71f0b3d804070f67ad2d42880c516ce612ee7b0)) 38 | * **eap-ttls:** reset last processed identifier ([7179c16](https://github.com/simllll/node-radius-server/commit/7179c1682d33ead0e00d7aae17a97428f1fa4ea5)) 39 | 40 | ### [1.1.4](https://github.com/simllll/node-radius-server/compare/v1.1.3...v1.1.4) (2020-06-24) 41 | 42 | ### [1.1.3](https://github.com/simllll/node-radius-server/compare/v1.1.2...v1.1.3) (2020-05-14) 43 | 44 | ### [1.1.2](https://github.com/simllll/node-radius-server/compare/v1.1.1...v1.1.2) (2020-03-02) 45 | 46 | ### [1.1.1](https://github.com/simllll/node-radius-server/compare/v1.1.0...v1.1.1) (2020-03-02) 47 | 48 | 49 | ### Bug Fixes 50 | 51 | * **auth:** cache only valid results for one day ([a8a0247](https://github.com/simllll/node-radius-server/commit/a8a02478ce522eb51c46517b4176aa0d50481676)) 52 | * **config:** use __dirname to resolve path to ssl certs ([7a28a0d](https://github.com/simllll/node-radius-server/commit/7a28a0dc6bfbae307765e03f4b15c57c84fa0dc2)) 53 | * **docs:** fix some typos ([5519391](https://github.com/simllll/node-radius-server/commit/5519391aa3c688422da8d98a3bd789615738b974)) 54 | 55 | ## [1.1.0](https://github.com/simllll/node-radius-server/compare/v1.0.0...v1.1.0) (2020-02-28) 56 | 57 | 58 | ### Features 59 | 60 | * **cli:** allow setting config vars via cli ([d9ff95b](https://github.com/simllll/node-radius-server/commit/d9ff95bbbbea9ade9721e3f5d4dc2323988da3d6)) 61 | 62 | ## 1.0.0 (2020-02-27) 63 | 64 | 65 | ### Features 66 | 67 | * **ssl:** enable session resumptions for even quicker reintinaliztions :) ([e1b4bb5](https://github.com/simllll/node-radius-server/commit/e1b4bb5597ac74f10b120a5f8cfef7b407a48c8f)) 68 | * add debug pkg to reduce output ([9fe25a8](https://github.com/simllll/node-radius-server/commit/9fe25a8b497071ea9276785b7f7710ae0e1e88f8)) 69 | * add more auth providers and cleanup google auth ([3f600c6](https://github.com/simllll/node-radius-server/commit/3f600c664ffa7315053d47773c7f9d5060b68d32)) 70 | * inner tunnel for TTSL support added ([6aa4b9f](https://github.com/simllll/node-radius-server/commit/6aa4b9f92efb271ee327d3d70bccba27284304ee)) 71 | 72 | 73 | ### Bug Fixes 74 | 75 | * **docs:** better file names ([5897498](https://github.com/simllll/node-radius-server/commit/589749883c4c881c3af753530987d6f57d8d809d)) 76 | * improve coping with long running auth requests ([7ca60a2](https://github.com/simllll/node-radius-server/commit/7ca60a20cc24eb8100ed1f20fe18e7ec664fd176)) 77 | * **auth:** improve google auth ([0baf815](https://github.com/simllll/node-radius-server/commit/0baf8155bf74fed9e08826b1aea8242f72c81878)) 78 | * **docs:** add examples ([a3ed0be](https://github.com/simllll/node-radius-server/commit/a3ed0be02db0a7fcd89544c89d9b0ee11e949808)) 79 | * docs ([cca4dce](https://github.com/simllll/node-radius-server/commit/cca4dce96142d2b2d04b419bd7500e3841262235)) 80 | * ldap auth failed auth and added test scripts ([5e5005c](https://github.com/simllll/node-radius-server/commit/5e5005cf6bcbc3d9450db3651478249f8deb92a6)) 81 | * ssl again ([a624bc1](https://github.com/simllll/node-radius-server/commit/a624bc15b0e1fde4f2a268c62500b090e4f366a5)) 82 | * **ssl:** move files ([f53a423](https://github.com/simllll/node-radius-server/commit/f53a42335bb583af7575b8cf5fcf5fe58cdeaed4)) 83 | * a lot of bug fixes, first running version for windows and android :) ([0cb807a](https://github.com/simllll/node-radius-server/commit/0cb807a555febec461edf1280fe1a7e1b72186b0)) 84 | * a lot of bug fixes, first running version for windows and android :) ([4989c2b](https://github.com/simllll/node-radius-server/commit/4989c2b6bc162a1688e84c21919835cb8637854c)) 85 | * add MS-MPPE-Send-Key and MS-MPPE-Recv-Key ([7e28c60](https://github.com/simllll/node-radius-server/commit/7e28c60d81abe4c2c5269babbf6ef5951d65d682)) 86 | * eap call is using wrong this, needs more refactoring later ([837453f](https://github.com/simllll/node-radius-server/commit/837453fca250abb45f1069405b96e29fc0e3e9c4)) 87 | 88 | # Changelog 89 | 90 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 91 | -------------------------------------------------------------------------------- /src/tls/crypt.ts: -------------------------------------------------------------------------------- 1 | import * as events from 'events'; 2 | import * as tls from 'tls'; 3 | import { createSecureContext } from 'tls'; 4 | import * as crypto from 'crypto'; 5 | import * as DuplexPair from 'native-duplexpair'; 6 | import debug from 'debug'; 7 | import * as NodeCache from 'node-cache'; 8 | // import * as constants from 'constants'; 9 | import * as config from '../../config'; 10 | 11 | const log = debug('radius:tls'); 12 | 13 | // https://nodejs.org/api/tls.html 14 | const tlsOptions: tls.SecureContextOptions = { 15 | ...config.certificate, 16 | }; 17 | log('tlsOptions', tlsOptions); 18 | const secureContext = createSecureContext(tlsOptions); 19 | 20 | export interface ITLSServer { 21 | events: events.EventEmitter; 22 | tls: tls.TLSSocket; 23 | } 24 | 25 | const resumeSessions = new NodeCache({ stdTTL: 86400 }); // session reidentification maximum 1 day 26 | 27 | export function startTLSServer(): ITLSServer { 28 | const duplexpair = new DuplexPair(); 29 | const emitter = new events.EventEmitter(); 30 | 31 | const cleartext = new tls.TLSSocket(duplexpair.socket1, { 32 | secureContext, 33 | isServer: true, 34 | // enableTrace: true, 35 | rejectUnauthorized: false, 36 | // handshakeTimeout: 10, 37 | requestCert: false, 38 | }); 39 | const encrypted = duplexpair.socket2; 40 | 41 | // for older tls versions without ticketing support 42 | cleartext.on('newSession', (sessionId: Buffer, sessionData: Buffer, callback: () => void) => { 43 | log(`TLS new session (${sessionId.toString('hex')})`); 44 | 45 | resumeSessions.set(sessionId.toString('hex'), sessionData); 46 | callback(); 47 | }); 48 | 49 | cleartext.on( 50 | 'resumeSession', 51 | (sessionId: Buffer, callback: (err: Error | null, sessionData: Buffer | null) => void) => { 52 | const resumedSession = (resumeSessions.get(sessionId.toString('hex')) as Buffer) || null; 53 | 54 | if (resumedSession) { 55 | log(`TLS resumed session (${sessionId.toString('hex')})`); 56 | } 57 | 58 | callback(null, resumedSession); 59 | } 60 | ); 61 | 62 | emitter.on('decrypt', (data: Buffer) => { 63 | encrypted.write(data); 64 | // encrypted.sync(); 65 | }); 66 | 67 | emitter.on('encrypt', (data: Buffer) => { 68 | cleartext.write(data); 69 | // encrypted.sync(); 70 | }); 71 | 72 | encrypted.on('data', (data: Buffer) => { 73 | // log('encrypted data', data, data.toString()); 74 | emitter.emit('response', data); 75 | }); 76 | 77 | cleartext.on('secure', () => { 78 | const cipher = cleartext.getCipher(); 79 | 80 | if (cipher) { 81 | log(`TLS negotiated (${cipher.name}, ${cipher.version})`); 82 | } 83 | 84 | cleartext.on('data', (data: Buffer) => { 85 | // log('cleartext data', data, data.toString()); 86 | emitter.emit('incoming', data); 87 | }); 88 | 89 | cleartext.once('close', (_data: Buffer) => { 90 | log('cleartext close'); 91 | emitter.emit('end'); 92 | }); 93 | 94 | cleartext.on('keylog', (line) => { 95 | log('############ KEYLOG #############', line); 96 | // cleartext.getTicketKeys() 97 | }); 98 | 99 | log('*********** new TLS connection established / secured ********'); 100 | emitter.emit('secured', cleartext.isSessionReused()); 101 | }); 102 | 103 | cleartext.on('error', (err?: Error) => { 104 | log('cleartext error', err); 105 | 106 | encrypted.destroy(); 107 | cleartext.destroy(err); 108 | 109 | emitter.emit('end'); 110 | }); 111 | 112 | return { 113 | events: emitter, 114 | tls: cleartext, 115 | }; 116 | } 117 | 118 | function md5Hex(buffer: Buffer): Buffer { 119 | const hasher = crypto.createHash('md5'); 120 | hasher.update(buffer); 121 | return hasher.digest(); // new Buffer(hasher.digest("binary"), "binary"); 122 | } 123 | 124 | /* 125 | const buffer = tlsSocket.exportKeyingMaterial(128, 'ttls keying material'); 126 | 127 | EAP_TLS_KEY from 0 to 64 128 | EAP_EMSK from 64 to 128 129 | */ 130 | 131 | export function encodeTunnelPW(key: Buffer, authenticator: Buffer, secret: string): Buffer { 132 | // see freeradius TTLS implementation how to obtain "key"...... 133 | // https://tools.ietf.org/html/rfc2548 134 | 135 | /** 136 | * Salt 137 | The Salt field is two octets in length and is used to ensure the 138 | uniqueness of the keys used to encrypt each of the encrypted 139 | attributes occurring in a given Access-Accept packet. The most 140 | significant bit (leftmost) of the Salt field MUST be set (1). The 141 | contents of each Salt field in a given Access-Accept packet MUST 142 | be unique. 143 | */ 144 | const salt = crypto.randomBytes(2); 145 | 146 | // eslint-disable-next-line no-bitwise 147 | salt[0] |= 0b10000000; // ensure leftmost bit is set to 1 148 | 149 | /* 150 | String 151 | The plaintext String field consists of three logical sub-fields: 152 | the Key-Length and Key sub-fields (both of which are required), 153 | and the optional Padding sub-field. The Key-Length sub-field is 154 | one octet in length and contains the length of the unencrypted Key 155 | sub-field. The Key sub-field contains the actual encryption key. 156 | If the combined length (in octets) of the unencrypted Key-Length 157 | and Key sub-fields is not an even multiple of 16, then the Padding 158 | sub-field MUST be present. If it is present, the length of the 159 | Padding sub-field is variable, between 1 and 15 octets. The 160 | String field MUST be encrypted as follows, prior to transmission: 161 | 162 | Construct a plaintext version of the String field by concate- 163 | nating the Key-Length and Key sub-fields. If necessary, pad 164 | the resulting string until its length (in octets) is an even 165 | multiple of 16. It is recommended that zero octets (0x00) be 166 | used for padding. Call this plaintext P. 167 | */ 168 | 169 | let P = Buffer.concat([new Uint8Array([key.length]), key]); // + key + padding; 170 | 171 | // fill up with 0x00 till we have % 16 172 | while (P.length % 16 !== 0) { 173 | P = Buffer.concat([P, Buffer.from([0x00])]); 174 | } 175 | 176 | /* 177 | Call the shared secret S, the pseudo-random 128-bit Request 178 | Authenticator (from the corresponding Access-Request packet) R, 179 | and the contents of the Salt field A. Break P into 16 octet 180 | chunks p(1), p(2)...p(i), where i = len(P)/16. Call the 181 | ciphertext blocks c(1), c(2)...c(i) and the final ciphertext C. 182 | Intermediate values b(1), b(2)...c(i) are required. Encryption 183 | is performed in the following manner ('+' indicates 184 | concatenation): 185 | 186 | Zorn Informational [Page 21] 187 | 188 | RFC 2548 Microsoft Vendor-specific RADIUS Attributes March 1999 189 | 190 | 191 | b(1) = MD5(S + R + A) c(1) = p(1) xor b(1) C = c(1) 192 | b(2) = MD5(S + c(1)) c(2) = p(2) xor b(2) C = C + c(2) 193 | . . 194 | . . 195 | . . 196 | b(i) = MD5(S + c(i-1)) c(i) = p(i) xor b(i) C = C + c(i) 197 | 198 | The resulting encrypted String field will contain 199 | c(1)+c(2)+...+c(i). 200 | */ 201 | 202 | const p: Buffer[] = []; 203 | for (let i = 0; i < P.length; i += 16) { 204 | p.push(P.slice(i, i + 16)); 205 | } 206 | 207 | const S = secret; 208 | const R = authenticator; 209 | const A = salt; 210 | 211 | let C; 212 | const c: { [key: number]: Buffer } = {}; 213 | const b: { [key: number]: Buffer } = {}; 214 | 215 | for (let i = 0; i < p.length; i++) { 216 | if (!i) { 217 | b[i] = md5Hex(Buffer.concat([Buffer.from(S), R, A])); 218 | } else { 219 | b[i] = md5Hex(Buffer.concat([Buffer.from(S), c[i - 1]])); 220 | } 221 | 222 | c[i] = Buffer.alloc(16); // ''; //p[i]; 223 | for (let n = 0; n < p[i].length; n++) { 224 | // eslint-disable-next-line no-bitwise 225 | c[i][n] = p[i][n] ^ b[i][n]; 226 | } 227 | 228 | C = C ? Buffer.concat([C, c[i]]) : c[i]; 229 | } 230 | 231 | const bufferC = Buffer.from(C); 232 | 233 | return Buffer.concat([salt, bufferC]); 234 | } 235 | -------------------------------------------------------------------------------- /src/radius/handler/eap/eapMethods/EAP-TTLS.ts: -------------------------------------------------------------------------------- 1 | // https://tools.ietf.org/html/rfc5281 TTLS v0 2 | // https://tools.ietf.org/html/draft-funk-eap-ttls-v1-00 TTLS v1 (not implemented) 3 | /* eslint-disable no-bitwise */ 4 | import * as tls from 'tls'; 5 | import * as NodeCache from 'node-cache'; 6 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 7 | // @ts-ignore 8 | import { attr_id_to_name, attr_name_to_id } from 'radius'; 9 | import debug from 'debug'; 10 | 11 | import { encodeTunnelPW, ITLSServer, startTLSServer } from '../../../../tls/crypt'; 12 | import { 13 | IPacket, 14 | IPacketAttributes, 15 | IPacketHandler, 16 | IPacketHandlerResult, 17 | PacketResponseCode, 18 | } from '../../../../types/PacketHandler'; 19 | import { MAX_RADIUS_ATTRIBUTE_SIZE, newDeferredPromise } from '../../../../helpers'; 20 | import { IEAPMethod } from '../../../../types/EAPMethod'; 21 | import { IAuthentication } from '../../../../types/Authentication'; 22 | import { secret } from '../../../../../config'; 23 | 24 | const log = debug('radius:eap:ttls'); 25 | 26 | function tlsHasExportKeyingMaterial( 27 | tlsSocket 28 | ): tlsSocket is { 29 | exportKeyingMaterial: (length: number, label: string, context?: Buffer) => Buffer; 30 | } { 31 | return typeof (tlsSocket as any).exportKeyingMaterial === 'function'; 32 | } 33 | 34 | interface IAVPEntry { 35 | type: number; 36 | flags: string; 37 | decodedFlags: { 38 | V: boolean; 39 | M: boolean; 40 | }; 41 | length: number; 42 | vendorId?: number; 43 | data: Buffer; 44 | } 45 | 46 | export class EAPTTLS implements IEAPMethod { 47 | private lastProcessedIdentifier = new NodeCache({ useClones: false, stdTTL: 60 }); 48 | 49 | // { [key: string]: Buffer } = {}; 50 | private queueData = new NodeCache({ useClones: false, stdTTL: 60 }); // queue data maximum for 60 seconds 51 | 52 | private openTLSSockets = new NodeCache({ useClones: false, stdTTL: 3600 }); // keep sockets for about one hour 53 | 54 | getEAPType(): number { 55 | return 21; 56 | } 57 | 58 | identify(identifier: number, stateID: string): IPacketHandlerResult { 59 | return this.buildEAPTTLSResponse(identifier, 21, 0x20, stateID); 60 | } 61 | 62 | constructor(private authentication: IAuthentication, private innerTunnel: IPacketHandler) {} 63 | 64 | private buildEAPTTLS( 65 | identifier: number, 66 | msgType = 21, 67 | msgFlags = 0x00, 68 | stateID: string, 69 | data?: Buffer, 70 | newResponse = true, 71 | maxSize = (MAX_RADIUS_ATTRIBUTE_SIZE - 5) * 4 72 | ): Buffer { 73 | log('maxSize', maxSize); 74 | 75 | /* it's the first one and we have more, therefore include length */ 76 | const includeLength = maxSize > 0 && data && newResponse && data.length > maxSize; 77 | 78 | // extract data party 79 | const dataToSend = maxSize > 0 ? data && data.length > 0 && data.slice(0, maxSize) : data; 80 | const dataToQueue = maxSize > 0 && data && data.length > maxSize && data.slice(maxSize); 81 | 82 | /* 83 | 0 1 2 3 4 5 6 7 8 84 | +-+-+-+-+-+-+-+-+ 85 | |L M R R R R R R| 86 | +-+-+-+-+-+-+-+-+ 87 | 88 | L = Length included 89 | M = More fragments 90 | R = Reserved 91 | 92 | The L bit (length included) is set to indicate the presence of the 93 | four-octet TLS Message Length field, and MUST be set for the first 94 | fragment of a fragmented TLS message or set of messages. The M 95 | bit (more fragments) is set on all but the last fragment. 96 | Implementations of this specification MUST set the reserved bits 97 | to zero, and MUST ignore them on reception. 98 | */ 99 | 100 | const flags = 101 | msgFlags + 102 | (includeLength ? 0b10000000 : 0) + // set L bit 103 | (dataToQueue && dataToQueue.length > 0 ? 0b01000000 : 0); // we have more data to come, set M bit 104 | 105 | let buffer = Buffer.from([ 106 | 1, // request 107 | identifier + 1, // increase id by 1 108 | 0, // length (1/2) 109 | 0, // length (2/2) 110 | msgType, // 1 = identity, 21 = EAP-TTLS, 2 = notificaiton, 4 = md5-challenge, 3 = NAK 111 | flags, // flags: 000000 (L include lenghts, M .. more to come) 112 | ]); 113 | 114 | // append length 115 | if (includeLength && data) { 116 | const length = Buffer.alloc(4); 117 | length.writeInt32BE(data.byteLength, 0); 118 | 119 | buffer = Buffer.concat([buffer, length]); 120 | } 121 | 122 | // build final buffer with data 123 | const resBuffer = dataToSend ? Buffer.concat([buffer, dataToSend]) : buffer; 124 | 125 | // set EAP length header 126 | resBuffer.writeUInt16BE(resBuffer.byteLength, 2); 127 | 128 | log('<<<<<<<<<<<< EAP RESPONSE TO CLIENT', { 129 | code: 1, 130 | identifier: identifier + 1, 131 | includeLength, 132 | dataLength: (data && data.byteLength) || 0, 133 | msgType: msgType.toString(10), 134 | flags: `00000000${flags.toString(2)}`.substr(-8), 135 | data, 136 | }); 137 | 138 | if (dataToQueue) { 139 | // we couldn't send all at once, queue the rest and send later 140 | this.queueData.set(stateID, dataToQueue); 141 | } else { 142 | this.queueData.del(stateID); 143 | } 144 | 145 | return resBuffer; 146 | } 147 | 148 | private buildEAPTTLSResponse( 149 | identifier: number, 150 | msgType = 21, 151 | msgFlags = 0x00, 152 | stateID: string, 153 | data?: Buffer, 154 | newResponse = true 155 | ): IPacketHandlerResult { 156 | const resBuffer = this.buildEAPTTLS(identifier, msgType, msgFlags, stateID, data, newResponse); 157 | 158 | const attributes: any = [['State', Buffer.from(stateID)]]; 159 | let sentDataSize = 0; 160 | do { 161 | if (resBuffer.length > 0) { 162 | attributes.push([ 163 | 'EAP-Message', 164 | resBuffer.slice(sentDataSize, sentDataSize + MAX_RADIUS_ATTRIBUTE_SIZE), 165 | ]); 166 | sentDataSize += MAX_RADIUS_ATTRIBUTE_SIZE; 167 | } 168 | } while (sentDataSize < resBuffer.length); 169 | 170 | return { 171 | code: PacketResponseCode.AccessChallenge, 172 | attributes, 173 | }; 174 | } 175 | 176 | decodeTTLSMessage(msg: Buffer) { 177 | /** 178 | * The EAP-TTLS packet format is shown below. The fields are 179 | transmitted left to right. 180 | 181 | 0 1 2 3 182 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 183 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 184 | | Code | Identifier | Length | 185 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 186 | | Type | Flags | Message Length 187 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 188 | Message Length | Data... 189 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 190 | */ 191 | const identifier = msg.slice(1, 2).readUInt8(0); 192 | const flags = msg.slice(5, 6).readUInt8(0); // .toString('hex'); 193 | /* 194 | 0 1 2 3 4 5 6 7 195 | +---+---+---+---+---+---+---+---+ 196 | | L | M | S | R | R | V | 197 | +---+---+---+---+---+---+---+---+ 198 | 199 | L = Length included 200 | M = More fragments 201 | S = Start 202 | R = Reserved 203 | V = Version (000 for EAP-TTLSv0) 204 | */ 205 | const decodedFlags = { 206 | // L 207 | lengthIncluded: !!(flags & 0b10000000), 208 | // M 209 | moreFragments: !!(flags & 0b01000000), 210 | // S 211 | start: !!(flags & 0b00100000), 212 | // R 213 | // reserved: flags & 0b00011000, 214 | // V 215 | version: flags & 0b00000111, 216 | }; 217 | 218 | let msglength; 219 | if (decodedFlags.lengthIncluded) { 220 | msglength = msg.slice(6, 10).readUInt32BE(0); // .readDoubleLE(0); // .toString('hex'); 221 | } 222 | const data = msg.slice(decodedFlags.lengthIncluded ? 10 : 6).slice(0, msglength); 223 | 224 | log('>>>>>>>>>>>> REQUEST FROM CLIENT: EAP TTLS', { 225 | flags: `00000000${flags.toString(2)}`.substr(-8), 226 | decodedFlags, 227 | identifier, 228 | msglengthBuffer: msg.length, 229 | msglength, 230 | data, 231 | // dataStr: data.toString() 232 | }); 233 | 234 | return { 235 | decodedFlags, 236 | msglength, 237 | data, 238 | }; 239 | } 240 | 241 | authResponse( 242 | identifier: number, 243 | success: boolean, 244 | socket: tls.TLSSocket, 245 | packet: IPacket 246 | ): IPacketHandlerResult { 247 | const buffer = Buffer.from([ 248 | success ? 3 : 4, // 3.. success, 4... failure 249 | identifier + 1, 250 | 0, // length (1/2) 251 | 4, // length (2/2) 252 | ]); 253 | 254 | const attributes: any[] = []; 255 | attributes.push(['EAP-Message', buffer]); 256 | 257 | if (packet.attributes && packet.attributes['User-Name']) { 258 | // reappend username to response 259 | attributes.push(['User-Name', packet.attributes['User-Name'].toString()]); 260 | } 261 | 262 | if (tlsHasExportKeyingMaterial(socket)) { 263 | const keyingMaterial = socket.exportKeyingMaterial(128, 'ttls keying material'); 264 | 265 | if (!packet.authenticator) { 266 | throw new Error('FATAL: no packet authenticator variable set'); 267 | } 268 | 269 | attributes.push([ 270 | 'Vendor-Specific', 271 | 311, 272 | [[16, encodeTunnelPW(keyingMaterial.slice(64), packet.authenticator, secret)]], 273 | ]); // MS-MPPE-Send-Key 274 | 275 | attributes.push([ 276 | 'Vendor-Specific', 277 | 311, 278 | [[17, encodeTunnelPW(keyingMaterial.slice(0, 64), packet.authenticator, secret)]], 279 | ]); // MS-MPPE-Recv-Key 280 | } else { 281 | console.error( 282 | 'FATAL: no exportKeyingMaterial method available!!!, you need latest NODE JS, see https://github.com/nodejs/node/pull/31814' 283 | ); 284 | } 285 | 286 | return { 287 | code: success ? PacketResponseCode.AccessAccept : PacketResponseCode.AccessReject, 288 | attributes, 289 | }; 290 | } 291 | 292 | async handleMessage( 293 | identifier: number, 294 | stateID: string, 295 | msg: Buffer, 296 | packet: IPacket 297 | ): Promise { 298 | if (identifier === this.lastProcessedIdentifier.get(stateID)) { 299 | log(`ignoring message ${identifier}, because it's processing already... ${stateID}`); 300 | 301 | return {}; 302 | } 303 | this.lastProcessedIdentifier.set(stateID, identifier); 304 | try { 305 | const { data } = this.decodeTTLSMessage(msg); 306 | 307 | // check if no data package is there and we have something in the queue, if so.. empty the queue first 308 | if (!data || data.length === 0) { 309 | const queuedData = this.queueData.get(stateID); 310 | if (queuedData instanceof Buffer && queuedData.length > 0) { 311 | log(`returning queued data for ${stateID}`); 312 | return this.buildEAPTTLSResponse(identifier, 21, 0x00, stateID, queuedData, false); 313 | } 314 | 315 | log(`empty data queue for ${stateID}`); 316 | return {}; 317 | } 318 | 319 | let connection = this.openTLSSockets.get(stateID) as ITLSServer; 320 | 321 | if (!connection) { 322 | connection = startTLSServer(); 323 | this.openTLSSockets.set(stateID, connection); 324 | 325 | connection.events.on('end', () => { 326 | // cleanup socket 327 | log('ENDING SOCKET'); 328 | this.openTLSSockets.del(stateID); 329 | }); 330 | } 331 | 332 | const sendResponsePromise = newDeferredPromise(); 333 | 334 | const incomingMessageHandler = async (incomingData: Buffer) => { 335 | const ret: any = {}; 336 | ret.attributes = {}; 337 | ret.raw_attributes = []; 338 | 339 | const AVPs = this.decodeAVPs(incomingData); 340 | 341 | // build attributes for packet handler 342 | const attributes: IPacketAttributes = {}; 343 | AVPs.forEach((avp) => { 344 | attributes[attr_id_to_name(avp.type)] = avp.data; 345 | }); 346 | 347 | attributes.State = `${stateID}-inner`; 348 | 349 | // handle incoming package via inner tunnel 350 | const result = await this.innerTunnel.handlePacket( 351 | { 352 | attributes, 353 | }, 354 | this.getEAPType() 355 | ); 356 | 357 | log('inner tunnel result', result); 358 | 359 | if ( 360 | result.code === PacketResponseCode.AccessReject || 361 | result.code === PacketResponseCode.AccessAccept 362 | ) { 363 | sendResponsePromise.resolve( 364 | this.authResponse( 365 | identifier, 366 | result.code === PacketResponseCode.AccessAccept, 367 | connection.tls, 368 | { 369 | ...packet, 370 | attributes: { 371 | ...packet.attributes, 372 | ...this.transformAttributesArrayToMap(result.attributes), 373 | }, 374 | } 375 | ) 376 | ); 377 | return; 378 | } 379 | 380 | const eapMessage = result.attributes?.find((attr) => attr[0] === 'EAP-Message'); 381 | if (!eapMessage) { 382 | throw new Error('no eap message found'); 383 | } 384 | 385 | connection.events.emit( 386 | 'encrypt', 387 | this.buildAVP(attr_name_to_id('EAP-Message'), eapMessage[1] as Buffer) 388 | ); 389 | }; 390 | 391 | const responseHandler = (encryptedResponseData: Buffer) => { 392 | log('complete'); 393 | // send back... 394 | sendResponsePromise.resolve( 395 | this.buildEAPTTLSResponse(identifier, 21, 0x00, stateID, encryptedResponseData) 396 | ); 397 | }; 398 | 399 | const checkExistingSession = (isSessionReused) => { 400 | if (isSessionReused) { 401 | log('secured, session reused, accept auth!'); 402 | sendResponsePromise.resolve(this.authResponse(identifier, true, connection.tls, packet)); 403 | } 404 | }; 405 | 406 | // register event listeners 407 | connection.events.on('incoming', incomingMessageHandler); 408 | connection.events.on('response', responseHandler); 409 | connection.events.on('secured', checkExistingSession); 410 | 411 | // emit data to tls server 412 | connection.events.emit('decrypt', data); 413 | const responseData = await sendResponsePromise.promise; 414 | 415 | // cleanup 416 | connection.events.off('incoming', incomingMessageHandler); 417 | connection.events.off('response', responseHandler); 418 | connection.events.off('secured', checkExistingSession); 419 | 420 | // connection.events.off('secured'); 421 | 422 | // send response 423 | return responseData; // this.buildEAPTTLSResponse(identifier, 21, 0x00, stateID, encryptedResponseData); 424 | } catch (err) { 425 | console.error('decoding of EAP-TTLS package failed', msg, err); 426 | return { 427 | code: PacketResponseCode.AccessReject, 428 | }; 429 | } finally { 430 | this.lastProcessedIdentifier.set(stateID, undefined); 431 | } 432 | } 433 | 434 | private transformAttributesArrayToMap(attributes: [string, Buffer | string][] | undefined) { 435 | const result = {}; 436 | attributes?.forEach(([key, value]) => { 437 | result[key] = value; 438 | }); 439 | return result; 440 | } 441 | 442 | private decodeAVPs(buffer: Buffer): IAVPEntry[] { 443 | const results: { 444 | type: number; 445 | flags: string; 446 | decodedFlags: { 447 | V: boolean; 448 | M: boolean; 449 | }; 450 | length: number; 451 | vendorId?: number; 452 | data: Buffer; 453 | }[] = []; 454 | 455 | let currentBuffer = buffer; 456 | do { 457 | /** 458 | * 4.1. AVP Header 459 | 460 | The fields in the AVP header MUST be sent in network byte order. The 461 | format of the header is: 462 | 463 | 0 1 2 3 464 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 465 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 466 | | AVP Code | 467 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 468 | |V M P r r r r r| AVP Length | 469 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 470 | | Vendor-ID (opt) | 471 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 472 | | Data ... 473 | +-+-+-+-+-+-+-+-+ 474 | */ 475 | const type = currentBuffer.slice(0, 4).readUInt32BE(0); 476 | const flags = currentBuffer.slice(4, 5).readUInt8(0); 477 | const decodedFlags = { 478 | // L 479 | V: !!(flags & 0b10000000), 480 | // M 481 | M: !!(flags & 0b01000000), 482 | }; 483 | 484 | // const length = buffer.slice(5, 8).readUInt16BE(0); // actually a Int24BE 485 | const length = currentBuffer.slice(6, 8).readUInt16BE(0); // actually a Int24BE 486 | 487 | let vendorId; 488 | let data; 489 | if (decodedFlags.V) { 490 | // V flag set 491 | vendorId = currentBuffer.slice(8, 12).readUInt32BE(0); 492 | data = currentBuffer.slice(12, length); 493 | } else { 494 | data = currentBuffer.slice(8, length); 495 | } 496 | 497 | results.push({ 498 | type, 499 | flags: `00000000${flags.toString(2)}`.substr(-8), 500 | decodedFlags, 501 | length, 502 | vendorId, 503 | data, 504 | }); 505 | 506 | // ensure length is a multiple of 4 octect 507 | let totalAVPSize = length; 508 | while (totalAVPSize % 4 !== 0) { 509 | totalAVPSize += 1; 510 | } 511 | currentBuffer = currentBuffer.slice(totalAVPSize); 512 | } while (currentBuffer.length > 0); 513 | 514 | return results; 515 | } 516 | 517 | private buildAVP( 518 | code: number, 519 | data: Buffer, 520 | flags: { VendorSpecific?: boolean; Mandatory?: boolean } = { Mandatory: true } 521 | ) { 522 | /** 523 | * 4.1. AVP Header 524 | 525 | The fields in the AVP header MUST be sent in network byte order. The 526 | format of the header is: 527 | 528 | 0 1 2 3 529 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 530 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 531 | | AVP Code | 532 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 533 | |V M r r r r r r| AVP Length | 534 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 535 | | Vendor-ID (opt) | 536 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 537 | | Data ... 538 | +-+-+-+-+-+-+-+-+ 539 | */ 540 | let AVP = Buffer.alloc(8); 541 | 542 | AVP.writeInt32BE(code, 0); // EAP-Message 543 | /** 544 | * The 'V' (Vendor-Specific) bit indicates whether the optional 545 | Vendor-ID field is present. When set to 1, the Vendor-ID field is 546 | present and the AVP Code is interpreted according to the namespace 547 | defined by the vendor indicated in the Vendor-ID field. 548 | 549 | The 'M' (Mandatory) bit indicates whether support of the AVP is 550 | required. If this bit is set to 0, this indicates that the AVP 551 | may be safely ignored if the receiving party does not understand 552 | or support it. If set to 1, this indicates that the receiving 553 | party MUST fail the negotiation if it does not understand the AVP; 554 | for a TTLS server, this would imply returning EAP-Failure, for a 555 | client, this would imply abandoning the negotiation. 556 | */ 557 | let flagValue = 0; 558 | if (flags.VendorSpecific) { 559 | flagValue += 0b10000000; 560 | } 561 | if (flags.Mandatory) { 562 | flagValue += 0b01000000; 563 | } 564 | 565 | // log('flagValue', flagValue, `00000000${flagValue.toString(2)}`.substr(-8)); 566 | 567 | AVP.writeInt8(flagValue, 4); // flags (set V..) 568 | 569 | AVP = Buffer.concat([AVP, data]); // , Buffer.from('\0')]); 570 | 571 | AVP.writeInt16BE(AVP.byteLength, 6); // write size (actually we would need a Int24BE here, but it is good to go with 16bits) 572 | 573 | // fill up with 0x00 till we have % 4 574 | while (AVP.length % 4 !== 0) { 575 | AVP = Buffer.concat([AVP, Buffer.from([0x00])]); 576 | } 577 | 578 | return AVP; 579 | } 580 | } 581 | --------------------------------------------------------------------------------