Implement better radius support
This commit is contained in:
parent
0d24782691
commit
d63de520c9
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,3 +1,5 @@
|
|||||||
|
*.conf
|
||||||
|
|
||||||
# ---> Node
|
# ---> Node
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
|
@ -44,7 +44,7 @@ const FlitterUnits = {
|
|||||||
'LDAPController': require('./app/unit/LDAPControllerUnit'),
|
'LDAPController': require('./app/unit/LDAPControllerUnit'),
|
||||||
'LDAPRoutingUnit': require('./app/unit/LDAPRoutingUnit'),
|
'LDAPRoutingUnit': require('./app/unit/LDAPRoutingUnit'),
|
||||||
'OpenIDConnect' : require('./app/unit/OpenIDConnectUnit'),
|
'OpenIDConnect' : require('./app/unit/OpenIDConnectUnit'),
|
||||||
// 'Radius' : require('./app/unit/RadiusUnit'),
|
'Radius' : require('./app/unit/RadiusUnit'),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The Core Flitter Units
|
* The Core Flitter Units
|
||||||
|
55
app/classes/radius/CoreIDAuthentication.js
Normal file
55
app/classes/radius/CoreIDAuthentication.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
const User = require('../../models/auth/User.model')
|
||||||
|
const Client = require('../../models/radius/Client.model')
|
||||||
|
const Application = require('../../models/Application.model')
|
||||||
|
const Policy = require('../../models/iam/Policy.model')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements IAuthentication from radius-server
|
||||||
|
*/
|
||||||
|
class CoreIDAuthentication {
|
||||||
|
async authenticate(username, password, packet) {
|
||||||
|
// We only allow client-specific secrets to authenticate
|
||||||
|
if ( !packet || !packet.secret ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to look up the client
|
||||||
|
const client = await Client.findOne({
|
||||||
|
active: true,
|
||||||
|
secret: packet.secret,
|
||||||
|
})
|
||||||
|
if ( !client ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to look up the associated application
|
||||||
|
const application = await Application.findOne({
|
||||||
|
radius_client_ids: client.id,
|
||||||
|
})
|
||||||
|
if ( !application ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to look up the user
|
||||||
|
/** @var {User} */
|
||||||
|
const user = await User.findByLogin(username)
|
||||||
|
if ( !user ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the incoming credential
|
||||||
|
if ( !(await user.check_credential_string(password)) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't allow login if the user has a trap set
|
||||||
|
if ( user.trap ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the IAM policy engine to make sure the user can access this resource
|
||||||
|
return Policy.check_user_access(user, application.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exports = CoreIDAuthentication
|
28
app/classes/radius/CoreIDRadiusServer.mjs
Normal file
28
app/classes/radius/CoreIDRadiusServer.mjs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import radius from 'radius'
|
||||||
|
import { RadiusServer } from '@coreid/radius-server'
|
||||||
|
import RadiusClient from '../../models/radius/Client.model.js'
|
||||||
|
import CoreIDUserPasswordPacketHandler from './CoreIDUserPasswordPacketHandler.mjs'
|
||||||
|
|
||||||
|
export default class CoreIDRadiusServer extends RadiusServer {
|
||||||
|
|
||||||
|
// constructor(options) {
|
||||||
|
// super(options)
|
||||||
|
// this.packetHandler.packetHandlers.pop()
|
||||||
|
// this.packetHandler.packetHandlers.push(new CoreIDUserPasswordPacketHandler(options.authentication, this.logger))
|
||||||
|
// console.log(this.packetHandler.packetHandlers)
|
||||||
|
// }
|
||||||
|
|
||||||
|
async decodeMessage(msg) {
|
||||||
|
const clients = await RadiusClient.find({ active: true })
|
||||||
|
for ( const client of clients ) {
|
||||||
|
try {
|
||||||
|
const packet = radius.decode({ packet: msg, secret: client.secret })
|
||||||
|
packet.secret = client.secret
|
||||||
|
return packet
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('Unable to determine client to decode RADIUS packet: is the client active?')
|
||||||
|
}
|
||||||
|
}
|
40
app/classes/radius/CoreIDUserPasswordPacketHandler.mjs
Normal file
40
app/classes/radius/CoreIDUserPasswordPacketHandler.mjs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { UserPasswordPacketHandler } from '@coreid/radius-server/dist/radius/handler/UserPasswordPacketHandler.js'
|
||||||
|
|
||||||
|
export default class CoreIDUserPasswordPacketHandler extends UserPasswordPacketHandler {
|
||||||
|
async handlePacket(packet) {
|
||||||
|
console.log('coreid user password packet handler handlePacket', packet)
|
||||||
|
const username = packet.attributes['User-Name'];
|
||||||
|
let password = packet.attributes['User-Password'];
|
||||||
|
|
||||||
|
if (Buffer.isBuffer(password) && password.indexOf(0x00) > 0) {
|
||||||
|
// check if there is a 0x00 in it, and trim it from there
|
||||||
|
password = password.slice(0, password.indexOf(0x00));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
// params missing, this handler cannot continue...
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.debug('username', username, username.toString());
|
||||||
|
this.logger.debug('token', password, password.toString());
|
||||||
|
console.log('client', packet.__coreid_client)
|
||||||
|
|
||||||
|
const authenticated = await this.authentication.authenticate(
|
||||||
|
username.toString(),
|
||||||
|
password.toString()
|
||||||
|
);
|
||||||
|
if (authenticated) {
|
||||||
|
// success
|
||||||
|
return {
|
||||||
|
code: 'Access-Accept',
|
||||||
|
attributes: [['User-Name', username]],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Failed
|
||||||
|
return {
|
||||||
|
code: 'Access-Reject',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -59,34 +59,8 @@ class LDAPController extends Injectable {
|
|||||||
return next(new LDAP.InsufficientAccessRightsError())
|
return next(new LDAP.InsufficientAccessRightsError())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the credentials are an app_password
|
// Check if the credentials are valid
|
||||||
const app_password_verified = Array.isArray(item.app_passwords)
|
if ( !(await item.check_credential_string(req.credentials)) ) {
|
||||||
&& item.app_passwords.length > 0
|
|
||||||
&& await item.check_app_password(req.credentials)
|
|
||||||
|
|
||||||
// Check if the user has MFA enabled.
|
|
||||||
// If so, split the incoming password to fetch the MFA code
|
|
||||||
// e.g. normalPassword:123456
|
|
||||||
if ( !app_password_verified && item.mfa_enabled ) {
|
|
||||||
const parts = req.credentials.split(':')
|
|
||||||
const mfa_code = parts.pop()
|
|
||||||
const actual_password = parts.join(':')
|
|
||||||
|
|
||||||
// Check the credentials
|
|
||||||
if ( !await item.check_password(actual_password) ) {
|
|
||||||
this.output.debug(`Bind failure: user w/ MFA provided invalid credentials`)
|
|
||||||
return next(new LDAP.InvalidCredentialsError('Invalid credentials. Make sure MFA code is included at the end of your password (e.g. password:123456)'))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now, check the MFA code
|
|
||||||
if ( !item.mfa_token.verify(mfa_code) ) {
|
|
||||||
this.output.debug(`Bind failure: user w/ MFA provided invalid MFA token`)
|
|
||||||
return next(new LDAP.InvalidCredentialsError('Invalid credentials. Verification of the MFA token failed.'))
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not MFA, just check the credentials
|
|
||||||
} else if (!app_password_verified && !await item.check_password(req.credentials)) {
|
|
||||||
this.output.debug(`Bind failure: user w/ simple auth provided invalid credentials`)
|
|
||||||
return next(new LDAP.InvalidCredentialsError())
|
return next(new LDAP.InvalidCredentialsError())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,6 +155,38 @@ class User extends AuthUser {
|
|||||||
await this.save()
|
await this.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async check_credential_string(credential) {
|
||||||
|
// Check if the credentials are an app_password
|
||||||
|
const app_password_verified = Array.isArray(this.app_passwords)
|
||||||
|
&& this.app_passwords.length > 0
|
||||||
|
&& await this.check_app_password(credential)
|
||||||
|
|
||||||
|
// Check if the user has MFA enabled.
|
||||||
|
// If so, split the incoming password to fetch the MFA code
|
||||||
|
// e.g. normalPassword:123456
|
||||||
|
if ( !app_password_verified && this.mfa_enabled ) {
|
||||||
|
const parts = credential.split(':')
|
||||||
|
const mfa_code = parts.pop()
|
||||||
|
const actual_password = parts.join(':')
|
||||||
|
|
||||||
|
// Check the credentials
|
||||||
|
if ( !await this.check_password(actual_password) ) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, check the MFA code
|
||||||
|
if ( !this.mfa_token.verify(mfa_code) ) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not MFA, just check the credentials
|
||||||
|
} else if (!app_password_verified && !await this.check_password(credential)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
async check_password(password) {
|
async check_password(password) {
|
||||||
return this.get_provider().check_user_auth(this, password)
|
return this.get_provider().check_user_auth(this, password)
|
||||||
}
|
}
|
||||||
|
@ -1,185 +1,47 @@
|
|||||||
|
const fs = require('fs/promises')
|
||||||
|
const uuid = require('uuid')
|
||||||
const { Unit } = require('libflitter')
|
const { Unit } = require('libflitter')
|
||||||
const fs = require('fs')
|
const CoreIDAuthentication = require('../classes/radius/CoreIDAuthentication')
|
||||||
const radius = require('radius')
|
|
||||||
const net = require("net");
|
const net = require("net");
|
||||||
const uuid = require('uuid').v4
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unit that provides a CoreID IAM-integrated RADIUS server.
|
|
||||||
*/
|
|
||||||
class RadiusUnit extends Unit {
|
class RadiusUnit extends Unit {
|
||||||
static get services() {
|
static get services() {
|
||||||
return [...super.services, 'configs', 'output', 'models']
|
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) {
|
async go(app) {
|
||||||
if ( !(await this.port_free()) ) {
|
if ( !this.configs.get('radius.enable') ) return;
|
||||||
this.output.info('RADIUS server port is in use. Will not start!')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = this.getConfig()
|
const CoreIDRadiusServer = (await import('../classes/radius/CoreIDRadiusServer.mjs')).default
|
||||||
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')
|
// Load the certificates
|
||||||
const server = RadiusServer.get()
|
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.radius = new CoreIDRadiusServer({
|
||||||
this.output.success('Started RADIUS server!')
|
// logger
|
||||||
|
secret: this.configs.get('radius.secret', uuid.v4()),
|
||||||
// await packageInterface.up()
|
port: this.configs.get('radius.port', 1812),
|
||||||
|
address: this.configs.get('radius.interface', '0.0.0.0'),
|
||||||
// Overwrite radius-server's global config object with the user-provided values
|
tlsOptions: {
|
||||||
/*const radiusConfig = require('radius-server/config')
|
cert: pubkey,
|
||||||
for ( const key in config ) {
|
key: privkey,
|
||||||
if ( !Object.hasOwnProperty.apply(config, [key]) ) continue
|
},
|
||||||
radiusConfig[key] = config[key]
|
authentication: new CoreIDAuthentication(),
|
||||||
}*/
|
|
||||||
|
|
||||||
/*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
|
if ( await this.port_free() ) {
|
||||||
await this.server.start()
|
this.output.info('Starting RADIUS server...')
|
||||||
this.output.success('Started 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) {
|
async cleanup(app) {
|
||||||
const { RadiusServer } = require('@coreid/radius-server/dist/radius/RadiusServer')
|
if ( this.radius ) {
|
||||||
const server = RadiusServer.get()
|
await this.radius.server.close()
|
||||||
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',
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,37 +55,9 @@ class RadiusUnit extends Unit {
|
|||||||
server.close()
|
server.close()
|
||||||
res(true)
|
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
|
module.exports = exports = RadiusUnit
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
|
const uuid = require('uuid')
|
||||||
|
|
||||||
const radius_config = {
|
const radius_config = {
|
||||||
|
enable: env('RADIUS_ENABLE', false),
|
||||||
port: env('RADIUS_PORT', 1812),
|
port: env('RADIUS_PORT', 1812),
|
||||||
|
interface: env('RADIUS_INTERFACE', '0.0.0.0'),
|
||||||
|
secret: env('RADIUS_SECRET', uuid.v4()),
|
||||||
|
|
||||||
cert_file: {
|
cert_file: {
|
||||||
public: env('RADIUS_CERT_FILE'),
|
public: env('RADIUS_CERT_FILE'),
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
"author": "Garrett Mills <garrett@glmdev.tech> (https://garrettmills.dev/)",
|
"author": "Garrett Mills <garrett@glmdev.tech> (https://garrettmills.dev/)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@coreid/radius-server": "^1.2.6",
|
"@coreid/radius-server": "^2.2.2",
|
||||||
"bullmq": "^1.8.8",
|
"bullmq": "^1.8.8",
|
||||||
"email-validator": "^2.0.4",
|
"email-validator": "^2.0.4",
|
||||||
"flitter-auth": "^0.19.6",
|
"flitter-auth": "^0.19.6",
|
||||||
|
Loading…
Reference in New Issue
Block a user