Implement better radius support
This commit is contained in:
@@ -1,185 +1,47 @@
|
||||
const fs = require('fs/promises')
|
||||
const uuid = require('uuid')
|
||||
const { Unit } = require('libflitter')
|
||||
const fs = require('fs')
|
||||
const radius = require('radius')
|
||||
const CoreIDAuthentication = require('../classes/radius/CoreIDAuthentication')
|
||||
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
|
||||
}
|
||||
if ( !this.configs.get('radius.enable') ) 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 CoreIDRadiusServer = (await import('../classes/radius/CoreIDRadiusServer.mjs')).default
|
||||
|
||||
const { RadiusServer } = require('@coreid/radius-server/dist/radius/RadiusServer')
|
||||
const server = RadiusServer.get()
|
||||
// Load the certificates
|
||||
const pubkey = await fs.readFile(this.configs.get('radius.cert_file.public'))
|
||||
const privkey = await fs.readFile(this.configs.get('radius.cert_file.private'))
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
this.radius = new CoreIDRadiusServer({
|
||||
// logger
|
||||
secret: this.configs.get('radius.secret', uuid.v4()),
|
||||
port: this.configs.get('radius.port', 1812),
|
||||
address: this.configs.get('radius.interface', '0.0.0.0'),
|
||||
tlsOptions: {
|
||||
cert: pubkey,
|
||||
key: privkey,
|
||||
},
|
||||
authentication: new CoreIDAuthentication(),
|
||||
})
|
||||
|
||||
// Start the radius server
|
||||
await this.server.start()
|
||||
this.output.success('Started RADIUS server!')*/
|
||||
if ( await this.port_free() ) {
|
||||
this.output.info('Starting RADIUS server...')
|
||||
await this.radius.start()
|
||||
} else {
|
||||
this.output.error('Will not start RADIUS server. Reason: configured port is already in use')
|
||||
delete this.radius
|
||||
}
|
||||
}
|
||||
|
||||
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',
|
||||
if ( this.radius ) {
|
||||
await this.radius.server.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,37 +55,9 @@ class RadiusUnit extends Unit {
|
||||
server.close()
|
||||
res(true)
|
||||
})
|
||||
server.listen(this.getConfig().port)
|
||||
server.listen(this.configs.get('radius.port', 1812))
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user