CoreID/app/unit/RadiusUnit.js
2021-11-22 09:08:22 -06:00

230 lines
8.2 KiB
JavaScript

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