SAML; Dashboard

This commit is contained in:
garrettmills
2020-05-03 20:16:54 -05:00
parent e3ecfb0d37
commit c389e151b5
1778 changed files with 148410 additions and 82 deletions

View File

@@ -10,6 +10,8 @@
*/
const Middleware = [
"auth:Utility",
"auth:TrustTokenUtility",
"SAMLUtility",
// 'MiddlewareName',

View 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

View 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

View 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

View 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

View 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

View File

@@ -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

View File

@@ -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',
],
},
}

View 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

View 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

View 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

View File

@@ -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: {

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -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' ],
},
/*