#4 & #6 - block_login, request.security, and key actions

master
garrettmills 4 years ago
parent a23fdb20b2
commit 3caeec7d14

@ -16,6 +16,8 @@ const Oauth2BearerToken = require('./model/Oauth2BearerToken')
const Oauth2Client = require('./model/Oauth2Client')
const Oauth2AuthorizationTicket = require('./model/Oauth2AuthorizationTicket')
const SecurityContext = require('./SecurityContext')
/**
* Registers functionality provided by flitter-auth.
* @extends module:libflitter/Unit~Unit
@ -74,6 +76,8 @@ class AuthUnit extends Unit {
this.canon.register_resource('auth', this.get_provider_instance.bind(this))
this.canon.register_resource('authProvider', this.resolve_provider.bind(this))
this.app.di().make(SecurityContext)
// Initialize Oauth2 Support
// await this.init_oauth()

@ -203,6 +203,8 @@ class Provider extends Injectable {
async session(request, user){
if ( !request.session.auth ) request.session.auth = {}
// request.session.auth.user = user
request.user = user
request.is_auth = true
request.session.auth.user_id = user.id
}

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

@ -84,8 +84,8 @@ class FlitterProvider extends Provider {
*/
async login(username, password, args = {}){
const User = this.User
const user = await User.findOne({ uid: username, provider: this.config.name })
const user = await User.lookup({ uid: username, provider: this.config.name })
if ( !user ) return false
const success = await this.check_user_auth(user, password)

@ -242,6 +242,8 @@ class LdapProvider extends Provider {
if ( !success ) return false
let user = await this.get_user_object(data)
if ( user.block_login ) return false
user.last_auth = new Date()
await user.save()
return user

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

@ -4,6 +4,7 @@
const Middleware = require('libflitter/middleware/Middleware')
const helpers = require('../Helpers')
const SecurityContext = require('../SecurityContext')
/**
* This should be applied globally. Ensures basic things about the request
@ -17,13 +18,20 @@ class Utility extends Middleware {
* @returns {Array<string>}
*/
static get services() {
return [...super.services, 'models']
return [...super.services, 'models', 'output']
}
/**
* Applies the middleware. Creates an unauthenticated auth session if one doesn't already exist.
* If the user is authenticated, injects an instance of {module:flitter-auth/model/User~User} into
* the request as 'request.user'.
*
* Also instantiates a {@link module:flitter-auth/SecurityContext~SecurityContext} for the request
* and injects it as 'request.security'.
*
* If the session has a key_action_key property, the respective {@link module:flitter-auth/model/KeyAction~KeyAction}
* will be inserted into "request.key_action".
*
* @param {express/request} req - the request
* @param {express/response} res - the response
* @param {function} next - the next function in the stack
@ -44,12 +52,27 @@ class Utility extends Middleware {
// If we have a user_id, but can't find a user in the database,
// something has gone horribly wrong. To prevent security flaws,
// reset the session to the logged-out state.
this.output.warn(`Authenticated session with invalid user_id: ${req.session.auth.user_id}. Kicking user.`)
delete req.user
req.session.auth.user_id = false
req.is_auth = false
}
}
// Create a security context for the request
const security = new SecurityContext(req, res)
req.security = security
if ( req.session.key_action_key ) {
const KeyAction = this.models.get('auth:KeyAction')
const ka = await KeyAction.findOne({key: req.session.key_action_key})
if ( ka ) {
req.key_action = ka
} else {
delete req.session.key_action_key
}
}
/*
* Call the next function in the stack.
*/

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

@ -32,12 +32,27 @@ class BaseUser extends Model {
roles: [String],
permissions: [String],
last_auth: Date,
block_login: Boolean,
// Fields needed by the flitter provider:
password: String,
}
}
static async authFilter() {
const filter = (await this.filter()).field('block_login')
.not().equal(true).end()
.end()
return filter
}
static async lookup(params) {
const filter = await this.authFilter()
filter.absorb(params)
return filter.end().findOne()
}
/**
* Get's a value from the user's serialized JSON.
* @param {string} key

@ -124,6 +124,7 @@ class Oauth2Provider extends Provider {
if ( this.config.user_data.data_root ) user_data = user_data[this.config.user_data.data_root]
const user = await this._get_user_object(user_data)
if ( user.block_login ) return false
await user.save()
return user

Loading…
Cancel
Save