SAML; Dashboard
This commit is contained in:
@@ -10,6 +10,8 @@
|
||||
*/
|
||||
const Middleware = [
|
||||
"auth:Utility",
|
||||
"auth:TrustTokenUtility",
|
||||
"SAMLUtility",
|
||||
|
||||
// 'MiddlewareName',
|
||||
|
||||
|
||||
9
app/routing/middleware/Redirect.middleware.js
Normal file
9
app/routing/middleware/Redirect.middleware.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const { Middleware } = require('libflitter')
|
||||
|
||||
class RedirectMiddleware extends Middleware {
|
||||
async test(req, res, next, { to }) {
|
||||
return res.redirect(to)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = RedirectMiddleware
|
||||
45
app/routing/middleware/SAMLRequest.middleware.js
Normal file
45
app/routing/middleware/SAMLRequest.middleware.js
Normal file
@@ -0,0 +1,45 @@
|
||||
const { Middleware } = require('libflitter')
|
||||
const samlp = require('samlp')
|
||||
|
||||
class SAMLRequestMiddleware extends Middleware {
|
||||
static get services() {
|
||||
return [...super.services, 'output', 'models']
|
||||
}
|
||||
|
||||
async test(req, res, next, args = null) {
|
||||
const ServiceProvider = this.models.get('saml:ServiceProvider')
|
||||
|
||||
samlp.parseRequest(req, async (err, data) => {
|
||||
if ( err )
|
||||
return res.error(400, { message: 'Unable to parse SAML request data.' })
|
||||
|
||||
if ( data ) {
|
||||
// Verify that the issuer is known
|
||||
const sp = await ServiceProvider.findOne({entity_id: data.issuer, active: true})
|
||||
if (!sp)
|
||||
return res.error(401, 'Unable to continue. The SAML issuer is unknown.')
|
||||
|
||||
req.saml_request = {
|
||||
relay_state: req.query.RelayState || req.body.RelayState,
|
||||
id: data.id,
|
||||
issuer: data.issuer,
|
||||
destination: data.destination,
|
||||
acs_url: data.assertionConsumerServiceURL,
|
||||
force_authn: data.forceAuthn === 'true',
|
||||
service_provider: sp,
|
||||
}
|
||||
|
||||
req.session.auth.message = `Please sign-in to continue to ${sp.name}.`
|
||||
|
||||
this.output.info('Parsed SAML request')
|
||||
this.output.debug(req.saml_request)
|
||||
} else {
|
||||
this.output.info(`Incoming request does not have an associated SAMLRequest`)
|
||||
}
|
||||
|
||||
return next()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = SAMLRequestMiddleware
|
||||
93
app/routing/middleware/SAMLUtility.middleware.js
Normal file
93
app/routing/middleware/SAMLUtility.middleware.js
Normal file
@@ -0,0 +1,93 @@
|
||||
const { Middleware } = require('libflitter')
|
||||
const { Injectable } = require('flitter-di')
|
||||
const SAMLParticipantWrapper = require('samlp/lib/sessionParticipants/index')
|
||||
|
||||
class SessionParticipantStore extends Injectable {
|
||||
static get services() {
|
||||
return ['models']
|
||||
}
|
||||
|
||||
constructor(request, participants = []) {
|
||||
super()
|
||||
this.request = request
|
||||
this.participants = participants
|
||||
this.SessionParticipant = this.models.get('saml:SessionParticipant')
|
||||
}
|
||||
|
||||
async issue({ service_provider }) {
|
||||
const sp = new this.SessionParticipant({
|
||||
service_provider_id: service_provider.id,
|
||||
name_id: this.request.user.uid,
|
||||
// session_index: this.get_index(),
|
||||
slo_url: service_provider.slo_url,
|
||||
// TODO sp_cert,
|
||||
})
|
||||
|
||||
await sp.save()
|
||||
this.participants.push(sp)
|
||||
this.flush_participants()
|
||||
return sp.session_index
|
||||
}
|
||||
|
||||
flush_participants() {
|
||||
this.request.session.saml_participant_uuids = this.participants.map(x => x.uuid)
|
||||
}
|
||||
|
||||
get_index() {
|
||||
const i = this.request.session.saml_next_index || 0
|
||||
this.request.session.saml_next_index = i + 1
|
||||
return i
|
||||
}
|
||||
|
||||
async clear() {
|
||||
for ( const p of this.participants ) {
|
||||
p.active = false
|
||||
await p.save()
|
||||
}
|
||||
|
||||
this.participants = []
|
||||
this.flush_participants()
|
||||
}
|
||||
|
||||
async wrapper() {
|
||||
const participants = []
|
||||
|
||||
for ( const p of this.participants ) {
|
||||
const sp = await this.models.get('saml:ServiceProvider').findById(p.service_provider_id)
|
||||
participants.push({
|
||||
serviceProviderId: sp.entity_id,
|
||||
nameId: p.name_id,
|
||||
nameIdFormat: p.name_id_format,
|
||||
sessionIndex: p.session_index,
|
||||
serviceProviderLogoutURL: p.slo_url,
|
||||
// TODO cert
|
||||
})
|
||||
}
|
||||
|
||||
return new SAMLParticipantWrapper(participants)
|
||||
}
|
||||
}
|
||||
|
||||
class SAMLUtilityMiddleware extends Middleware {
|
||||
static get services() {
|
||||
return [...super.services, 'models', 'app']
|
||||
}
|
||||
|
||||
async test(req, res, next, { flush = false }) {
|
||||
const SessionParticipant = this.models.get('saml:SessionParticipant')
|
||||
const di = this.app.di()
|
||||
|
||||
if ( !req.saml ) req.saml = {}
|
||||
if ( !Array.isArray(req.session.saml_participant_uuids) || flush ) {
|
||||
req.saml.participants = di.make(SessionParticipantStore, req)
|
||||
} else {
|
||||
const sps = await SessionParticipant.find({ uuid: { $in: req.session.saml_participant_uuids } })
|
||||
req.saml.participants = di.make(SessionParticipantStore, req, sps)
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = exports = SAMLUtilityMiddleware
|
||||
15
app/routing/middleware/auth/RequireTrust.middleware.js
Normal file
15
app/routing/middleware/auth/RequireTrust.middleware.js
Normal file
@@ -0,0 +1,15 @@
|
||||
const { Middleware } = require('libflitter')
|
||||
|
||||
class RequireTrustMiddleware extends Middleware {
|
||||
async test(req, res, next, { scope, deplete = false }) {
|
||||
if ( !req.trust.has(scope) ) {
|
||||
req.trust.init_flow(scope, req.originalUrl)
|
||||
return res.redirect('/auth/trust/token/issue')
|
||||
}
|
||||
|
||||
if ( deplete ) req.trust.deplete(scope)
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = RequireTrustMiddleware
|
||||
99
app/routing/middleware/auth/TrustTokenUtility.middleware.js
Normal file
99
app/routing/middleware/auth/TrustTokenUtility.middleware.js
Normal file
@@ -0,0 +1,99 @@
|
||||
const { Middleware } = require('libflitter')
|
||||
const moment = require('moment')
|
||||
const uuid = require('uuid/v4')
|
||||
|
||||
class TrustManager {
|
||||
constructor(request, response) {
|
||||
this.request = request
|
||||
this.response = response
|
||||
this.init_store()
|
||||
}
|
||||
|
||||
init_store() {
|
||||
if ( !Array.isArray(this.request.session.trust_tokens) ) {
|
||||
this.request.session.trust_tokens = []
|
||||
}
|
||||
|
||||
const now = moment()
|
||||
this.request.session.trust_tokens = this.request.session.trust_tokens.filter(x => {
|
||||
return moment(new Date(x.expires)) > now
|
||||
})
|
||||
}
|
||||
|
||||
init_flow(scope, next) {
|
||||
this.request.session.trust_flow = { scope, next, in_progress: false, requested: `${new Date}` }
|
||||
}
|
||||
|
||||
has_flow() {
|
||||
const flow = this.request.session?.trust_flow
|
||||
return flow && flow.scope && flow.next && moment(new Date(flow.requested)) < moment(new Date(flow.requested)).add('20', 'minutes')
|
||||
}
|
||||
|
||||
flow_scope() {
|
||||
if ( this.has_flow() ) {
|
||||
return this.request.session.trust_flow.scope
|
||||
}
|
||||
}
|
||||
|
||||
flow() {
|
||||
if ( this.has_flow() ) {
|
||||
return this.request.session.trust_flow.next
|
||||
}
|
||||
}
|
||||
|
||||
start() {
|
||||
delete this.request.session.user_id
|
||||
const grant_token = uuid()
|
||||
this.request.session.trust_flow.grant_token = grant_token
|
||||
this.request.session.trust_flow.in_progress = true
|
||||
this.request.session.trust_flow.started = `${new Date}`
|
||||
return grant_token
|
||||
}
|
||||
|
||||
check_grant(grant_token) {
|
||||
return grant_token === this.request.session?.trust_flow?.grant_token
|
||||
}
|
||||
|
||||
end() {
|
||||
const next = this.request.session.trust_flow.next
|
||||
delete this.request.session.trust_flow
|
||||
return next
|
||||
}
|
||||
|
||||
in_progress() {
|
||||
const flow = this.request.session.trust_flow
|
||||
return flow && flow.in_progress && flow.started && moment(new Date(flow.started)) < moment(new Date(flow.started)).add('10', 'minutes')
|
||||
}
|
||||
|
||||
has(scope) {
|
||||
return this.request.session.trust_tokens.some(x => x.scope === scope)
|
||||
}
|
||||
|
||||
grant(scope) {
|
||||
this.request.session.trust_tokens.push({
|
||||
scope,
|
||||
expires: moment().add('1', 'hour').toDate().toString(),
|
||||
})
|
||||
}
|
||||
|
||||
deplete(scope) {
|
||||
this.request.session.trust_tokens = this.request.session.trust_tokens.filter(x => x.scope !== scope)
|
||||
}
|
||||
|
||||
purge() {
|
||||
this.end()
|
||||
this.request.session.trust_tokens = []
|
||||
}
|
||||
}
|
||||
|
||||
class TrustTokenUtilityMiddleware extends Middleware {
|
||||
async test(req, res, next, args = {}) {
|
||||
if ( req.session ) {
|
||||
req.trust = new TrustManager(req, res)
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = TrustTokenUtilityMiddleware
|
||||
@@ -12,7 +12,6 @@ class UserOnly extends Middleware {
|
||||
}
|
||||
|
||||
async test(req, res, next, args = {}){
|
||||
|
||||
if ( req.is_auth && !req.session.auth.in_dmz ) return next()
|
||||
else if ( req.is_auth ) { // Need an MFA challenge
|
||||
if ( !req.session.auth.flow ) req.session.auth.flow = req.originalUrl
|
||||
|
||||
@@ -6,7 +6,7 @@ const auth_routes = {
|
||||
],
|
||||
|
||||
get: {
|
||||
|
||||
'/mfa/enable/date': ['middleware::auth:UserOnly', 'controller::api:v1:Auth.get_mfa_enable_date'],
|
||||
},
|
||||
|
||||
post: {
|
||||
@@ -14,7 +14,16 @@ const auth_routes = {
|
||||
'/attempt': [ 'controller::api:v1:Auth.attempt' ],
|
||||
'/mfa/generate': ['middleware::auth:UserOnly', 'controller::api:v1:Auth.generate_mfa_key'],
|
||||
'/mfa/attempt': ['middleware::auth:DMZOnly', 'controller::api:v1:Auth.attempt_mfa'],
|
||||
'/mfa/enable': ['middleware::auth:UserOnly', 'controller::api:v1:Auth.enable_mfa'],
|
||||
'/mfa/enable': [
|
||||
'middleware::auth:UserOnly',
|
||||
['middleware::auth:RequireTrust', { scope: 'mfa.enable', deplete: true }],
|
||||
'controller::api:v1:Auth.enable_mfa'
|
||||
],
|
||||
'/mfa/disable': [
|
||||
'middleware::auth:UserOnly',
|
||||
['middleware::auth:RequireTrust', { scope: 'mfa.disable', deplete: true }],
|
||||
'controller::api:v1:Auth.disable_mfa',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
17
app/routing/routers/api/v1/message.routes.js
Normal file
17
app/routing/routers/api/v1/message.routes.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const message_routes = {
|
||||
prefix: '/api/v1/message',
|
||||
|
||||
middleware: [
|
||||
'auth:UserOnly',
|
||||
],
|
||||
|
||||
get: {
|
||||
'/banners': ['controller::api:v1:Message.get_banners'],
|
||||
},
|
||||
|
||||
post: {
|
||||
'/banners/read/:banner_id': ['controller::api:v1:Message.read_banner'],
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = exports = message_routes
|
||||
26
app/routing/routers/api/v1/password.routes.js
Normal file
26
app/routing/routers/api/v1/password.routes.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const password_routes = {
|
||||
prefix: '/api/v1/password',
|
||||
|
||||
middleware: [
|
||||
'auth:UserOnly',
|
||||
],
|
||||
|
||||
get: {
|
||||
'/resets': ['controller::api:v1:Password.get_resets'],
|
||||
'/app_passwords': ['controller::api:v1:Password.get_app_passwords'],
|
||||
},
|
||||
|
||||
post: {
|
||||
'/app_passwords': ['controller::api:v1:Password.create_app_password'],
|
||||
'/resets': [
|
||||
['middleware::auth:RequireTrust', { scope: 'password.reset' }],
|
||||
'controller::api:v1:Password.reset_password',
|
||||
],
|
||||
},
|
||||
|
||||
delete: {
|
||||
'/app_passwords/:uuid': ['controller::api:v1:Password.delete_app_password'],
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = password_routes
|
||||
21
app/routing/routers/api/v1/profile.routes.js
Normal file
21
app/routing/routers/api/v1/profile.routes.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const profile_routes = {
|
||||
prefix: '/api/v1/profile',
|
||||
|
||||
middleware: [
|
||||
'auth:UserOnly',
|
||||
],
|
||||
|
||||
get: {
|
||||
'/:user_id': [ // user_id | 'me'
|
||||
'controller::api:v1:Profile.fetch',
|
||||
],
|
||||
},
|
||||
|
||||
patch: {
|
||||
'/:user_id': [ // user_id | 'me'
|
||||
'controller::api:v1:Profile.update',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = exports = profile_routes
|
||||
@@ -8,12 +8,22 @@ const mfa_routes = {
|
||||
get: {
|
||||
'/setup': [
|
||||
'middleware::auth:UserOnly',
|
||||
['middleware::auth:RequireTrust', { scope: 'mfa.enable' }],
|
||||
'controller::auth:MFA.setup',
|
||||
],
|
||||
'/challenge': [
|
||||
'middleware::auth:DMZOnly',
|
||||
'controller::auth:MFA.challenge',
|
||||
],
|
||||
'/disable': [
|
||||
'middleware::auth:UserOnly',
|
||||
'controller::auth:MFA.get_disable',
|
||||
],
|
||||
'/disable/process': [
|
||||
'middleware::auth:UserOnly',
|
||||
['middleware::auth:RequireTrust', { scope: 'mfa.disable' }],
|
||||
'controller::auth:MFA.do_disable',
|
||||
],
|
||||
},
|
||||
|
||||
post: {
|
||||
|
||||
16
app/routing/routers/auth/password.routes.js
Normal file
16
app/routing/routers/auth/password.routes.js
Normal file
@@ -0,0 +1,16 @@
|
||||
const password_routes = {
|
||||
prefix: '/password',
|
||||
|
||||
middleware: [
|
||||
'auth:UserOnly',
|
||||
],
|
||||
|
||||
get: {
|
||||
'/reset': [
|
||||
['middleware::auth:RequireTrust', { scope: 'password.reset' }],
|
||||
'controller::auth:Password.get_reset',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = exports = password_routes
|
||||
33
app/routing/routers/auth/saml.routes.js
Normal file
33
app/routing/routers/auth/saml.routes.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const saml_routes = {
|
||||
prefix: '/saml',
|
||||
|
||||
middleware: [
|
||||
|
||||
],
|
||||
|
||||
// TODO SLO
|
||||
|
||||
get: {
|
||||
'/metadata.xml': ['controller::saml:SAML.get_metadata'],
|
||||
'/sso': [
|
||||
'middleware::SAMLRequest',
|
||||
'middleware::auth:UserOnly',
|
||||
'controller::saml:SAML.get_sso',
|
||||
],
|
||||
'/logout': [
|
||||
'middleware::SAMLRequest',
|
||||
'middleware::auth:UserOnly',
|
||||
'controller::saml:SAML.get_logout',
|
||||
],
|
||||
},
|
||||
|
||||
post: {
|
||||
'/logout': [
|
||||
'middleware::SAMLRequest',
|
||||
'middleware::auth:UserOnly',
|
||||
'controller::saml:SAML.post_logout',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = exports = saml_routes
|
||||
18
app/routing/routers/auth/trust.routes.js
Normal file
18
app/routing/routers/auth/trust.routes.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const trust_routes = {
|
||||
prefix: '/auth/trust',
|
||||
|
||||
middleware: [
|
||||
'auth:UserOnly',
|
||||
],
|
||||
|
||||
get: {
|
||||
'/token/issue': ['controller::auth:Trust.get_issue'],
|
||||
// '/token/continue': ['controller::auth:Trust.get_continue'],
|
||||
},
|
||||
|
||||
post: {
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = exports = trust_routes
|
||||
13
app/routing/routers/dash/groups.routes.js
Normal file
13
app/routing/routers/dash/groups.routes.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const groups_routes = {
|
||||
prefix: '/dash/groups',
|
||||
|
||||
middleware: [
|
||||
'auth:UserOnly',
|
||||
],
|
||||
|
||||
get: {
|
||||
'/': [ 'controller::dash:Groups.get_listing' ]
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = exports = groups_routes
|
||||
15
app/routing/routers/dash/profile.routes.js
Normal file
15
app/routing/routers/dash/profile.routes.js
Normal file
@@ -0,0 +1,15 @@
|
||||
const profile_routes = {
|
||||
prefix: '/dash/profile',
|
||||
|
||||
middleware: ['auth:UserOnly'],
|
||||
|
||||
get: {
|
||||
'/': ['controller::dash:Profile.get_page'],
|
||||
},
|
||||
|
||||
post: {
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = exports = profile_routes
|
||||
13
app/routing/routers/dash/saml.routes.js
Normal file
13
app/routing/routers/dash/saml.routes.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const groups_routes = {
|
||||
prefix: '/dash/saml',
|
||||
|
||||
middleware: [
|
||||
'auth:UserOnly',
|
||||
],
|
||||
|
||||
get: {
|
||||
'/service-providers': [ 'controller::dash:SAML.get_sp_listing' ]
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = exports = groups_routes
|
||||
13
app/routing/routers/dash/users.routes.js
Normal file
13
app/routing/routers/dash/users.routes.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const user_routes = {
|
||||
prefix: '/dash/users',
|
||||
|
||||
middleware: [
|
||||
'auth:UserOnly',
|
||||
],
|
||||
|
||||
get: {
|
||||
'/': [ 'controller::dash:Users.get_listing' ]
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = exports = user_routes
|
||||
@@ -44,9 +44,10 @@ const index = {
|
||||
|
||||
// Placeholder for auth dashboard. You'd replace this with
|
||||
// your own route protected by 'middleware::auth:UserOnly'
|
||||
'/dash': [ 'middleware::auth:UserOnly', 'controller::Home.welcome' ],
|
||||
'/dash': [ 'middleware::auth:UserOnly', ['middleware::Redirect', {to: '/dash/profile'}] ],
|
||||
|
||||
'/tmpl': [ 'controller::Home.tmpl' ],
|
||||
// TODO remove this
|
||||
'/tmpl': [ 'middleware::auth:UserOnly', 'controller::Home.tmpl' ],
|
||||
},
|
||||
|
||||
/*
|
||||
|
||||
Reference in New Issue
Block a user