Implement RADIUS server!
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -115,6 +115,28 @@ class AppController extends Controller {
|
||||
application.oauth_client_ids = oauth_client_ids
|
||||
}
|
||||
|
||||
// Verify RADIUS client IDs
|
||||
const RadiusClient = this.models.get('radius:Client')
|
||||
if ( req.body.radius_client_ids ) {
|
||||
const parsed = typeof req.body.radius_client_ids === 'string' ? this.utility.infer(req.body.radius_client_ids) : req.body.radius_client_ids
|
||||
const radius_client_ids = Array.isArray(parsed) ? parsed : [parsed]
|
||||
for ( const id of radius_client_ids ) {
|
||||
const client = await RadiusClient.findById(id)
|
||||
if ( !client || !client.active || !req.user.can(`radius:client:${client.id}:view`) )
|
||||
return res.status(400)
|
||||
.message(`${req.T('api.invalid_radius_client_id')} ${id}`)
|
||||
.api()
|
||||
|
||||
const other_assoc_app = await Application.findOne({ radius_client_ids: client.id })
|
||||
if ( other_assoc_app )
|
||||
return res.status(400) // TODO translate this
|
||||
.message(`The RADIUS client ${client.name} is already associated with an existing application (${other_assoc_app.name}).`)
|
||||
.api()
|
||||
}
|
||||
|
||||
application.radius_client_ids = radius_client_ids
|
||||
}
|
||||
|
||||
// Verify OpenID client IDs
|
||||
const OpenIDClient = this.models.get('openid:Client')
|
||||
if ( req.body.openid_client_ids ) {
|
||||
@@ -242,6 +264,28 @@ class AppController extends Controller {
|
||||
application.oauth_client_ids = oauth_client_ids
|
||||
} else application.oauth_client_ids = []
|
||||
|
||||
// Verify OAuth client IDs
|
||||
const RadiusClient = this.models.get('radius:Client')
|
||||
if ( req.body.radius_client_ids ) {
|
||||
const parsed = typeof req.body.radius_client_ids === 'string' ? this.utility.infer(req.body.radius_client_ids) : req.body.radius_client_ids
|
||||
const radius_client_ids = Array.isArray(parsed) ? parsed : [parsed]
|
||||
for ( const id of radius_client_ids ) {
|
||||
const client = await RadiusClient.findById(id)
|
||||
if ( !client || !client.active || !req.user.can(`radius:client:${client.id}:view`) )
|
||||
return res.status(400)
|
||||
.message(`${req.T('api.invalid_radius_client_id')} ${id}`)
|
||||
.api()
|
||||
|
||||
const other_assoc_app = await Application.findOne({ radius_client_ids: client.id })
|
||||
if ( other_assoc_app && other_assoc_app.id !== application.id )
|
||||
return res.status(400) // TODO translate this
|
||||
.message(`The RADIUS client ${client.name} is already associated with an existing application (${other_assoc_app.name}).`)
|
||||
.api()
|
||||
}
|
||||
|
||||
application.radius_client_ids = radius_client_ids
|
||||
} else application.radius_client_ids = []
|
||||
|
||||
// Verify OpenID client IDs
|
||||
const OpenIDClient = this.models.get('openid:Client')
|
||||
if ( req.body.openid_client_ids ) {
|
||||
|
||||
190
app/controllers/api/v1/Radius.controller.js
Normal file
190
app/controllers/api/v1/Radius.controller.js
Normal file
@@ -0,0 +1,190 @@
|
||||
const { Controller } = require('libflitter')
|
||||
|
||||
class RadiusController extends Controller {
|
||||
static get services() {
|
||||
return [...super.services, 'models', 'output']
|
||||
}
|
||||
|
||||
async attempt(req, res, next) {
|
||||
const User = this.models.get('auth:User')
|
||||
const Client = this.models.get('radius:Client')
|
||||
|
||||
if ( !req.body.username || !req.body.password ) {
|
||||
this.output.error('RADIUS error: missing username or password')
|
||||
return this.fail(res)
|
||||
}
|
||||
|
||||
const parts = String(req.body.username).split('@')
|
||||
parts.reverse()
|
||||
|
||||
const clientId = parts.shift()
|
||||
parts.reverse()
|
||||
|
||||
const username = parts.join('@')
|
||||
const password = req.body.password
|
||||
|
||||
const user = await User.findOne({ uid: username, active: true })
|
||||
if ( !user ) {
|
||||
this.output.error(`RADIUS error: invalid username: ${username}`)
|
||||
return this.fail(res)
|
||||
}
|
||||
|
||||
const client = await Client.findById(clientId)
|
||||
if ( !client || !client.active ) {
|
||||
this.output.error(`RADIUS error: invalid client: ${clientId}`)
|
||||
return this.fail(res)
|
||||
}
|
||||
|
||||
// Check if the credentials are an app_password
|
||||
const app_password_verified = Array.isArray(user.app_passwords)
|
||||
&& user.app_passwords.length > 0
|
||||
&& await user.check_app_password(password)
|
||||
|
||||
// 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 && user.mfa_enabled ) {
|
||||
const parts = password.split(':')
|
||||
const mfa_code = parts.pop()
|
||||
const actual_password = parts.join(':')
|
||||
|
||||
// Check the credentials
|
||||
if ( !(await user.check_password(actual_password)) ) {
|
||||
this.output.debug(`RADIUS error: user w/ MFA provided invalid credentials`)
|
||||
return this.fail(res)
|
||||
}
|
||||
|
||||
// Now, check the MFA code
|
||||
if ( !user.mfa_token.verify(mfa_code) ) {
|
||||
this.output.debug(`RADIUS error: user w/ MFA provided invalid MFA token`)
|
||||
return this.fail(res)
|
||||
}
|
||||
|
||||
// If not MFA, just check the credentials
|
||||
} else if (!app_password_verified && !await user.check_password(password)) {
|
||||
this.output.debug(`RADIUS error: user w/ simple auth provided invalid credentials`)
|
||||
return this.fail(res)
|
||||
}
|
||||
|
||||
// Check if the user has any login interrupt traps set
|
||||
if ( user.trap ) {
|
||||
this.output.error(`RADIUS error: user has trap: ${user.trap}`)
|
||||
return this.fail(res)
|
||||
}
|
||||
|
||||
// Apply the appropriate IAM policy if this SAML SP is associated with an App
|
||||
// If the SAML service provider has no associated application, just allow it
|
||||
const associated_app = await client.application()
|
||||
if ( associated_app ) {
|
||||
const Policy = this.models.get('iam:Policy')
|
||||
const can_access = await Policy.check_user_access(user, associated_app.id)
|
||||
if ( !can_access ) {
|
||||
this.output.error(`RADIUS error: user denied IAM access`)
|
||||
return this.fail(res)
|
||||
}
|
||||
}
|
||||
|
||||
this.output.info(`Authenticated RADIUS user: ${user.uid} to IAM ${associated_app.name}`)
|
||||
return res.api({ success: true })
|
||||
}
|
||||
|
||||
fail(res) {
|
||||
return res.status(401).api({ success: false })
|
||||
}
|
||||
|
||||
async get_clients(req, res, next) {
|
||||
const Client = this.models.get('radius:Client')
|
||||
const clients = await Client.find({ active: true })
|
||||
const data = []
|
||||
|
||||
for ( const client of clients ) {
|
||||
if ( req.user.can(`radius:client:${client.id}:view`) ) {
|
||||
data.push(await client.to_api())
|
||||
}
|
||||
}
|
||||
|
||||
return res.api(data)
|
||||
}
|
||||
|
||||
async get_client(req, res, next) {
|
||||
const Client = this.models.get('radius:Client')
|
||||
const client = await Client.findById(req.params.id)
|
||||
|
||||
if ( !client || !client.active )
|
||||
return res.status(404)
|
||||
.message(req.T('api.client_not_found'))
|
||||
.api()
|
||||
|
||||
if ( !req.user.can(`radius:client:${client.id}:view`) )
|
||||
return res.status(401)
|
||||
.message(req.T('api.insufficient_permissions'))
|
||||
.api()
|
||||
|
||||
return res.api(await client.to_api())
|
||||
}
|
||||
|
||||
async create_client(req, res, next) {
|
||||
if ( !req.user.can('radius:client:create') )
|
||||
return res.status(401)
|
||||
.message(req.T('api.insufficient_permissions'))
|
||||
.api()
|
||||
|
||||
if ( !req.body.name )
|
||||
return res.status(400)
|
||||
.message(`${req.T('api.missing_field')} name`)
|
||||
.api()
|
||||
|
||||
const Client = this.models.get('radius:Client')
|
||||
const client = new Client({
|
||||
name: req.body.name,
|
||||
})
|
||||
|
||||
await client.save()
|
||||
return res.api(await client.to_api())
|
||||
}
|
||||
|
||||
async update_client(req, res, next) {
|
||||
const Client = this.models.get('radius:Client')
|
||||
const client = await Client.findById(req.params.id)
|
||||
|
||||
if ( !client || !client.active )
|
||||
return res.status(404)
|
||||
.message(req.T('api.client_not_found'))
|
||||
.api()
|
||||
|
||||
if ( !req.user.can(`radius:client:${client.id}:update`) )
|
||||
return res.status(401)
|
||||
.message(req.T('api.insufficient_permissions'))
|
||||
.api()
|
||||
|
||||
if ( !req.body.name )
|
||||
return res.status(400)
|
||||
.message(`${req.T('api.missing_field')} name`)
|
||||
.api()
|
||||
|
||||
client.name = req.body.name
|
||||
await client.save()
|
||||
return res.api()
|
||||
}
|
||||
|
||||
async delete_client(req, res, next) {
|
||||
const Client = this.models.get('radius:Client')
|
||||
const client = await Client.findById(req.params.id)
|
||||
|
||||
if ( !client || !client.active )
|
||||
return res.status(404)
|
||||
.message(req.T('api.client_not_found'))
|
||||
.api()
|
||||
|
||||
if ( !req.user.can(`radius:client:${client.id}:delete`) )
|
||||
return res.status(401)
|
||||
.message(req.T('api.insufficient_permissions'))
|
||||
.api()
|
||||
|
||||
client.active = false
|
||||
await client.save()
|
||||
return res.api()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = RadiusController
|
||||
Reference in New Issue
Block a user