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 ) 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) const name = prompt.name if ( typeof this[name] !== 'function' ) { return this.fail(res, 'Sorry, something has gone wrong.') } return this[name](req, res, { uid: uid.toLowerCase(), 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.warn('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.warn('IAM Denial!') return this.Vue.auth_message(res, { message: req.T('saml.no_access').replace('APP_NAME', application.name), next_destination: '/dash', }) } // If the user has already authorized this app, just redirect if ( req.user.has_authorized({ id: application.id }) ) { return res.redirect(`/openid/interaction/${uid.toLowerCase()}/grant`) } // Otherwise, prompt them for authorization return res.page('public:message', { ...this.Vue.data({ message: `

Authorize ${application.name}?


${req.T('auth.oauth_prompt').replace('CLIENT_NAME', application.name).replace('APP_NAME', this.configs.get('app.name'))}


${req.T('auth.will_redirect')} ${uri.host}`, actions: [ { text: req.T('common.deny'), action: 'redirect', next: '/dash', }, { text: req.T('common.grant'), action: 'redirect', next: `/openid/grant-and-save/${application.id}/${uid.toLowerCase()}`, }, { text: req.T('common.grant_once'), action: 'redirect', next: `/openid/interaction/${uid.toLowerCase()}/grant`, }, ], }) }) } async grant_and_save(req, res, next) { if ( !req.user.has_authorized({ id: req.params.app_id }) ) { req.user.authorize({ id: req.params.app_id, api_scopes: ['openid-connect'], }) await req.user.save() } return res.redirect(`/openid/interaction/${req.params.uid.toLowerCase()}/grant`) } async login(req, res, { uid, prompt, params, session }) { return res.redirect(`/openid/interaction/${uid.toLowerCase()}/start-session`) } /** * The user has been logged in, so set the OpenID acccount ID and redirect. * @param req * @param res * @returns {Promise} */ 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.warn('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.warn('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.warn('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.warn('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