parent
a23fdb20b2
commit
3caeec7d14
@ -0,0 +1,131 @@
|
||||
/**
|
||||
* @module flitter-auth/SecurityContext
|
||||
*/
|
||||
|
||||
const { Injectable } = require('flitter-di')
|
||||
|
||||
/**
|
||||
* Request-specific security context that provides helper functions
|
||||
* with regard to security checks for the relevant request.
|
||||
* @extends module:flitter-di/src/Injectable~Injectable
|
||||
*/
|
||||
class SecurityContext extends Injectable {
|
||||
/**
|
||||
* Defines the services required by this unit.
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
static get services() {
|
||||
return [...super.services, 'configs', 'auth', 'models']
|
||||
}
|
||||
|
||||
/**
|
||||
* The relevant request.
|
||||
* @type {express/request}
|
||||
*/
|
||||
#request
|
||||
|
||||
/**
|
||||
* The relevant response.
|
||||
* @type {express/response}
|
||||
*/
|
||||
#response
|
||||
|
||||
/**
|
||||
* Instantiate the security context.
|
||||
* @param {express/request} req - the relevant request
|
||||
* @param {express/response} res - the relevant response
|
||||
*/
|
||||
constructor(req, res) {
|
||||
super()
|
||||
this.#request = req
|
||||
this.#response = res
|
||||
}
|
||||
|
||||
/**
|
||||
* Deny the client access to the requested resource.
|
||||
* Displays the 401 error page and passes along the specified message.
|
||||
* @param {string} [message = 'Access Denied']
|
||||
*/
|
||||
deny(message = 'Access Denied') {
|
||||
this.#response.error(401, {message})
|
||||
}
|
||||
|
||||
/**
|
||||
* Deny the client access to the requested resource.
|
||||
* Displays the 401 error page and passes along the specified message.
|
||||
* If the request has a user in the session, the user will be forcibly signed out.
|
||||
* @param {string} [message = 'Access Denied']
|
||||
*/
|
||||
kickout(message = 'Access Denied') {
|
||||
if ( this.#request.user ) {
|
||||
this.provider().logout(this.#request).then(() => {
|
||||
this.deny(message)
|
||||
})
|
||||
} else {
|
||||
this.deny(message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deny the client access to the requested resource.
|
||||
* Displays the 401 error page and passes along the specified message.
|
||||
* If the request has a user in the session, the user's block_login flag will
|
||||
* be set, and they will be forcibly signed out.
|
||||
*
|
||||
* WARNING: this flag will prevent the user from signing into the application AT ALL.
|
||||
* @param {string} [message = 'Access Denied']
|
||||
*/
|
||||
ban(message = 'Access Denied') {
|
||||
if ( this.#request.user ) {
|
||||
this.#request.user.block_login = true
|
||||
this.#request.user.save().then(() => {
|
||||
this.kickout(message)
|
||||
})
|
||||
} else {
|
||||
this.deny(message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the auth provider for the request.
|
||||
* If the request is authenticated, use the user's provider.
|
||||
* Otherwise, if a provider exists in the route params, use that.
|
||||
* Otherwise, use the default_provider specified in the config.
|
||||
* @returns {string}
|
||||
*/
|
||||
provider_name() {
|
||||
let provider_name = this.configs.get('auth.default_provider')
|
||||
if ( this.#request.is_auth ) provider_name = this.#request.user.provider
|
||||
else provider_name = this.#request.params.provider ? this.#request.params.provider : provider_name
|
||||
return provider_name
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the auth provider for the request.
|
||||
* @returns {module:flitter-auth/Provider~Provider}
|
||||
*/
|
||||
provider() {
|
||||
return this.auth.get_provider(this.provider_name())
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a key action that will resolve to the specified handler.
|
||||
*
|
||||
* @example
|
||||
* const action = await request.security.key_action('controller::Home.password_reset')
|
||||
* return res.send(`Reset your password at: ${action.url()}`)
|
||||
* @param {string} handler - canonical name of the handler - e.g. "controller::Home.welcome"
|
||||
* @returns {Promise<module:flitter-auth/model/KeyAction~KeyAction>}
|
||||
*/
|
||||
async key_action(handler) {
|
||||
const KeyAction = this.models.get('auth:KeyAction')
|
||||
const ka_data = { handler, used: false }
|
||||
if ( this.#request.user ) ka_data.user_id = this.#request.user._id
|
||||
|
||||
const ka = new KeyAction(ka_data)
|
||||
await ka.save()
|
||||
return ka
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = SecurityContext
|
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* @module flitter-auth/controllers/KeyAction
|
||||
*/
|
||||
|
||||
const Controller = require('libflitter/controller/Controller')
|
||||
|
||||
/**
|
||||
* Provides handler methods for flitter-auth's key actions.
|
||||
* Key actions allow your application to dynamically generate
|
||||
* one-time links that call methods on controllers and (optionally)
|
||||
* can even automatically sign in a user for the request, then log
|
||||
* them out. e.g. a password reset link could use a key action.
|
||||
* @extends module:libflitter/controller/Controller~Controller
|
||||
*/
|
||||
class KeyAction extends Controller {
|
||||
/**
|
||||
* Defines the services required by this unit.
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
static get services() {
|
||||
return [...super.services, 'canon']
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a key action request by calling the configured
|
||||
* controller handler method. Closes the key action afterward
|
||||
* based on the configured settings.
|
||||
* @param {express/request} req - the request
|
||||
* @param {express/response} res - the response
|
||||
* @param {function} next - the next function in the stack
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async handle(req, res, next) {
|
||||
if ( !req.key_action ) throw new Error('Missing required key action.')
|
||||
const handler = await this.canon.get(req.key_action.handler)
|
||||
if ( !handler ) throw new Error('Unable to find handler for key action.')
|
||||
await handler(req, res, next)
|
||||
await req.key_action.close(req)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = exports = KeyAction
|
@ -0,0 +1,16 @@
|
||||
const Controller = require('flitter-auth/controllers/KeyAction')
|
||||
|
||||
/*
|
||||
* KeyAction Controller
|
||||
* -------------------------------------------------------------
|
||||
* Provides handler methods for flitter-auth's key actions.
|
||||
* Key actions allow your application to dynamically generate
|
||||
* one-time links that call methods on controllers and (optionally)
|
||||
* can even automatically sign in a user for the request, then log
|
||||
* them out. e.g. a password reset link could use a key action.
|
||||
*/
|
||||
class KeyAction extends Controller {
|
||||
|
||||
}
|
||||
|
||||
module.exports = exports = KeyAction
|
@ -0,0 +1,25 @@
|
||||
const Model = require('flitter-auth/model/KeyAction')
|
||||
|
||||
/*
|
||||
* KeyAction Model
|
||||
* -------------------------------------------------------------
|
||||
* Represents a single available key action. Key actions
|
||||
* are one-time use links that directly call a method on
|
||||
* a controller. These actions:
|
||||
*
|
||||
* - Can pass along context
|
||||
* - Have expiration dates
|
||||
* - Are single-use only
|
||||
* - Can automatically log in a user during the request lifecycle
|
||||
*
|
||||
* You can generate these actions using the request.security.keyaction()
|
||||
* method.
|
||||
*
|
||||
* See: module:flitter-auth/SecurityContext~SecurityContext#keyaction
|
||||
* See: module:flitter-auth/model/KeyAction~KeyAction
|
||||
*/
|
||||
class KeyAction extends Model {
|
||||
|
||||
}
|
||||
|
||||
module.exports = exports = KeyAction
|
@ -0,0 +1,12 @@
|
||||
const Middleware = require('flitter-auth/middleware/KeyAction')
|
||||
|
||||
/*
|
||||
* KeyAction Middleware
|
||||
* -------------------------------------------------------------
|
||||
* Middleware for processing key actions.
|
||||
*/
|
||||
class KeyAction extends Middleware {
|
||||
|
||||
}
|
||||
|
||||
module.exports = exports = KeyAction
|
@ -0,0 +1,16 @@
|
||||
module.exports = exports = {
|
||||
prefix: '/auth/action', // This is assumed by flitter-auth. Don't change it.
|
||||
middleware: [],
|
||||
get: {
|
||||
'/:key': [
|
||||
'middleware::auth:KeyAction',
|
||||
'controller::auth:KeyAction.handle',
|
||||
],
|
||||
},
|
||||
post: {
|
||||
'/:key': [
|
||||
'middleware::auth:KeyAction',
|
||||
'controller::auth:KeyAction.handle',
|
||||
],
|
||||
},
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
/**
|
||||
* @module flitter-auth/middleware/KeyAction
|
||||
*/
|
||||
|
||||
const Middleware = require('libflitter/middleware/Middleware')
|
||||
|
||||
/**
|
||||
* Middleware for processing key actions.
|
||||
* @extends module:libflitter/middleware/Middleware~Middleware
|
||||
*/
|
||||
class KeyAction extends Middleware {
|
||||
/**
|
||||
* Defines the services required by this middleware.
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
static get services() {
|
||||
return [...super.services, 'models']
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up the key action from the request params' "key"
|
||||
* property and injects it into the session. If necessary,
|
||||
* authenticates the user and injects them as well.
|
||||
*
|
||||
* Will send a 401 denial if the key action or user are invalid.
|
||||
*
|
||||
* @param {express/request} req - the request
|
||||
* @param {express/response} res - the response
|
||||
* @param {function} next - the next function in the stack
|
||||
* @param {*} [args = {}] - optional arguments
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async test(req, res, next, args = {}){
|
||||
const KeyAction = this.models.get('auth:KeyAction')
|
||||
|
||||
const lookup_key = req.params.key ? req.params.key : (req.session.key_action_key ? req.session.key_action_key : false)
|
||||
if ( !lookup_key ) return req.security.deny()
|
||||
|
||||
const action = await KeyAction.lookup({ key: lookup_key })
|
||||
if ( !action ) return req.security.deny()
|
||||
|
||||
if ( action.user_id ) {
|
||||
const user = await action.user()
|
||||
if ( req.user && String(req.user._id) !== String(user._id) ) return req.security.kickout()
|
||||
if ( action.auto_login ) {
|
||||
if ( req.user ) {
|
||||
action.did_auto_login = false
|
||||
} else {
|
||||
const provider = await req.security.provider()
|
||||
await provider.session(req, user)
|
||||
action.did_auto_login = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
action.used = true
|
||||
await action.save()
|
||||
|
||||
req.key_action = action
|
||||
req.session.key_action_key = String(action.key)
|
||||
|
||||
/*
|
||||
* Call the next function in the stack.
|
||||
*/
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = KeyAction
|
@ -0,0 +1,151 @@
|
||||
/**
|
||||
* @module flitter-auth/model/KeyAction
|
||||
*/
|
||||
|
||||
const Model = require('flitter-orm/src/model/Model')
|
||||
const uuid = require('uuid/v4')
|
||||
const { ObjectId } = require('mongodb')
|
||||
|
||||
/**
|
||||
* Represents a single available key action. Key actions
|
||||
* are one-time use links that directly call a method on
|
||||
* a controller. These actions:
|
||||
*
|
||||
* - Can pass along context
|
||||
* - Have expiration dates
|
||||
* - Are single-use only
|
||||
* - Can automatically log in a user during the request lifecycle
|
||||
* @extends module:flitter-orm/src/model/Model~Model
|
||||
*/
|
||||
class KeyAction extends Model {
|
||||
/**
|
||||
* Defines the services required by this model.
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
static get services() {
|
||||
return [...super.services, 'models', 'configs']
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the schema for this model. Provides the following fields:
|
||||
* - key: String [uuid]
|
||||
* - secret: String [uuid]
|
||||
* - user_id: ObjectId
|
||||
* - handler: String
|
||||
* - created: Date [now]
|
||||
* - expires: Date [now +1 day]
|
||||
* - used: Boolean
|
||||
* - auto_login: Boolean
|
||||
* - no_auto_logout: Boolean
|
||||
* - did_auto_login: Boolean
|
||||
* - data: String ['{}']
|
||||
* @type {object}
|
||||
*/
|
||||
static get schema() {
|
||||
return {
|
||||
key: { type: String, default: uuid },
|
||||
secret: { type: String, default: uuid },
|
||||
user_id: ObjectId,
|
||||
handler: String,
|
||||
created: { type: Date, default: () => new Date },
|
||||
expires: {
|
||||
type: Date,
|
||||
default: () => {
|
||||
const date = new Date
|
||||
date.setDate(date.getDate() + 1)
|
||||
return date
|
||||
}
|
||||
},
|
||||
used: Boolean,
|
||||
auto_login: Boolean,
|
||||
no_auto_logout: Boolean,
|
||||
did_auto_login: Boolean,
|
||||
data: { type: String, default: '{}' },
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a filter object for this model that restricts
|
||||
* the result set to unused, unexpired key actions.
|
||||
* @returns {Promise<module:flitter-orm/src/filter/Filter~Filter>}
|
||||
*/
|
||||
static async availableFilter() {
|
||||
const filter = await this.filter()
|
||||
filter.field('expires').greater_than(new Date).end()
|
||||
filter.field('used').not().equal(true).end().end()
|
||||
return filter
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup a single key action based on the passed in filter parameters.
|
||||
* Automatically restricts the result set to unused, unexpired key actions.
|
||||
* @param {object} params
|
||||
* @returns {Promise<module:flitter-auth/model/KeyAction~KeyAction>}
|
||||
*/
|
||||
static async lookup(params) {
|
||||
const filter = await this.availableFilter()
|
||||
filter.absorb(params)
|
||||
return filter.end().findOne()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a value from the action's metadata.
|
||||
* @param {string} key
|
||||
* @returns {*}
|
||||
*/
|
||||
data_get(key) {
|
||||
return JSON.parse(this.data ? this.data : '{}')[key]
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a value in the action's metadata.
|
||||
* @param {string} key
|
||||
* @param {*} value
|
||||
*/
|
||||
data_set(key, value) {
|
||||
const data = JSON.parse(this.data ? this.data : '{}')
|
||||
data[key] = value
|
||||
this.data = JSON.stringify(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL path for this key action. Uses the 'app.url' config.
|
||||
* @returns {string}
|
||||
*/
|
||||
url() {
|
||||
const config_app_url = this.configs.get('app.url')
|
||||
const base_url = config_app_url.endsWith('/') ? config_app_url : `${config_app_url}/`
|
||||
return `${base_url}auth/action/${this.key}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the associated user for this action.
|
||||
* @returns {Promise<module:flitter-auth/model/User~BaseUser|void>}
|
||||
*/
|
||||
async user() {
|
||||
const User = this.models.get('auth:User')
|
||||
return this.has_one(User, 'user_id', '_id')
|
||||
}
|
||||
|
||||
/**
|
||||
* Close out the key action. If did_auto_login is set and no_auto_logout is not set,
|
||||
* log out the user and remove the key action from the session for the provided request.
|
||||
* @param {express/request} request - the request
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async close(request) {
|
||||
if ( request.key_action && request.key_action.did_auto_login && !request.key_action.no_auto_logout ) {
|
||||
const action = request.key_action
|
||||
delete request.key_action
|
||||
delete request.session.key_action_key
|
||||
|
||||
if ( action.user_id && action.auto_login && request.is_auth ) {
|
||||
const provider = await request.security.provider()
|
||||
await provider.logout(request)
|
||||
await request.session.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = KeyAction
|
Loading…
Reference in new issue