const { Unit } = require('libflitter') const fs = require('fs') const radius = require('radius') const net = require("net"); const uuid = require('uuid').v4 /** * Unit that provides a CoreID IAM-integrated RADIUS server. */ class RadiusUnit extends Unit { static get services() { return [...super.services, 'configs', 'output', 'models'] } getPacketDecoder() { const RadiusClient = this.models.get('radius:Client') return async (msg) => { const clients = await RadiusClient.find({ active: true }) // Try the secrets for all active clients. // If we can successfully decode the packet with a client's secret, then we know // that the message came from that client. let authenticatedClient let packet for ( const client of clients ) { try { packet = radius.decode({ packet: msg, secret: client.secret }) authenticatedClient = client break } catch (e) {} } packet.credentialMiddleware = (username, password) => { this.output.debug(`Called credential middleware: ${username}`) return [`${username}@${authenticatedClient.id}`, password] } return { packet, secret: authenticatedClient.secret || '', } } } async go(app) { if ( !(await this.port_free()) ) { this.output.info('RADIUS server port is in use. Will not start!') return } const config = this.getConfig() const packageInterface = require('@coreid/radius-server/dist/interface').default.get() packageInterface.setConfig(config) packageInterface.packetDecoder = this.getPacketDecoder() packageInterface.log = (...any) => any.forEach(x => this.output.info(x)) const { RadiusServer } = require('@coreid/radius-server/dist/radius/RadiusServer') const server = RadiusServer.get() await server.up() this.output.success('Started RADIUS server!') // await packageInterface.up() // Overwrite radius-server's global config object with the user-provided values /*const radiusConfig = require('radius-server/config') for ( const key in config ) { if ( !Object.hasOwnProperty.apply(config, [key]) ) continue radiusConfig[key] = config[key] }*/ /*const { Authentication } = require('radius-server/dist/auth') const { UDPServer } = require('radius-server/dist/server/UDPServer') const { RadiusService } = require('radius-server/dist/radius/RadiusService') this.output.info('Starting RADIUS server...') // Create the authenticator instance const AuthMechanismus = require(`radius-server/dist/auth/${config.authentication}`)[config.authentication] const auth = new AuthMechanismus(config.authenticationOptions) const authentication = new Authentication(auth) this.server = new UDPServer(config.port) this.radiusService = new RadiusService(config.secret, authentication) // Define the server handler this.server.on('message', async (msg, rinfo) => { this.output.debug('Incoming RADIUS message...') const response = await this.handleMessage(msg) if ( response ) { this.output.debug('Sending response...') this.server.sendToClient( response.data, rinfo.port, rinfo.address, (err, _bytes) => { if ( err ) { this.output.error(`Error sending response to:`) this.output.error(rinfo) } }, response.expectAcknowledgment ) } }) // Start the radius server await this.server.start() this.output.success('Started RADIUS server!')*/ } async cleanup(app) { const { RadiusServer } = require('@coreid/radius-server/dist/radius/RadiusServer') const server = RadiusServer.get() await server.down() // const packageInterface = require('@coreid/radius-server/dist/interface').get() // await packageInterface.down() /*if ( this.server ) { // radius-server doesn't expose a "close" method explicitly, which is annoying // instead, reach in and close the internal UDP socket this.server.server.close() }*/ } /** * Handle an incoming radius request message. * @param msg * @returns {Promise<{expectAcknowledgment: boolean, data: *}|undefined>} */ async handleMessage(msg) { const RadiusClient = this.models.get('radius:Client') const clients = await RadiusClient.find({ active: true }) // Try the secrets for all active clients. // If we can successfully decode the packet with a client's secret, then we know // that the message came from that client. let authenticatedClient let packet for ( const client of clients ) { try { packet = radius.decode({ packet: msg, secret: this.getConfig().secret /*client.secret*/ }) authenticatedClient = client break } catch (e) {} } if (packet.code !== 'Access-Request') { console.error('unknown packet type: ', packet.code) return } // Include the RADIUS Client ID in the username so we can parse it out in the controller // This allows us to check IAM access in the controller packet.attributes['User-Name'] = `${packet.attributes['User-Name']}@${authenticatedClient.id}` this.output.info(`RADIUS auth attempt: ${packet.attributes['User-Name']}`) this.output.debug(packet) const response = await this.radiusService.packetHandler.handlePacket(packet) // still no response, we are done here if (!response || !response.code) { this.output.debug(`RADIUS error: no response / response code`) this.output.debug(response) return } // all fine, return radius encoded response return { data: radius.encode_response({ packet, code: response.code, secret: this.getConfig().secret, // authenticatedClient.secret, // use the client's secret to encode the response attributes: response.attributes, }), // if message is accept or reject, we conside this as final message // this means we do not expect a reponse from the client again (acknowledgement for package) expectAcknowledgment: response.code === 'Access-Challenge', } } async port_free() { return new Promise((res, rej) => { const server = net.createServer() server.once('error', (e) => { res(false) }) server.once('listening', () => { server.close() res(true) }) server.listen(this.getConfig().port) }) } getConfig() { const baseUrl = this.configs.get('app.url') const config = { port: this.configs.get('radius.port', 1812), secret: 'testing123', // uuid(), // this is never used - client-specific secrets are injected instead certificate: { cert: fs.readFileSync(this.configs.get('radius.cert_file.public')), key: [ { pem: fs.readFileSync(this.configs.get('radius.cert_file.private')), }, ], }, authentication: 'HTTPAuth', authenticationOptions: { url: `${baseUrl}api/v1/radius/attempt`, }, } const passphrase = this.configs.get('saml.cert_file.passphrase') if ( passphrase ) { config.certificate.key[0].passphrase = passphrase } return config } } module.exports = exports = RadiusUnit