Add support for OpenID connect!!
This commit is contained in:
@@ -67,6 +67,12 @@ export default class SideBarComponent extends Component {
|
||||
type: 'resource',
|
||||
resource: 'oauth/Client',
|
||||
},
|
||||
{
|
||||
text: 'OpenID Connect Clients',
|
||||
action: 'list',
|
||||
type: 'resource',
|
||||
resource: 'openid/Client',
|
||||
},
|
||||
{
|
||||
text: 'SAML Service Providers',
|
||||
action: 'list',
|
||||
|
||||
@@ -101,6 +101,16 @@ class AppResource extends CRUDBase {
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Associated OpenID Connect Clients',
|
||||
field: 'openid_client_ids',
|
||||
type: 'select.dynamic.multiple',
|
||||
options: {
|
||||
resource: 'openid/Client',
|
||||
display: 'client_name',
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Associated SAML Service Providers',
|
||||
field: 'saml_service_provider_ids',
|
||||
|
||||
97
app/assets/app/resource/openid/Client.resource.js
Normal file
97
app/assets/app/resource/openid/Client.resource.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import CRUDBase from '../CRUDBase.js'
|
||||
import { session } from '../../service/Session.service.js'
|
||||
|
||||
class ClientResource extends CRUDBase {
|
||||
endpoint = '/openid/clients'
|
||||
required_fields = ['client_name', 'grant_types', 'redirect_uri']
|
||||
permission_base = 'v1:openid:clients'
|
||||
|
||||
item = 'OpenID Connect Client'
|
||||
plural = 'OpenID Connect Clients'
|
||||
|
||||
listing_definition = {
|
||||
display: `
|
||||
OpenID Connect clients are applications that support authentication over the OpenID Connect protocol. This allows you to add a "Sign-In with XXX" button for ${session.get('app.name')} to the application in question. To do this, the application need only comply with the OpenID standards.
|
||||
`,
|
||||
columns: [
|
||||
{
|
||||
name: 'Client Name',
|
||||
field: 'client_name',
|
||||
},
|
||||
{
|
||||
name: 'Redirect URI',
|
||||
field: 'redirect_uri',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
type: 'resource',
|
||||
position: 'main',
|
||||
action: 'insert',
|
||||
text: 'Create New',
|
||||
color: 'success',
|
||||
},
|
||||
{
|
||||
type: 'resource',
|
||||
position: 'row',
|
||||
action: 'update',
|
||||
icon: 'fa fa-edit',
|
||||
color: 'primary',
|
||||
},
|
||||
{
|
||||
type: 'resource',
|
||||
position: 'row',
|
||||
action: 'delete',
|
||||
icon: 'fa fa-times',
|
||||
color: 'danger',
|
||||
confirm: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
form_definition = {
|
||||
fields: [
|
||||
{
|
||||
name: 'Client Name',
|
||||
field: 'client_name',
|
||||
placeholder: 'Awesome External App',
|
||||
required: true,
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'Redirect URI',
|
||||
field: 'redirect_uri',
|
||||
placeholder: 'https://awesome.app/oauth2/callback',
|
||||
required: true,
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'Grant Types',
|
||||
field: 'grant_types',
|
||||
type: 'select.multiple',
|
||||
options: [
|
||||
{ display: 'Refresh Token', value: 'refresh_token' },
|
||||
{ display: 'Authorization Code', value: 'authorization_code' },
|
||||
],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'Client ID',
|
||||
field: 'client_id',
|
||||
type: 'text',
|
||||
readonly: true,
|
||||
hidden: ['insert'],
|
||||
},
|
||||
{
|
||||
name: 'Client Secret',
|
||||
field: 'client_secret',
|
||||
type: 'text',
|
||||
readonly: true,
|
||||
hidden: ['insert'],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
const openid_client = new ClientResource()
|
||||
export { openid_client }
|
||||
89
app/classes/oidc/CoreIDAdapter.js
Normal file
89
app/classes/oidc/CoreIDAdapter.js
Normal file
@@ -0,0 +1,89 @@
|
||||
const Connection = require('flitter-orm/src/services/Connection')
|
||||
const { ObjectId } = require('mongodb')
|
||||
|
||||
let DB
|
||||
|
||||
/**
|
||||
* An OpenID Connect provider adapter with some CoreID specific tweaks.
|
||||
*/
|
||||
class CoreIDAdapter {
|
||||
constructor(name) {
|
||||
this.name = 'openid_' + name
|
||||
}
|
||||
|
||||
async upsert(_id, payload, expiresIn) {
|
||||
let expiresAt
|
||||
|
||||
if (expiresIn) {
|
||||
expiresAt = new Date(Date.now() + (expiresIn * 1000))
|
||||
}
|
||||
|
||||
await this.coll().updateOne(
|
||||
{ _id },
|
||||
{ $set: { payload, ...(expiresAt ? { expiresAt } : undefined) } },
|
||||
{ upsert: true },
|
||||
)
|
||||
}
|
||||
|
||||
async find(_id) {
|
||||
if ( this.name === 'openid_Client' ) _id = ObjectId(_id)
|
||||
|
||||
const result = await this.coll().find(
|
||||
{ _id },
|
||||
{ payload: 1 },
|
||||
).limit(1).next()
|
||||
|
||||
if (!result) return undefined
|
||||
return result.payload
|
||||
}
|
||||
|
||||
async findByUserCode(userCode) {
|
||||
const result = await this.coll().find(
|
||||
{ 'payload.userCode': userCode },
|
||||
{ payload: 1 },
|
||||
).limit(1).next()
|
||||
|
||||
if (!result) return undefined
|
||||
return result.payload
|
||||
}
|
||||
|
||||
async findByUid(uid) {
|
||||
const result = await this.coll().find(
|
||||
{ 'payload.uid': uid },
|
||||
{ payload: 1 },
|
||||
).limit(1).next()
|
||||
|
||||
if (!result) return undefined
|
||||
return result.payload
|
||||
}
|
||||
|
||||
async destroy(_id) {
|
||||
await this.coll().deleteOne({ _id })
|
||||
}
|
||||
|
||||
async revokeByGrantId(grantId) {
|
||||
await this.coll().deleteMany({ 'payload.grantId': grantId })
|
||||
}
|
||||
|
||||
async consume(_id) {
|
||||
await this.coll().findOneAndUpdate(
|
||||
{ _id },
|
||||
{ $set: { 'payload.consumed': Math.floor(Date.now() / 1000) } },
|
||||
)
|
||||
}
|
||||
|
||||
coll(name) {
|
||||
return this.constructor.coll(name || this.name)
|
||||
}
|
||||
|
||||
static coll(name) {
|
||||
return DB.collection(name)
|
||||
}
|
||||
|
||||
static connect(app) {
|
||||
DB = app.di().get(Connection.name)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CoreIDAdapter
|
||||
|
||||
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() {
|
||||
|
||||
@@ -10,6 +10,7 @@ class ApplicationModel extends Model {
|
||||
saml_service_provider_ids: [String],
|
||||
ldap_client_ids: [String],
|
||||
oauth_client_ids: [String],
|
||||
openid_client_ids: [String],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +23,7 @@ class ApplicationModel extends Model {
|
||||
saml_service_provider_ids: this.saml_service_provider_ids,
|
||||
ldap_client_ids: this.ldap_client_ids,
|
||||
oauth_client_ids: this.oauth_client_ids,
|
||||
openid_client_ids: this.openid_client_ids,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const { Model } = require('flitter-orm')
|
||||
const bcrypt = require('bcrypt')
|
||||
const uuid = require('uuid/v4')
|
||||
const uuid = require('uuid').v4
|
||||
|
||||
class AppPasswordModel extends Model {
|
||||
static get schema() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const { Model } = require('flitter-orm')
|
||||
const speakeasy = require('speakeasy')
|
||||
const MFARecoveryCode = require('./MFARecoveryCode.model')
|
||||
const uuid = require('uuid/v4')
|
||||
const uuid = require('uuid').v4
|
||||
|
||||
class MFATokenModel extends Model {
|
||||
static get services() {
|
||||
|
||||
@@ -6,7 +6,7 @@ const MFAToken = require('./MFAToken.model')
|
||||
const PasswordReset = require('./PasswordReset.model')
|
||||
const AppAuthorization = require('./AppAuthorization.model')
|
||||
const AppPassword = require('./AppPassword.model')
|
||||
const uuid = require('uuid/v4')
|
||||
const uuid = require('uuid').v4
|
||||
|
||||
/*
|
||||
* Auth user model. This inherits fields and methods from the default
|
||||
@@ -197,6 +197,37 @@ class User extends AuthUser {
|
||||
get dn() {
|
||||
return LDAP.parseDN(`uid=${this.uid},${this.ldap_server.auth_dn().format(this.configs.get('ldap:server.format'))}`)
|
||||
}
|
||||
|
||||
// The following are used by OpenID connect
|
||||
|
||||
async claims(use, scope) {
|
||||
return {
|
||||
sub: this.id,
|
||||
email: this.email,
|
||||
email_verified: true, // TODO
|
||||
family_name: this.last_name,
|
||||
given_name: this.first_name,
|
||||
locale: 'en_US', // TODO
|
||||
name: `${this.first_name} ${this.last_name}`,
|
||||
preferred_username: this.uid,
|
||||
username: this.uid,
|
||||
}
|
||||
}
|
||||
|
||||
static async findByLogin(login) {
|
||||
return this.findOne({
|
||||
active: true,
|
||||
uid: login,
|
||||
})
|
||||
}
|
||||
|
||||
static async findAccount(ctx, id, token) {
|
||||
return this.findById(id)
|
||||
}
|
||||
|
||||
get accountId() {
|
||||
return this.id
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = User
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const { Model } = require('flitter-orm')
|
||||
const uuid = require('uuid/v4')
|
||||
const uuid = require('uuid').v4
|
||||
|
||||
/*
|
||||
* OAuth2 Client Model
|
||||
|
||||
44
app/models/openid/Client.model.js
Normal file
44
app/models/openid/Client.model.js
Normal file
@@ -0,0 +1,44 @@
|
||||
const { Model } = require('flitter-orm')
|
||||
const uuid = require('uuid').v4
|
||||
|
||||
class ClientModel extends Model {
|
||||
static get services() {
|
||||
return [...super.services, 'models']
|
||||
}
|
||||
|
||||
static get schema() {
|
||||
return {
|
||||
payload: {
|
||||
client_id: { type: String, default: uuid },
|
||||
client_secret: { type: String, default: uuid },
|
||||
client_name: String,
|
||||
grant_types: [String],
|
||||
redirect_uris: [String],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
to_api() {
|
||||
const vals = ['client_id', 'client_secret', 'client_name', 'grant_types']
|
||||
const val = {}
|
||||
for ( const item of vals ) {
|
||||
val[item] = this.payload[item]
|
||||
}
|
||||
val.redirect_uri = this.payload?.redirect_uris?.[0]
|
||||
val.id = this.id
|
||||
return val
|
||||
}
|
||||
|
||||
async save() {
|
||||
await super.save()
|
||||
this.payload.client_id = this.id
|
||||
return super.save()
|
||||
}
|
||||
|
||||
async application() {
|
||||
const Application = this.models.get('Application')
|
||||
return Application.findOne({ active: true, oauth_client_ids: this.id })
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = ClientModel
|
||||
@@ -1,5 +1,5 @@
|
||||
const { Model } = require('flitter-orm')
|
||||
const uuid = require('uuid/v4')
|
||||
const uuid = require('uuid').v4
|
||||
|
||||
class SessionParticipantModel extends Model {
|
||||
static get schema() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const { Middleware } = require('libflitter')
|
||||
const moment = require('moment')
|
||||
const uuid = require('uuid/v4')
|
||||
const uuid = require('uuid').v4
|
||||
|
||||
class TrustManager {
|
||||
assume_trust = false
|
||||
|
||||
52
app/routing/routers/openid.routes.js
Normal file
52
app/routing/routers/openid.routes.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const openid = {
|
||||
|
||||
prefix: '/openid',
|
||||
|
||||
middleware: [
|
||||
|
||||
],
|
||||
|
||||
get: {
|
||||
'/interaction/:uid': [
|
||||
'controller::OpenID.handle_interaction',
|
||||
],
|
||||
'/interaction/:uid/start-session': [
|
||||
'middleware::auth:UserOnly', 'controller::OpenID.start_session',
|
||||
],
|
||||
'/interaction/:uid/grant': [
|
||||
'middleware::auth:UserOnly', 'controller::OpenID.process_grant',
|
||||
],
|
||||
|
||||
'/clients': [
|
||||
['middleware::api:Permission', { check: 'v1:openid:clients:list' }],
|
||||
'controller::OpenID.get_clients',
|
||||
],
|
||||
'/clients/:id': [
|
||||
['middleware::api:Permission', { check: 'v1:openid:clients:get' }],
|
||||
'controller::OpenID.get_client',
|
||||
],
|
||||
},
|
||||
|
||||
post: {
|
||||
'/clients': [
|
||||
['middleware::api:Permission', { check: 'v1:openid:clients:create' }],
|
||||
'controller::OpenID.create_client',
|
||||
],
|
||||
},
|
||||
|
||||
patch: {
|
||||
'/clients/:id': [
|
||||
['middleware::api:Permission', { check: 'v1:openid:clients:update' }],
|
||||
'controller::OpenID.update_client',
|
||||
],
|
||||
},
|
||||
|
||||
delete: {
|
||||
'/clients/:id': [
|
||||
['middleware::api:Permission', { check: 'v1:openid:clients:delete' }],
|
||||
'controller::OpenID.delete_client',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = exports = openid
|
||||
98
app/unit/OpenIDConnectUnit.js
Normal file
98
app/unit/OpenIDConnectUnit.js
Normal file
@@ -0,0 +1,98 @@
|
||||
const Unit = require('libflitter/Unit')
|
||||
const { Provider, interactionPolicy: { Prompt, base: policy } } = require('oidc-provider')
|
||||
const uuid = require('uuid').v4
|
||||
const CoreIDAdapter = require('../classes/oidc/CoreIDAdapter')
|
||||
const RequestLocalizationHelper = require('flitter-i18n/src/RequestLocalizationHelper')
|
||||
const ResponseSystemMiddleware = require('libflitter/routing/ResponseSystemMiddleware')
|
||||
|
||||
class OpenIDConnectUnit extends Unit {
|
||||
static get name() {
|
||||
return 'openid_connect'
|
||||
}
|
||||
|
||||
static get services() {
|
||||
return [...super.services, 'output', 'configs', 'models']
|
||||
}
|
||||
|
||||
async go(app) {
|
||||
this.Vue = this.app.di().get('Vue')
|
||||
const issuer = this.configs.get('app.url')
|
||||
const configuration = this.configs.get('oidc.provider')
|
||||
const interactions = policy()
|
||||
const User = this.models.get('auth:User')
|
||||
|
||||
CoreIDAdapter.connect(app)
|
||||
|
||||
this.provider = new Provider(issuer, {
|
||||
adapter: CoreIDAdapter,
|
||||
clients: [],
|
||||
interactions: {
|
||||
interactions,
|
||||
url: (ctx, interaction) => `/openid/interaction/${ctx.oidc.uid}`,
|
||||
},
|
||||
cookies: {
|
||||
long: { signed: true, maxAge: 24 * 60 * 60 * 1000 }, // 1 day, ms
|
||||
short: { signed: true },
|
||||
keys: [this.configs.get('server.session.secret') || uuid()]
|
||||
},
|
||||
claims: {
|
||||
email: ['email'],
|
||||
profile: [
|
||||
'first_name', 'last_name', 'picture', 'tagline', 'username',
|
||||
],
|
||||
},
|
||||
features: {
|
||||
devInteractions: { enabled: false },
|
||||
deviceFlow: { enabled: true },
|
||||
introspection: { enabled: true },
|
||||
revocation: { enabled: true },
|
||||
},
|
||||
ttl: {
|
||||
AccessToken: 60 * 60, // 1 hour in seconds
|
||||
AuthorizationCode: 10 * 60, // 10 minutes in seconds
|
||||
IdToken: 60 * 60, // 1 hour in seconds
|
||||
DeviceCode: 10 * 60, // 10 minutes in seconds
|
||||
RefreshToken: 24 * 60 * 60, // 1 day in seconds
|
||||
},
|
||||
findAccount: (...args) => User.findAccount(...args),
|
||||
...configuration,
|
||||
})
|
||||
|
||||
app.express.use('/oidc', this.wrap(this.provider.callback))
|
||||
}
|
||||
|
||||
wrap(callback) {
|
||||
return async (req, res, next) => {
|
||||
const client_id = req?.query?.client_id
|
||||
|
||||
// Provide some basic Flitter niceties in the request
|
||||
req.i18n = new RequestLocalizationHelper(req, res)
|
||||
new ResponseSystemMiddleware(this.app, res, req)
|
||||
|
||||
// If we got a client ID, make sure the current user has access to it
|
||||
if ( req?.session?.auth?.user_id && client_id ) {
|
||||
const User = this.models.get('auth:User')
|
||||
const Application = this.models.get('Application')
|
||||
const Policy = this.models.get('iam:Policy')
|
||||
|
||||
const user = await User.findById(req.session.auth.user_id)
|
||||
const application = await Application.findOne({ openid_client_ids: client_id })
|
||||
if ( !application ) {
|
||||
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(user, application.id)) ) {
|
||||
return this.Vue.auth_message(res, {
|
||||
message: req.T('saml.no_access').replace('APP_NAME', application.name),
|
||||
next_destination: '/dash',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return callback(req, res, next)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = OpenIDConnectUnit
|
||||
Reference in New Issue
Block a user