Add support for OpenID connect!!
This commit is contained in:
266
app/controllers/OpenID.controller.js
Normal file
266
app/controllers/OpenID.controller.js
Normal file
@@ -0,0 +1,266 @@
|
||||
const Controller = require('libflitter/controller/Controller')
|
||||
const is_absolute_url = require('is-absolute-url')
|
||||
|
||||
class OpenIDController extends Controller {
|
||||
static get services() {
|
||||
return [...super.services, 'Vue', 'openid_connect', 'configs', 'models', 'output']
|
||||
}
|
||||
|
||||
fail(res, message) {
|
||||
return this.Vue.auth_message(res, { message, next_destination: '/dash' })
|
||||
}
|
||||
|
||||
async get_clients(req, res) {
|
||||
const Client = this.models.get('openid:Client')
|
||||
const clients = await Client.find()
|
||||
|
||||
const data = []
|
||||
for ( const client of clients ) {
|
||||
data.push(await client.to_api())
|
||||
}
|
||||
|
||||
return res.api(data)
|
||||
}
|
||||
|
||||
async get_client(req, res) {
|
||||
const Client = this.models.get('openid:Client')
|
||||
const client = await Client.findById(req.params.id)
|
||||
|
||||
if ( !client )
|
||||
return res.status(404)
|
||||
.message(req.T('api.client_not_found'))
|
||||
.api()
|
||||
|
||||
return res.api(await client.to_api())
|
||||
}
|
||||
|
||||
async create_client(req, res) {
|
||||
const required_fields = ['client_name', 'grant_types', 'redirect_uri']
|
||||
for ( const field of required_fields ) {
|
||||
if ( !req.body[field] )
|
||||
return res.status(400)
|
||||
.message(`${req.T('api.missing_field')} ${field}`)
|
||||
.api()
|
||||
}
|
||||
|
||||
if ( !Array.isArray(req.body.grant_types) )
|
||||
return res.status(400)
|
||||
.message(`${req.T('api.improper_field')} grant_types ${req.T('api.array')}`)
|
||||
.api()
|
||||
|
||||
if ( !is_absolute_url(req.body.redirect_uri) )
|
||||
return res.status(400)
|
||||
.message(`${req.T('api.improper_field')} redirect_uri ${req.T('api.absolute_url')}`)
|
||||
.api()
|
||||
|
||||
const payload = {
|
||||
client_name: req.body.client_name,
|
||||
grant_types: req.body.grant_types,
|
||||
redirect_uris: [req.body.redirect_uri],
|
||||
}
|
||||
|
||||
const Client = this.models.get('openid:Client')
|
||||
const client = new Client({ payload })
|
||||
|
||||
await client.save()
|
||||
return res.api(await client.to_api())
|
||||
}
|
||||
|
||||
async update_client(req, res) {
|
||||
const Client = this.models.get('openid:Client')
|
||||
const client = await Client.findById(req.params.id)
|
||||
|
||||
if ( !client )
|
||||
return res.status(404)
|
||||
.message(req.T('api.client_not_found'))
|
||||
.api()
|
||||
|
||||
const required_fields = ['client_name', 'grant_types', 'redirect_uri']
|
||||
for ( const field of required_fields ) {
|
||||
if ( !req.body[field] )
|
||||
return res.status(400)
|
||||
.message(`${req.T('api.missing_field')} ${field}`)
|
||||
.api()
|
||||
}
|
||||
|
||||
if ( !Array.isArray(req.body.grant_types) )
|
||||
return res.status(400)
|
||||
.message(`${req.T('api.improper_field')} grant_types ${req.T('api.array')}`)
|
||||
.api()
|
||||
|
||||
if ( !is_absolute_url(req.body.redirect_uri) )
|
||||
return res.status(400)
|
||||
.message(`${req.T('api.improper_field')} redirect_uri ${req.T('api.absolute_url')}`)
|
||||
.api()
|
||||
|
||||
client.payload.client_name = req.body.client_name
|
||||
client.payload.grant_types = req.body.grant_types
|
||||
client.payload.redirect_uris = [req.body.redirect_uri]
|
||||
|
||||
await client.save()
|
||||
return res.api()
|
||||
}
|
||||
|
||||
async delete_client(req, res, next) {
|
||||
const Client = this.models.get('openid: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()
|
||||
|
||||
await client.delete()
|
||||
return res.api()
|
||||
}
|
||||
|
||||
async handle_interaction(req, res) {
|
||||
const {
|
||||
uid, prompt, params, session,
|
||||
} = await this.openid_connect.provider.interactionDetails(req, res)
|
||||
|
||||
console.log({uid, prompt, params, session})
|
||||
|
||||
const name = prompt.name
|
||||
if ( typeof this[name] !== 'function' ) {
|
||||
return this.fail(res, 'Sorry, something has gone wrong.')
|
||||
}
|
||||
|
||||
return this[name](req, res, { uid, prompt, params, session })
|
||||
}
|
||||
|
||||
async consent(req, res, { uid, prompt, params, session }) {
|
||||
const Client = this.models.get('openid:Client')
|
||||
const { details: { scopes, claims } } = prompt
|
||||
const { client_id, redirect_uri } = params
|
||||
|
||||
const client_raw = await Client.findById(client_id)
|
||||
const client = client_raw.to_api()
|
||||
const uri = new URL(redirect_uri)
|
||||
|
||||
const Application = this.models.get('Application')
|
||||
const Policy = this.models.get('iam:Policy')
|
||||
const application = await Application.findOne({ openid_client_ids: params.client_id })
|
||||
if ( !application ) {
|
||||
this.output.warning('IAM Denial!')
|
||||
return this.Vue.auth_message(res, {
|
||||
message: req.T('saml.no_access').replace('APP_NAME', 'this application'),
|
||||
next_destination: '/dash',
|
||||
})
|
||||
} else if ( !(await Policy.check_user_access(req.user, application.id)) ) {
|
||||
this.output.warning('IAM Denial!')
|
||||
return this.Vue.auth_message(res, {
|
||||
message: req.T('saml.no_access').replace('APP_NAME', application.name),
|
||||
next_destination: '/dash',
|
||||
})
|
||||
}
|
||||
|
||||
return res.page('public:message', {
|
||||
...this.Vue.data({
|
||||
message: `<h3 class="font-weight-light">Authorize ${application.name}?</h3>
|
||||
<br>
|
||||
${req.T('auth.oauth_prompt').replace('CLIENT_NAME', application.name).replace('APP_NAME', this.configs.get('app.name'))}
|
||||
<br><br><br>
|
||||
<i><small>${req.T('auth.will_redirect')} ${uri.host}</small></i>`,
|
||||
|
||||
actions: [
|
||||
{
|
||||
text: req.T('common.deny'),
|
||||
action: 'redirect',
|
||||
next: '/dash',
|
||||
},
|
||||
{
|
||||
text: req.T('common.grant'),
|
||||
action: 'redirect',
|
||||
next: `/openid/interaction/${uid}/grant`,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async login(req, res, { uid, prompt, params, session }) {
|
||||
return res.redirect(`/openid/interaction/${uid}/start-session`)
|
||||
}
|
||||
|
||||
/**
|
||||
* The user has been logged in, so set the OpenID acccount ID and redirect.
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async start_session(req, res) {
|
||||
const {
|
||||
uid, prompt, params, session,
|
||||
} = await this.openid_connect.provider.interactionDetails(req, res)
|
||||
|
||||
if ( prompt.name !== 'login' ) {
|
||||
return this.fail(res,'Sorry, something has gone wrong.')
|
||||
}
|
||||
|
||||
const Application = this.models.get('Application')
|
||||
const Policy = this.models.get('iam:Policy')
|
||||
const application = await Application.findOne({ openid_client_ids: params.client_id })
|
||||
if ( !application ) {
|
||||
this.output.warning('IAM Denial!')
|
||||
return this.Vue.auth_message(res, {
|
||||
message: req.T('saml.no_access').replace('APP_NAME', 'this application'),
|
||||
next_destination: '/dash',
|
||||
})
|
||||
} else if ( !(await Policy.check_user_access(req.user, application.id)) ) {
|
||||
this.output.warning('IAM Denial!')
|
||||
return this.Vue.auth_message(res, {
|
||||
message: req.T('saml.no_access').replace('APP_NAME', application.name),
|
||||
next_destination: '/dash',
|
||||
})
|
||||
}
|
||||
|
||||
const result = {
|
||||
select_account: {},
|
||||
login: {
|
||||
account: req.user.id,
|
||||
},
|
||||
}
|
||||
|
||||
await this.openid_connect.provider.interactionFinished(req, res, result, { mergeWithLastSubmission: true })
|
||||
}
|
||||
|
||||
async process_grant(req, res) {
|
||||
const {
|
||||
uid, prompt, params, session,
|
||||
} = await this.openid_connect.provider.interactionDetails(req, res)
|
||||
|
||||
if ( prompt.name !== 'consent' ) {
|
||||
return this.fail(res,'Sorry, something has gone wrong.')
|
||||
}
|
||||
|
||||
const Application = this.models.get('Application')
|
||||
const Policy = this.models.get('iam:Policy')
|
||||
const application = await Application.findOne({ openid_client_ids: params.client_id })
|
||||
if ( !application ) {
|
||||
this.output.warning('IAM Denial!')
|
||||
return this.Vue.auth_message(res, {
|
||||
message: req.T('saml.no_access').replace('APP_NAME', 'this application'),
|
||||
next_destination: '/dash',
|
||||
})
|
||||
} else if ( !(await Policy.check_user_access(req.user, application.id)) ) {
|
||||
this.output.warning('IAM Denial!')
|
||||
return this.Vue.auth_message(res, {
|
||||
message: req.T('saml.no_access').replace('APP_NAME', application.name),
|
||||
next_destination: '/dash',
|
||||
})
|
||||
}
|
||||
|
||||
// TODO allow listing of scopes/claims and rejecting
|
||||
const consent = {
|
||||
rejectedScopes: [],
|
||||
rejectedClaims: [],
|
||||
replace: false,
|
||||
}
|
||||
|
||||
const result = { consent }
|
||||
await this.openid_connect.provider.interactionFinished(req, res, result, { mergeWithLastSubmission: true })
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OpenIDController
|
||||
@@ -115,6 +115,28 @@ class AppController extends Controller {
|
||||
application.oauth_client_ids = oauth_client_ids
|
||||
}
|
||||
|
||||
// Verify OpenID client IDs
|
||||
const OpenIDClient = this.models.get('openid:Client')
|
||||
if ( req.body.openid_client_ids ) {
|
||||
const parsed = typeof req.body.openid_client_ids === 'string' ? this.utility.infer(req.body.openid_client_ids) : req.body.openid_client_ids
|
||||
const openid_client_ids = Array.isArray(parsed) ? parsed : [parsed]
|
||||
for ( const id of openid_client_ids ) {
|
||||
const client = await OpenIDClient.findById(id)
|
||||
if ( !client )
|
||||
return res.status(400)
|
||||
.message(`${req.T('api.invalid_oauth_client_id')} ${id}`)
|
||||
.api()
|
||||
|
||||
const other_assoc_app = await Application.findOne({ openid_client_ids: client.id })
|
||||
if ( other_assoc_app )
|
||||
return res.status(400) // TODO translate this
|
||||
.message(`The OpenID Connect client ${client.name} is already associated with an existing application (${other_assoc_app.name}).`)
|
||||
.api()
|
||||
}
|
||||
|
||||
application.openid_client_ids = openid_client_ids
|
||||
}
|
||||
|
||||
// Verify SAML service provider IDs
|
||||
const ServiceProvider = this.models.get('saml:ServiceProvider')
|
||||
if ( req.body.saml_service_provider_ids ) {
|
||||
@@ -220,6 +242,28 @@ class AppController extends Controller {
|
||||
application.oauth_client_ids = oauth_client_ids
|
||||
} else application.oauth_client_ids = []
|
||||
|
||||
// Verify OpenID client IDs
|
||||
const OpenIDClient = this.models.get('openid:Client')
|
||||
if ( req.body.openid_client_ids ) {
|
||||
const parsed = typeof req.body.openid_client_ids === 'string' ? this.utility.infer(req.body.openid_client_ids) : req.body.openid_client_ids
|
||||
const openid_client_ids = Array.isArray(parsed) ? parsed : [parsed]
|
||||
for ( const id of openid_client_ids ) {
|
||||
const client = await OpenIDClient.findById(id)
|
||||
if ( !client )
|
||||
return res.status(400)
|
||||
.message(`${req.T('api.invalid_oauth_client_id')} ${id}`)
|
||||
.api()
|
||||
|
||||
const other_assoc_app = await Application.findOne({ openid_client_ids: client.id })
|
||||
if ( other_assoc_app && other_assoc_app.id !== application.id )
|
||||
return res.status(400) // TODO translate this
|
||||
.message(`The OpenID Connect client ${client.name} is already associated with an existing application (${other_assoc_app.name}).`)
|
||||
.api()
|
||||
}
|
||||
|
||||
application.openid_client_ids = openid_client_ids
|
||||
} else application.openid_client_ids = []
|
||||
|
||||
// Verify SAML service provider IDs
|
||||
const ServiceProvider = this.models.get('saml:ServiceProvider')
|
||||
if ( req.body.saml_service_provider_ids ) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const { Controller } = require('libflitter')
|
||||
const uuid = require('uuid/v4')
|
||||
const uuid = require('uuid').v4
|
||||
|
||||
class ReflectController extends Controller {
|
||||
static get services() {
|
||||
|
||||
Reference in New Issue
Block a user