├── .npmrc ├── .travis.yml ├── .gitignore ├── example ├── files │ ├── static403.html │ └── template403.marko ├── 4 - caching.js ├── 1 - simple.js ├── 2 - hierarchy.js ├── 3 - withHTTPBasicAuth.js ├── 6 - withHapiFormAuthentication.js └── 5 - customForbiddenPage.js ├── test ├── test.cache.js ├── test.options.js ├── test.authorization.js └── integration.js ├── lib ├── cache.js ├── options.js └── authorization.js ├── package.json ├── index.js └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | npm-debug.log 4 | coverage 5 | .DS_Store -------------------------------------------------------------------------------- /example/files/static403.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Go away! 5 | 6 | 7 | 8 |

9 | Oops :/ 10 |

11 |

12 | Looks like you're not allowed to be here. 13 |

14 | 15 | -------------------------------------------------------------------------------- /example/files/template403.marko: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Go away! 5 | 6 | 7 | 8 |

9 | Oops :/ 10 |

11 |

12 | Sorry ${input.firstName}, you can't handle this page! 13 |

14 | 15 | -------------------------------------------------------------------------------- /test/test.cache.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const chai = require('chai') 5 | chai.should() 6 | 7 | let cache 8 | 9 | describe('cache.js', function () { 10 | beforeEach(function () { 11 | cache = require(path.join(__dirname, '..', 'lib', 'cache.js'))() 12 | }) 13 | afterEach(function () { 14 | cache.reset() 15 | cache = undefined 16 | }) 17 | it('cache should initialize', function () { 18 | cache.should.not.be.null 19 | }) 20 | it('cache should store value', function () { 21 | cache.set('foo', 'bar') 22 | cache.get('foo').should.equal('bar') 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /lib/cache.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const LRU = require('lru-cache') 4 | const deepExtend = require('deep-extend') 5 | const debug = require('debug')('hapi-acl-auth:cache') 6 | 7 | let _cache 8 | 9 | const defaultOptions = { 10 | max: 500, 11 | length: function (n, key) { 12 | return n * 2 + key.length 13 | }, 14 | maxAge: 1000 * 60 * 60 15 | } 16 | 17 | module.exports = function (options) { 18 | if (!_cache) { 19 | const cacheOptions = deepExtend({}, defaultOptions, options) 20 | debug('cacheOptions: ') 21 | debug(cacheOptions) 22 | _cache = LRU(cacheOptions) 23 | } 24 | return _cache 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-acl-auth", 3 | "version": "1.0.5", 4 | "description": "Authentication agnostic authorization plugin for HapiJS", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/charlesread/hapi-acl-auth" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/charlesread/hapi-acl-auth/issues", 17 | "email": "charles@charlesread.com" 18 | }, 19 | "keywords": [ 20 | "hapi", 21 | "authorization", 22 | "acl", 23 | "auth" 24 | ], 25 | "dependencies": { 26 | "boom": "^7.1.1", 27 | "debug": "^3.1.0", 28 | "deep-extend": "^0.5.0", 29 | "joi": "^13.1.1", 30 | "lodash": "^4.17.4", 31 | "lru-cache": "^4.1.1", 32 | "type-detect": "^4.0.7" 33 | }, 34 | "devDependencies": { 35 | "chai": "^4.1.2", 36 | "hapi": "^17.2.0", 37 | "mocha": "^5.0.0", 38 | "pre-commit": "^1.2.2", 39 | "request": "^2.83.0" 40 | }, 41 | "standard": { 42 | "env": [ 43 | "mocha" 44 | ], 45 | "ignore": [ 46 | "test/**" 47 | ] 48 | }, 49 | "pre-commit": [ 50 | "test" 51 | ], 52 | "optionalDependencies": { 53 | "hapi-auth-basic": "^5.0.0", 54 | "marko": "^4.7.5" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/options.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const util = require('util') 4 | const deepExtend = require('deep-extend') 5 | const joi = require('joi') 6 | const debug = require('debug')('hapi-acl-auth:options') 7 | 8 | const pluginDefaults = { 9 | any: true, 10 | all: false, 11 | policy: 'deny', 12 | secure: true 13 | } 14 | 15 | const optionsSchema = joi.object().keys({ 16 | handler: joi.func().required(), 17 | roles: joi.alternatives().try(joi.array(), joi.func()), 18 | any: joi.boolean().required(), 19 | all: joi.boolean().required(), 20 | forbiddenPageFunction: joi.func().optional(), 21 | hierarchy: joi.array().items(joi.string()).optional(), 22 | secure: joi.boolean().required(), 23 | policy: joi.string().valid('allow', 'deny').required(), 24 | cache: joi.boolean().optional(), 25 | allowUnauthenticated: joi.boolean().optional(), 26 | exempt: joi.array().optional() 27 | }) 28 | 29 | module.exports = function (req, options) { 30 | const routeOptions = req.route.settings.plugins['hapi-acl-auth'] || req.route.settings.plugins['hapiAclAuth'] || {} 31 | const pluginOptions = deepExtend({}, pluginDefaults, options) 32 | const combinedOptions = deepExtend({}, pluginOptions, routeOptions) 33 | debug('routeOptions:') 34 | debug(routeOptions) 35 | debug('pluginOptions:') 36 | debug(pluginOptions) 37 | debug('combinedOptions:') 38 | debug(combinedOptions) 39 | const _options = joi.validate(combinedOptions, optionsSchema) 40 | if (_options.error) { 41 | throw new Error(util.format('options could not be validated: %s', _options.error.message)) 42 | } 43 | return {routeOptions, pluginOptions, combinedOptions} 44 | } 45 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const pjson = require('./package.json') 4 | 5 | const path = require('path') 6 | const Boom = require('boom') 7 | const debug = require('debug')('hapi-acl-auth:plugin') 8 | 9 | const options = require(path.join(__dirname, 'lib', 'options.js')) 10 | const authorization = require(path.join(__dirname, 'lib', 'authorization.js')) 11 | 12 | const plugin = {} 13 | 14 | plugin.register = async function (server, opts) { 15 | let result 16 | debug('opts:') 17 | debug(opts) 18 | server.ext('onPostAuth', async function (req, h) { 19 | try { 20 | debug('request for %s caught', req.path) 21 | if (opts.exempt && opts.exempt.includes(req.path)) { 22 | debug('%s is exempt', req.path) 23 | return h.continue 24 | } else { 25 | debug('%s is not exempt', req.path) 26 | } 27 | if (opts.allowUnauthenticated && !req.auth.isAuthenticated) { 28 | return h.continue 29 | } 30 | const {routeOptions, pluginOptions, combinedOptions} = options(req, opts) 31 | if ((pluginOptions.policy === 'deny' && combinedOptions.secure) || (pluginOptions.policy === 'allow' && routeOptions.secure)) { 32 | debug('policy if statement has evaluated to true') 33 | const handlerResult = combinedOptions.handler(req) 34 | const handlerObject = handlerResult.then ? await handlerResult : handlerResult 35 | debug('handlerObject:') 36 | debug(handlerObject) 37 | const isAuthorized = await authorization.determineAuthorization(combinedOptions, handlerObject, req) 38 | debug('isAuthorized: %s', isAuthorized) 39 | if (!isAuthorized) { 40 | const forbiddenResult = combinedOptions.forbiddenPageFunction ? combinedOptions.forbiddenPageFunction(handlerObject, req, h) : Boom.forbidden() 41 | result = forbiddenResult && forbiddenResult.then ? await forbiddenResult : forbiddenResult 42 | } else { 43 | result = h.continue 44 | } 45 | } else { 46 | debug('policy if statement has evaluated to false') 47 | result = h.continue 48 | } 49 | } catch (e) { 50 | throw e 51 | } 52 | debug('result:') 53 | debug(result) 54 | return result 55 | }) 56 | } 57 | 58 | plugin.pkg = pjson 59 | 60 | module.exports = plugin 61 | -------------------------------------------------------------------------------- /example/4 - caching.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* 4 | 5 | Simple example with caching, also displays the ability for `roles` to be a function. 6 | 7 | The `roles` attributes in both the plugin and route options can be either arrays of strings, functions that return 8 | arrays, or functions that return Promises that resolve arrays. If the functions are do resource intensive operations, 9 | or operations that can take some time, it might make sense to cache the results so that each request does not have 10 | to wait on the function to return. The `cache` plugin attribute will enable caching of the roles, increasing 11 | response time and generally just being more responsible with resources. 12 | 13 | Policy is `deny`, by default. 14 | User has one role ('admin'). 15 | User has role required by /admin. 16 | 17 | */ 18 | 19 | const Hapi = require('hapi') 20 | 21 | const server = new Hapi.Server() 22 | server.connection({ 23 | host: 'localhost', 24 | port: 8000 25 | }) 26 | 27 | const plugins = [ 28 | { 29 | register: require('../index'), 30 | options: { 31 | handler: function (request, callback) { 32 | callback(null, {username: 'cread', roles: ['admin']}) 33 | }, 34 | cache: true 35 | } 36 | } 37 | ] 38 | 39 | server.register( 40 | plugins, 41 | function (err) { 42 | if (err) { 43 | throw err 44 | } 45 | 46 | // notice that the first request to this route will take 5000 milliseconds to respond, subsequent 47 | // request will not, because the result of the function is cached 48 | server.route({ 49 | method: 'GET', 50 | path: '/admin', 51 | handler: function (request, reply) { 52 | return reply('admin') 53 | }, 54 | config: { 55 | plugins: { 56 | hapiAclAuth: { 57 | roles: function () { 58 | return new Promise((resolve) => { 59 | // simulating some task that takes some time, like hitting a DB for roles for this route 60 | setTimeout(function () { 61 | resolve(['admin1']) 62 | }, 5000) 63 | }) 64 | } 65 | } 66 | } 67 | } 68 | }) 69 | }) 70 | 71 | server.start((err) => { 72 | if (err) { 73 | throw err 74 | } 75 | console.log('Server running at:', server.info.uri) 76 | }) 77 | -------------------------------------------------------------------------------- /example/1 - simple.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* 4 | 5 | Simple example. 6 | 7 | Policy is `deny`, by default. 8 | User has one role ('admin'). 9 | User has role required by /admin. 10 | User does not have role required by /superuser. 11 | Endpoint /notsecure has secure: false, thus overriding the default deny policy. 12 | 13 | */ 14 | 15 | const Hapi = require('hapi') 16 | 17 | const server = Hapi.server({ 18 | host: 'localhost', 19 | port: 8000 20 | }) 21 | 22 | ;(async function () { 23 | await server.register({ 24 | plugin: require('hapi-acl-auth'), 25 | options: { 26 | handler: async function () { 27 | return {user: 'cread', roles: ['admin']} 28 | }, 29 | // optional, dy default a simple 403 will be returned when not authorized 30 | forbiddenPageFunction: async function (credentials, request, h) { 31 | // some fancy "logging" 32 | console.log('%s (roles: %s) wanted %s (requires %s) but was not allowed', credentials.user, credentials.roles, request.path, request.route.settings.plugins['hapiAclAuth'].roles) 33 | // some fancy error page 34 | const response = h.response('

Not Authorized!

') 35 | response.code(200) 36 | return response.takeover() 37 | } 38 | } 39 | }) 40 | server.route({ 41 | method: 'get', 42 | path: '/admin', 43 | handler: async function (request, h) { 44 | return '

Welcome to /admin!

' 45 | }, 46 | config: { 47 | plugins: { 48 | hapiAclAuth: { 49 | roles: ['admin'] 50 | } 51 | } 52 | } 53 | }) 54 | server.route({ 55 | method: 'get', 56 | path: '/superuser', 57 | handler: async function (request, h) { 58 | return '

Welcome to /superuser!

' 59 | }, 60 | config: { 61 | plugins: { 62 | hapiAclAuth: { 63 | roles: ['superuser'] 64 | } 65 | } 66 | } 67 | }) 68 | server.route({ 69 | method: 'get', 70 | path: '/notsecure', 71 | handler: async function (request, h) { 72 | return '

Welcome to /notsecure!

' 73 | }, 74 | config: { 75 | plugins: { 76 | hapiAclAuth: { 77 | secure: false 78 | } 79 | } 80 | } 81 | }) 82 | await server.start() 83 | })() 84 | .then(function () { 85 | console.log('server started: %s', server.info.uri) 86 | }) 87 | .catch(function (err) { 88 | console.error(err.message) 89 | }) 90 | -------------------------------------------------------------------------------- /lib/authorization.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const type = require('type-detect') 5 | const _ = require('lodash') 6 | const debug = require('debug')('hapi-acl-auth:authorization') 7 | 8 | const cache = require(path.join(__dirname, 'cache.js'))() 9 | 10 | module.exports = { 11 | determineAuthorization: async function (combinedOptions, handlerObject, req) { 12 | debug('combinedOptions:') 13 | debug(combinedOptions) 14 | debug('handlerObject:') 15 | debug(handlerObject) 16 | let allowed = [] 17 | let actual = [] 18 | const cachedData = cache.get(req.path) 19 | if (cachedData && combinedOptions.cache) { 20 | allowed = cachedData.allowed 21 | actual = cachedData.actual 22 | } else { 23 | if (type(combinedOptions.roles) === 'Array') { 24 | allowed = combinedOptions.roles 25 | } else if (type(combinedOptions.roles) === 'string') { 26 | allowed.push(combinedOptions.roles) 27 | } else if (type(combinedOptions.roles) === 'function') { 28 | const res = combinedOptions.roles(handlerObject, req) 29 | allowed = res.then ? await res : res 30 | } 31 | if (type(handlerObject.roles) === 'Array') { 32 | actual = handlerObject.roles 33 | } else if (type(handlerObject.roles) === 'string') { 34 | actual.push(handlerObject.roles) 35 | } else if (type(handlerObject.roles) === 'function') { 36 | const res = handlerObject.roles(handlerObject, req) 37 | actual = res.then ? await res : res 38 | } 39 | if (combinedOptions.cache) { 40 | cache.set(req.path, {allowed, actual}) 41 | } 42 | } 43 | debug('allowed:') 44 | debug(allowed) 45 | debug('actual:') 46 | debug(actual) 47 | if (combinedOptions.hierarchy) { 48 | let lowestAllowedIndex = 50 49 | let highestActualIndex = -1 50 | for (let actualRole of actual) { 51 | let i = combinedOptions.hierarchy.indexOf(actualRole) 52 | if (i >= 0 && i > highestActualIndex) { 53 | highestActualIndex = i 54 | } 55 | } 56 | for (let allowedRole of allowed) { 57 | let i = combinedOptions.hierarchy.indexOf(allowedRole) 58 | if (i >= 0 && i < lowestAllowedIndex) { 59 | lowestAllowedIndex = i 60 | } 61 | } 62 | debug('highestActualIndex: %s, lowestAllowedIndex: %s', highestActualIndex, lowestAllowedIndex) 63 | return (highestActualIndex >= lowestAllowedIndex) 64 | } 65 | const intersection = _.intersection(allowed, actual) 66 | debug('intersection: %j', intersection) 67 | if (combinedOptions.all) { 68 | return (intersection.length === allowed.length) 69 | } 70 | return (combinedOptions.any && intersection.length > 0) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /example/2 - hierarchy.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* 4 | 5 | Example using a hierarchy of roles. 6 | 7 | Policy is `deny`, by default. 8 | User has one role ('admin'). 9 | User does not explicitly have the role 'user', but because the hierarchy places 'admin' as higher privilege 10 | than 'user' the user will be authorized for /user 11 | User has role required by /admin. 12 | User does not have role required by /superuser. 13 | Endpoint /notsecure has secure: false, thus overriding the default deny policy. 14 | 15 | */ 16 | 17 | const Hapi = require('hapi') 18 | 19 | const server = new Hapi.Server() 20 | server.connection({ 21 | host: 'localhost', 22 | port: 8000 23 | }) 24 | 25 | const plugins = [ 26 | { 27 | register: require('../index'), 28 | options: { 29 | handler: function (request, callback) { 30 | callback(null, {username: 'cread', roles: ['admin']}) 31 | }, 32 | hierarchy: ['user', 'admin', 'superuser'] 33 | } 34 | } 35 | ] 36 | 37 | server.register( 38 | plugins, 39 | function (err) { 40 | if (err) { 41 | throw err 42 | } 43 | 44 | // should return 200 45 | server.route({ 46 | method: 'GET', 47 | path: '/user', 48 | handler: function (request, reply) { 49 | return reply('user') 50 | }, 51 | config: { 52 | plugins: { 53 | hapiAclAuth: { 54 | roles: ['user'] 55 | } 56 | } 57 | } 58 | }) 59 | 60 | // should return 200 61 | server.route({ 62 | method: 'GET', 63 | path: '/admin', 64 | handler: function (request, reply) { 65 | return reply('admin') 66 | }, 67 | config: { 68 | plugins: { 69 | hapiAclAuth: { 70 | roles: ['admin'] 71 | } 72 | } 73 | } 74 | }) 75 | 76 | // should return 403 77 | server.route({ 78 | method: 'GET', 79 | path: '/superuser', 80 | handler: function (request, reply) { 81 | return reply('superuser') 82 | }, 83 | config: { 84 | plugins: { 85 | hapiAclAuth: { 86 | roles: ['superuser'] 87 | } 88 | } 89 | } 90 | }) 91 | 92 | // should return 200 93 | server.route({ 94 | method: 'GET', 95 | path: '/notsecure', 96 | config: { 97 | plugins: { 98 | hapiAclAuth: { 99 | secure: false 100 | } 101 | } 102 | }, 103 | handler: function (request, reply) { 104 | return reply('notsecure') 105 | } 106 | }) 107 | }) 108 | 109 | server.start((err) => { 110 | if (err) { 111 | throw err 112 | } 113 | console.log('Server running at:', server.info.uri) 114 | }) 115 | -------------------------------------------------------------------------------- /example/3 - withHTTPBasicAuth.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* 4 | 5 | Simple example using HTTP Basic for authentication. 6 | In this example we are "getting" the roles for the user in validateFunc, really just specifying them, they will 7 | go into the request.auth.credentials object, which will later be used in the hapi-acl-auth handler. In practice 8 | you would probably get roles from some other service, like a DB, during the authentication process. 9 | 10 | Policy is `deny`, by default. 11 | User has one role ('admin'). 12 | User has role required by /admin. 13 | User does not have role required by /superuser. 14 | Endpoint /notsecure has secure: false, thus overriding the default deny policy. 15 | 16 | */ 17 | 18 | const Hapi = require('hapi') 19 | 20 | const server = new Hapi.Server() 21 | server.connection({ 22 | host: 'localhost', 23 | port: 8000 24 | }) 25 | 26 | const plugins = [ 27 | { 28 | register: require('../index'), 29 | options: { 30 | handler: function (request, callback) { 31 | // request.auth.credentials just happens to be the object passed to callback() in validateFunc 32 | callback(null, request.auth.credentials) 33 | } 34 | } 35 | }, 36 | { 37 | register: require('hapi-auth-basic') 38 | } 39 | ] 40 | 41 | const validateFunc = function (request, username, password, callback) { 42 | callback(null, true, {id: username, name: username, roles: ['admin']}) 43 | } 44 | 45 | server.register( 46 | plugins, 47 | function (err) { 48 | if (err) { 49 | throw err 50 | } 51 | 52 | server.auth.strategy('simple', 'basic', {validateFunc: validateFunc}) 53 | 54 | server.route({ 55 | method: 'GET', 56 | path: '/admin', 57 | handler: function (request, reply) { 58 | return reply('admin') 59 | }, 60 | config: { 61 | auth: 'simple', 62 | plugins: { 63 | hapiAclAuth: { 64 | roles: ['admin'] 65 | } 66 | } 67 | } 68 | }) 69 | 70 | server.route({ 71 | method: 'GET', 72 | path: '/superuser', 73 | handler: function (request, reply) { 74 | return reply('superuser') 75 | }, 76 | config: { 77 | auth: 'simple', 78 | plugins: { 79 | hapiAclAuth: { 80 | roles: ['superuser'] 81 | } 82 | } 83 | } 84 | }) 85 | 86 | server.route({ 87 | method: 'GET', 88 | path: '/notsecure', 89 | config: { 90 | plugins: { 91 | hapiAclAuth: { 92 | secure: false 93 | } 94 | } 95 | }, 96 | handler: function (request, reply) { 97 | return reply('notsecure') 98 | } 99 | }) 100 | }) 101 | 102 | server.start((err) => { 103 | if (err) { 104 | throw err 105 | } 106 | console.log('Server running at:', server.info.uri) 107 | }) 108 | -------------------------------------------------------------------------------- /example/6 - withHapiFormAuthentication.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* 4 | 5 | Simple example using hapi-form-authentication for authentication. 6 | 7 | Policy is `deny`, by default. 8 | User has one role ('admin'). 9 | User has role required by /admin. 10 | User does not have role required by /superuser. 11 | Endpoint /notsecure has secure: false, thus overriding the default deny policy. 12 | 13 | */ 14 | 15 | const Hapi = require('hapi') 16 | 17 | const server = new Hapi.Server() 18 | server.connection({ 19 | host: 'localhost', 20 | port: 8000 21 | }) 22 | 23 | const plugins = [ 24 | { 25 | register: require('hapi-form-authentication'), 26 | options: { 27 | handler: function (username, password, callback) { 28 | // if the password is "password" let them in 29 | const isValid = password === 'password' 30 | // the callback takes two parameters; the first is a simple Boolean 31 | // that indicates whether or not the user is valid, the second is an 32 | // object that must contain, at a minimum, a `username` attribute, 33 | // this object will accessible as `request.auth.credentials` in routes 34 | callback(isValid, {username: username}) 35 | } 36 | } 37 | }, 38 | { 39 | register: require('../index'), 40 | options: { 41 | handler: function (request, callback) { 42 | callback(null, {username: request.auth.credentials.username, roles: ['admin']}) 43 | }, 44 | allowUnauthenticated: true 45 | } 46 | } 47 | ] 48 | 49 | server.register( 50 | plugins, 51 | function (err) { 52 | if (err) { 53 | throw err 54 | } 55 | 56 | server.auth.strategy('form', 'form') 57 | 58 | server.route({ 59 | method: 'GET', 60 | path: '/admin', 61 | handler: function (request, reply) { 62 | return reply('admin') 63 | }, 64 | config: { 65 | auth: 'form', 66 | plugins: { 67 | hapiAclAuth: { 68 | roles: ['admin'] 69 | } 70 | } 71 | } 72 | }) 73 | 74 | server.route({ 75 | method: 'GET', 76 | path: '/superuser', 77 | handler: function (request, reply) { 78 | return reply('superuser') 79 | }, 80 | config: { 81 | auth: 'form', 82 | plugins: { 83 | hapiAclAuth: { 84 | roles: ['superuser'] 85 | } 86 | } 87 | } 88 | }) 89 | 90 | server.route({ 91 | method: 'GET', 92 | path: '/notsecure', 93 | config: { 94 | plugins: { 95 | hapiAclAuth: { 96 | secure: false 97 | } 98 | } 99 | }, 100 | handler: function (request, reply) { 101 | return reply('notsecure') 102 | } 103 | }) 104 | }) 105 | 106 | server.start((err) => { 107 | if (err) { 108 | throw err 109 | } 110 | console.log('Server running at:', server.info.uri) 111 | }) 112 | -------------------------------------------------------------------------------- /example/5 - customForbiddenPage.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* 4 | 5 | Simple example that shows how you can render a custom page when not authorized. 6 | 7 | Policy is `deny`, by default. 8 | User has one role ('admin'). 9 | User has role required by /admin. 10 | User does not have role required by /superuser. 11 | Endpoint /notsecure has secure: false, thus overriding the default deny policy. 12 | 13 | */ 14 | 15 | // allowing Marko template requires 16 | require('marko/node-require') 17 | require('marko/compiler').defaultOptions.writeToDisk = false 18 | 19 | const fs = require('fs') 20 | const path = require('path') 21 | const Hapi = require('hapi') 22 | 23 | const server = new Hapi.Server() 24 | server.connection({ 25 | host: 'localhost', 26 | port: 8000 27 | }) 28 | 29 | const plugins = [ 30 | { 31 | register: require('../index'), 32 | options: { 33 | handler: function (request, callback) { 34 | callback(null, {username: 'cread', firstName: 'Charles', roles: ['user']}) 35 | }, 36 | // used at the plugin options level will make this the default, it can be overridden in the routes, see below 37 | forbiddenPageFunction: function () { 38 | return fs.createReadStream(path.join(__dirname, 'files', 'static403.html')) 39 | } 40 | } 41 | } 42 | ] 43 | 44 | server.register( 45 | plugins, 46 | function (err) { 47 | if (err) { 48 | throw err 49 | } 50 | 51 | server.route({ 52 | method: 'GET', 53 | path: '/admin', 54 | handler: function (request, reply) { 55 | return reply('admin') 56 | }, 57 | config: { 58 | plugins: { 59 | hapiAclAuth: { 60 | roles: ['admin'] 61 | } 62 | } 63 | } 64 | }) 65 | 66 | // here we're just using Marko to render dynamic content, notice that the function assigned to forbiddenPageFunction 67 | // actually accepts a parameter; the object passed to callback(err, object) in the plugin handler also gets passed 68 | // to the forbiddenPageFunction function, {username: 'cread', firstName: 'Charles', roles: ['user']} in this case 69 | server.route({ 70 | method: 'GET', 71 | path: '/superuser', 72 | handler: function (request, reply) { 73 | return reply('superuser') 74 | }, 75 | config: { 76 | plugins: { 77 | hapiAclAuth: { 78 | roles: ['superuser'], 79 | forbiddenPageFunction: function (handlerObject) { 80 | // grab the Marko template 81 | const page = require(path.join(__dirname, 'files', 'template403.marko')) 82 | // pass the object from callback(err, object) to the template so that it can be used in the template 83 | return page.stream(handlerObject) 84 | } 85 | } 86 | } 87 | } 88 | }) 89 | 90 | server.route({ 91 | method: 'GET', 92 | path: '/notsecure', 93 | config: { 94 | plugins: { 95 | hapiAclAuth: { 96 | secure: false 97 | } 98 | } 99 | }, 100 | handler: function (request, reply) { 101 | return reply('notsecure') 102 | } 103 | }) 104 | }) 105 | 106 | server.start((err) => { 107 | if (err) { 108 | throw err 109 | } 110 | console.log('Server running at:', server.info.uri) 111 | }) 112 | -------------------------------------------------------------------------------- /test/test.options.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const chai = require('chai') 4 | 5 | chai.should() 6 | 7 | const path = require('path') 8 | 9 | const options = require(path.join(__dirname, '..', 'lib', 'options.js')) 10 | 11 | describe('options.js', function () { 12 | describe('Joi validation', function () { 13 | it('should throw when nothing is passed', function () { 14 | const req = {} 15 | req.route = { 16 | settings: { 17 | plugins: { 18 | 'hapi-acl-auth': undefined 19 | } 20 | } 21 | } 22 | const pluginOptions = {} 23 | try { 24 | options(req, pluginOptions) 25 | } catch (err) { 26 | err.should.not.be.null 27 | } 28 | }) 29 | it('should not throw when handler and roles are passed in route options', function () { 30 | const req = {} 31 | req.route = { 32 | settings: { 33 | plugins: { 34 | 'hapi-acl-auth': { 35 | handler: function () { 36 | }, 37 | roles: [] 38 | } 39 | } 40 | } 41 | } 42 | const pluginOptions = {} 43 | try { 44 | options(req, pluginOptions) 45 | } catch (err) { 46 | err.should.be.null 47 | } 48 | }) 49 | it('should not throw when handler and roles are passed in plugin options', function () { 50 | const req = {} 51 | req.route = { 52 | settings: { 53 | plugins: {} 54 | } 55 | } 56 | const pluginOptions = { 57 | handler: function () { 58 | }, 59 | roles: [] 60 | } 61 | try { 62 | options(req, pluginOptions) 63 | } catch (err) { 64 | err.should.be.null 65 | } 66 | }) 67 | it('policy should be deny when not specified in plugin options', function () { 68 | const req = {} 69 | req.route = { 70 | settings: { 71 | plugins: {} 72 | } 73 | } 74 | const _pluginOptions = { 75 | handler: function () { 76 | }, 77 | roles: [] 78 | } 79 | const {combinedOptions} = options(req, _pluginOptions) 80 | combinedOptions.policy.should.equal('deny') 81 | }) 82 | it('secure should be false when overridden in route options', function () { 83 | const req = {} 84 | req.route = { 85 | settings: { 86 | plugins: { 87 | 'hapiAclAuth': { 88 | secure: false 89 | } 90 | } 91 | } 92 | } 93 | const _pluginOptions = { 94 | handler: function () { 95 | }, 96 | roles: [] 97 | } 98 | const {combinedOptions} = options(req, _pluginOptions) 99 | combinedOptions.secure.should.equal(false) 100 | }) 101 | }) 102 | describe('general functionality', function () { 103 | it('roles in combinedOptions should match route roles when overridden in route', function () { 104 | const req = {} 105 | req.route = { 106 | settings: { 107 | plugins: { 108 | 'hapiAclAuth': { 109 | roles: ['a', 'b'] 110 | } 111 | } 112 | } 113 | } 114 | const _pluginOptions = { 115 | handler: function () { 116 | }, 117 | roles: ['a'] 118 | } 119 | const {combinedOptions} = options(req, _pluginOptions) 120 | req.route.settings.plugins['hapiAclAuth'].roles.should.eql(combinedOptions.roles) 121 | }) 122 | it('roles in combinedOptions should match plugin roles not when overridden in route', function () { 123 | const req = {} 124 | req.route = { 125 | settings: { 126 | plugins: {} 127 | } 128 | } 129 | const _pluginOptions = { 130 | handler: function () { 131 | }, 132 | roles: ['a'] 133 | } 134 | const {combinedOptions} = options(req, _pluginOptions) 135 | _pluginOptions.roles.should.eql(combinedOptions.roles) 136 | }) 137 | it('roles in plugin options should be able to be a function', function () { 138 | const req = {} 139 | req.route = { 140 | settings: { 141 | plugins: {} 142 | } 143 | } 144 | const _pluginOptions = { 145 | handler: function () { 146 | }, 147 | roles: function () { 148 | return ['a'] 149 | } 150 | } 151 | options(req, _pluginOptions) 152 | }) 153 | it('roles in plugin options and route options should be able to be a function', function () { 154 | const req = {} 155 | req.route = { 156 | settings: { 157 | plugins: { 158 | hapiAclAuth: { 159 | roles: function () { 160 | return ['a'] 161 | } 162 | } 163 | } 164 | } 165 | } 166 | const _pluginOptions = { 167 | handler: function () { 168 | }, 169 | roles: function () { 170 | return ['a'] 171 | } 172 | } 173 | options(req, _pluginOptions) 174 | }) 175 | }) 176 | }) 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![Build Status](https://travis-ci.org/charlesread/hapi-acl-auth.svg?branch=master)](https://travis-ci.org/charlesread/hapi-acl-auth) 3 | 4 | 5 | ---------- 6 | 7 | 8 | **IMPORTANT: Please see [this issue](https://github.com/charlesread/hapi-acl-auth/issues/6).** 9 | 10 | 11 | ---------- 12 | 13 | 14 | 15 | Note: most examples in the examples directory are for previous versions, although the logic will still be the same, they will not work with versions 1.x of the plugin. 16 | 17 | 18 | # hapi-acl-auth 19 | 20 | I didn't like how tightly coupled other `hapi` ACL authorization plugins were to authentication mechanisms, so I wrote my own that doesn't care what authentication mechanism that you use, or even if you use an authentication mechanism at all (although that would be a bit dumb). 21 | 22 | Basically you just tell the plugin what roles a user has, what roles an endpoint allows, and you're set. 23 | 24 | Cool stuff that `hapi-acl-auth` gives you: 25 | 26 | * The ability to lock down the entire application, or just a few routes 27 | * Typical any/all functionality (allow if user has _any_ of these roles, allow if users has _all_ of these roles, for example) 28 | * Specifying a hierarchy of roles ("admins" are clearly "users" too, so let them through without explicitly letting "admins" through, for example) 29 | * The ability to have custom forbidden pages 30 | * Caching of roles for performance 31 | * [And so much more!](https://www.google.com/search?q=but+wait+there%27s+more&tbm=isch&tbo=u&source=univ&sa=X&ved=0ahUKEwiBjtj4tJvVAhXEPD4KHbPyCN4QsAQIJw&biw=1440&bih=776) 32 | 33 | Check out the [example](https://github.com/charlesread/hapi-acl-auth/tree/master/example) directory for examples! 34 | 35 | 36 | 37 | - [Installation](#installation) 38 | - [Utilization](#utilization) 39 | - [Change Log](#change-log) 40 | * [1.0.x](#10x) 41 | - [Plugin/Route Configuration Options](#pluginroute-configuration-options) 42 | * [Plugin _and_ route options](#plugin-_and_-route-options) 43 | * [Plugin _only_ options](#plugin-_only_-options) 44 | * [Route _only_ options](#route-_only_-options) 45 | 46 | 47 | 48 | ## Installation 49 | 50 | ```bash 51 | npm i -S hapi-acl-auth 52 | ``` 53 | 54 | ## Utilization 55 | 56 | ```js 57 | 'use strict' 58 | 59 | /* 60 | 61 | Simple example. 62 | 63 | Policy is `deny`, by default. 64 | User has one role ('admin'). 65 | User has role required by /admin. 66 | User does not have role required by /superuser. 67 | Endpoint /notsecure has secure: false, thus overriding the default deny policy. 68 | 69 | */ 70 | 71 | const Hapi = require('hapi') 72 | 73 | const server = Hapi.server({ 74 | host: 'localhost', 75 | port: 8000 76 | }) 77 | 78 | !async function () { 79 | await server.register({ 80 | plugin: require('hapi-acl-auth'), 81 | options: { 82 | handler: async function () { 83 | return {user: 'cread', roles: ['admin']} 84 | }, 85 | // optional, dy default a simple 403 will be returned when not authorized 86 | forbiddenPageFunction: async function (credentials, request, h) { 87 | // some fancy "logging" 88 | console.log('%s (roles: %s) wanted %s (requires %s) but was not allowed', credentials.user, credentials.roles, request.path, request.route.settings.plugins['hapiAclAuth'].roles) 89 | // some fancy error page 90 | const response = h.response('

Not Authorized!

') 91 | response.code(200) 92 | return response.takeover() 93 | } 94 | } 95 | }) 96 | server.route({ 97 | method: 'get', 98 | path: '/admin', 99 | handler: async function (request, h) { 100 | return '

Welcome to /admin!

' 101 | }, 102 | config: { 103 | plugins: { 104 | hapiAclAuth: { 105 | roles: ['admin'] 106 | } 107 | } 108 | } 109 | }) 110 | server.route({ 111 | method: 'get', 112 | path: '/superuser', 113 | handler: async function (request, h) { 114 | return '

Welcome to /superuser!

' 115 | }, 116 | config: { 117 | plugins: { 118 | hapiAclAuth: { 119 | roles: ['superuser'] 120 | } 121 | } 122 | } 123 | }) 124 | server.route({ 125 | method: 'get', 126 | path: '/notsecure', 127 | handler: async function (request, h) { 128 | return '

Welcome to /notsecure!

' 129 | }, 130 | config: { 131 | plugins: { 132 | hapiAclAuth: { 133 | secure: false 134 | } 135 | } 136 | } 137 | }) 138 | await server.start() 139 | }() 140 | .then(function () { 141 | console.log('server started: %s', server.info.uri) 142 | }) 143 | .catch(function (err) { 144 | console.error(err.message) 145 | }) 146 | ``` 147 | 148 | ## Change Log 149 | 150 | ### 1.0.x 151 | * `hapi-acl-auth` is now fully compatible with Hapi version 17 and above 152 | * All `function`s have been replaced with `async function`s 153 | * If you need Hapi version 16 support see the 0.x releases 154 | 155 | ## Plugin/Route Configuration Options 156 | 157 | Most options can be specified at the plugin level, or for each individual route. In other words, you can "lock down" every route all at once, or at each route, or both (with route config overriding plugin config). 158 | 159 | ### Plugin _and_ route options 160 | 161 | | Name | Type | Default | Allowed Values | Description | 162 | | --- | --- | --- | --- | --- | 163 | | handler (required) | `[async] function(request)` | | | This function must return an object (referred to as `handlerObject` henceforth), the object can be arbitrary, but it must contain a `roles` attribute that is an Array (or a function that returns an array) of roles that are allowed for the route (or routes, if configured in the plugin options).| 164 | | roles (required) | `Array`|`[async] function(handlerObject, request)` | | | An `Array` of roles (or an `[async] function` that returns an array of roles) that are allowed for the route or routes. *NOTE*: this attribute can be set at the plugin or route level, if it is set at the plugin level it will apply to _all_ routes, if set on an individual route it only applies to that route, but you can set a "policy" at the plugin level and then override it in individual routes should you so desire. | 165 | | any | `Boolean` | `true` | `true`|`false` | Apecifies whether a user may possess _any_ of the allowed roles in order to be authorized. | 166 | | all | `Boolean` | `false` | `true`|`false` | Apecifies whether a user _must_ possess _all_ of the allowed routes in order to be authorized. | 167 | | hierarchy | `Array` | | | An `Array` that specifies the privilege hierarchy of roles in order of ascending privilege. For instance, suppose we have `hierarchy: ['user', 'admin', 'superuser]` configured for a route and `roles: ['admin']` configured for that same route. A user with the `superuser` role will be able to access that route because the `superuser` role is of higher privilege than the `admin` role, as specified in the hierarchy. | 168 | | forbiddenPageFunction | `[async] function(handlerObject, request, h)` | | | By default the plugin will respond with a plain `Boom.forbidden()`, so you can use this function to override that behavior and do whatever you want. It is worth noting that if you use this function it is your responsibility to respond to the request. Thus you must return an error (preferably a Boom), a takeover response, or a continue signal.| 169 | | cache | `Boolean` | `false` | `true`|`false` | If caching is enabled the `roles` arrays will be cached, this is helpful if you use resource intensive functions to return roles in the `handler` function or the `roles` attribute | 170 | | allowUnauthenticated | `Boolean` | `false` | `true`|`false` | `hapi-acl-auth` makes use of the `onPostAuth` extension point, basically it does its processing to determine whether or not a user should have access before Hapi responds to a request. If you're using an authentication plugin for Hapi, or anything else really, that performs a redirect in order to authenticate, `hapi-acl-auth` will, depending on the value of `policy`, respond with a 403 before a user _has even been authenticated_. The `allowUnauthenticated` option, when set to `true`, will allow requests where `request.auth.isAuthenticated` is `false` to proceed so that any authentication redirects can occur. | 171 | 172 | ### Plugin _only_ options 173 | 174 | | Name | Type | Default | Allowed Values | Description | 175 | | --- | --- | --- | --- | --- | 176 | | policy | `string` | "deny" | "deny"|"allow" | The policy that the plugin should follow. If "deny" all routes will be secure, if "allow" all routes will be insecure. This can be overridden with the `secure` option in a route. | 177 | 178 | ### Route _only_ options 179 | 180 | 181 | | Name | Type | Default | Allowed Values | Description | 182 | | --- | --- | --- | --- | --- | 183 | | secure | `Boolean` | `true` | `true`|`false` | Indicates whether or not a route should be secure, i.e. if the plugin should be used on a particular route. | 184 | -------------------------------------------------------------------------------- /test/test.authorization.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const assert = require('assert') 4 | const path = require('path') 5 | const type = require('type-detect') 6 | 7 | const cache = require(path.join(__dirname, '..', 'lib', 'cache.js'))() 8 | const authorization = require(path.join(__dirname, '..', 'lib', 'authorization.js')) 9 | 10 | const routeOptions = {} 11 | const callbackObject = {} 12 | const req = {path: '/'} 13 | 14 | describe('authorization.js', function () { 15 | afterEach(function () { 16 | cache.reset() 17 | }) 18 | describe('#determineAuthorization()', function () { 19 | describe('any: true', function () { 20 | it('should return true if several roles are allowed and user has one', function () { 21 | routeOptions.any = true 22 | routeOptions.roles = ['foo', 'bar', 'baz'] 23 | callbackObject.roles = ['foo'] 24 | authorization.determineAuthorization(routeOptions, callbackObject, req) 25 | .then((v) => { 26 | assert.equal(true, v) 27 | }) 28 | }) 29 | it('should return false if several roles are allowed and user has none', function () { 30 | routeOptions.any = true 31 | routeOptions.roles = ['foo', 'bar', 'baz'] 32 | callbackObject.roles = ['bat'] 33 | authorization.determineAuthorization(routeOptions, callbackObject, req) 34 | .then((v) => { 35 | assert.equal(false, v) 36 | }) 37 | }) 38 | }) 39 | describe('all: false', function () { 40 | beforeEach(function () { 41 | cache.reset() 42 | }) 43 | afterEach(function () { 44 | cache.reset() 45 | }) 46 | it('should return true if several roles are allowed and user has one', function () { 47 | routeOptions.all = false 48 | routeOptions.roles = ['foo', 'bar', 'baz'] 49 | callbackObject.roles = ['foo'] 50 | authorization.determineAuthorization(routeOptions, callbackObject, req) 51 | .then((v) => { 52 | assert.equal(true, v) 53 | }) 54 | }) 55 | it('should return false if several roles are allowed and user has none', function () { 56 | routeOptions.all = false 57 | routeOptions.roles = ['foo', 'bar', 'baz'] 58 | callbackObject.roles = ['bat'] 59 | authorization.determineAuthorization(routeOptions, callbackObject, req) 60 | .then((v) => { 61 | assert.equal(false, v) 62 | }) 63 | }) 64 | }) 65 | describe('all: true', function () { 66 | it('should return true if several roles are allowed and user all', function () { 67 | routeOptions.all = true 68 | routeOptions.roles = ['foo', 'bar', 'baz'] 69 | callbackObject.roles = ['foo', 'bar', 'baz'] 70 | authorization.determineAuthorization(routeOptions, callbackObject, req) 71 | .then((v) => { 72 | assert.equal(true, v) 73 | }) 74 | }) 75 | it('should return false if several roles are allowed and user has none', function () { 76 | routeOptions.all = true 77 | routeOptions.roles = ['foo', 'bar', 'baz'] 78 | callbackObject.roles = [] 79 | authorization.determineAuthorization(routeOptions, callbackObject, req) 80 | .then((v) => { 81 | assert.equal(false, v) 82 | }) 83 | }) 84 | it('should return false if several roles are allowed and user has only one', function () { 85 | routeOptions.all = true 86 | routeOptions.roles = ['foo', 'bar', 'baz'] 87 | callbackObject.roles = ['foo'] 88 | authorization.determineAuthorization(routeOptions, callbackObject, req) 89 | .then((v) => { 90 | assert.equal(false, v) 91 | }) 92 | }) 93 | }) 94 | describe('all: true, any: true, all should take precedence', function () { 95 | it('should return true if several roles are allowed and user all', function () { 96 | routeOptions.any = true 97 | routeOptions.all = true 98 | routeOptions.roles = ['foo', 'bar', 'baz'] 99 | callbackObject.roles = ['foo', 'bar', 'baz'] 100 | authorization.determineAuthorization(routeOptions, callbackObject, req) 101 | .then((v) => { 102 | assert.equal(true, v) 103 | }) 104 | }) 105 | it('should return false if several roles are allowed and user has none', function () { 106 | routeOptions.any = true 107 | routeOptions.all = true 108 | routeOptions.roles = ['foo', 'bar', 'baz'] 109 | callbackObject.roles = [] 110 | authorization.determineAuthorization(routeOptions, callbackObject, req) 111 | .then((v) => { 112 | assert.equal(false, v) 113 | }) 114 | }) 115 | it('should return false if several roles are allowed and user has only one', function () { 116 | routeOptions.any = true 117 | routeOptions.all = true 118 | routeOptions.roles = ['foo', 'bar', 'baz'] 119 | callbackObject.roles = ['foo'] 120 | authorization.determineAuthorization(routeOptions, callbackObject, req) 121 | .then((v) => { 122 | assert.equal(false, v) 123 | }) 124 | }) 125 | }) 126 | describe('hierarchy', function () { 127 | it('should return true when actual role is more privileged than least privileged allowed role', function () { 128 | routeOptions.hierarchy = ['reporter', 'user', 'admin', 'superuser'] 129 | routeOptions.roles = ['superuser', 'user'] 130 | callbackObject.roles = ['admin'] 131 | authorization.determineAuthorization(routeOptions, callbackObject, req) 132 | .then((v) => { 133 | assert.equal(true, v) 134 | }) 135 | }) 136 | it('should return true when actual role is equal to an allowed role', function () { 137 | routeOptions.hierarchy = ['reporter', 'user', 'admin', 'superuser'] 138 | routeOptions.roles = ['superuser', 'admin'] 139 | callbackObject.roles = ['admin'] 140 | authorization.determineAuthorization(routeOptions, callbackObject, req) 141 | .then((v) => { 142 | assert.equal(true, v) 143 | }) 144 | }) 145 | it('should return true when actual roles are more privileged than least privileged allowed role', function () { 146 | routeOptions.hierarchy = ['reporter', 'user', 'admin', 'superuser'] 147 | routeOptions.roles = ['reporter', 'user'] 148 | callbackObject.roles = ['admin', 'superuser'] 149 | authorization.determineAuthorization(routeOptions, callbackObject, req) 150 | .then((v) => { 151 | assert.equal(true, v) 152 | }) 153 | }) 154 | it('should return false when actual role is less privileged than least privileged allowed role', function () { 155 | routeOptions.hierarchy = ['reporter', 'user', 'admin', 'superuser'] 156 | routeOptions.roles = ['superuser', 'admin'] 157 | callbackObject.roles = ['user'] 158 | authorization.determineAuthorization(routeOptions, callbackObject, req) 159 | .then((v) => { 160 | assert.equal(false, v) 161 | }) 162 | }) 163 | it('should return false when actual roles are less privileged than least privileged allowed role', function () { 164 | routeOptions.hierarchy = ['reporter', 'user', 'admin', 'superuser'] 165 | routeOptions.roles = ['superuser', 'admin'] 166 | callbackObject.roles = ['user', 'reporter'] 167 | authorization.determineAuthorization(routeOptions, callbackObject, req) 168 | .then((v) => { 169 | assert.equal(false, v) 170 | }) 171 | }) 172 | }) 173 | describe('roles as function', function () { 174 | it('plugin/route roles is function, callback roles is array, user has access', function () { 175 | routeOptions.roles = function (cbo) { 176 | assert(cbo) 177 | return ['reporter', 'user', 'admin', 'superuser'] 178 | } 179 | callbackObject.roles = ['user'] 180 | authorization.determineAuthorization(routeOptions, callbackObject, req) 181 | .then((v) => { 182 | assert.equal(true, v) 183 | }) 184 | }) 185 | it('plugin/route roles is array, callback roles is function, user has access', function () { 186 | routeOptions.roles = ['reporter', 'user', 'admin', 'superuser'] 187 | callbackObject.roles = function (cbo) { 188 | assert(cbo) 189 | return ['user'] 190 | } 191 | authorization.determineAuthorization(routeOptions, callbackObject, req) 192 | .then((v) => { 193 | assert.equal(true, v) 194 | }) 195 | }) 196 | it('plugin/route roles is function, callback roles is array, user does not have access', function () { 197 | routeOptions.roles = function (cbo) { 198 | assert(cbo) 199 | return ['admin', 'superuser'] 200 | } 201 | callbackObject.roles = ['user'] 202 | authorization.determineAuthorization(routeOptions, callbackObject, req) 203 | .then((v) => { 204 | assert.equal(false, v) 205 | }) 206 | }) 207 | it('plugin/route roles is array, callback roles is function, user does not have access', function () { 208 | routeOptions.roles = ['admin', 'superuser'] 209 | callbackObject.roles = function (cbo) { 210 | assert(cbo) 211 | return ['user'] 212 | } 213 | authorization.determineAuthorization(routeOptions, callbackObject, req) 214 | .then((v) => { 215 | assert.equal(false, v) 216 | }) 217 | }) 218 | describe('roles functions return a promise', function () { 219 | it('should work when combinedOptions roles function returns promise', function (done) { 220 | routeOptions.roles = function (cbo) { 221 | assert(cbo) 222 | return new Promise((resolve) => { 223 | setTimeout(function () { 224 | resolve(['user']) 225 | }, 1000) 226 | }) 227 | } 228 | callbackObject.roles = function (cbo) { 229 | assert(cbo) 230 | return ['user'] 231 | } 232 | authorization.determineAuthorization(routeOptions, callbackObject, req) 233 | .then((v) => { 234 | assert.equal(true, v) 235 | done() 236 | }) 237 | }) 238 | it('should work when callbackObject roles function returns promise', function (done) { 239 | routeOptions.roles = function (cbo) { 240 | assert(cbo) 241 | return ['user'] 242 | } 243 | callbackObject.roles = function (cbo) { 244 | assert(cbo) 245 | return new Promise((resolve) => { 246 | setTimeout(function () { 247 | resolve(['user']) 248 | }, 500) 249 | }) 250 | } 251 | authorization.determineAuthorization(routeOptions, callbackObject, req) 252 | .then((v) => { 253 | assert.equal(true, v) 254 | done() 255 | }) 256 | }) 257 | it('should work when both roles functions return promises', function (done) { 258 | routeOptions.roles = function (cbo) { 259 | assert(cbo) 260 | return new Promise((resolve) => { 261 | setTimeout(function () { 262 | resolve(['user']) 263 | }, 500) 264 | }) 265 | } 266 | callbackObject.roles = function (cbo) { 267 | assert(cbo) 268 | return new Promise((resolve) => { 269 | setTimeout(function () { 270 | resolve(['user']) 271 | }, 500) 272 | }) 273 | } 274 | authorization.determineAuthorization(routeOptions, callbackObject, req) 275 | .then((v) => { 276 | assert.equal(true, v) 277 | done() 278 | }) 279 | }) 280 | }) 281 | }) 282 | }) 283 | }) 284 | -------------------------------------------------------------------------------- /test/integration.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const chai = require('chai') 4 | 5 | chai.should() 6 | 7 | const path = require('path') 8 | const Hapi = require('hapi') 9 | const boom = require('boom') 10 | const request = require('request') 11 | 12 | let server 13 | 14 | const plugin = require(path.join(__dirname, '..', 'index.js')) 15 | const cache = require(path.join(__dirname, '..', 'lib', 'cache.js'))() 16 | 17 | const url = 'http://localhost:9999/protected' 18 | const method = 'get' 19 | 20 | describe('integration testing', function () { 21 | 22 | beforeEach(function (done) { 23 | server = Hapi.server({ 24 | host: 'localhost', 25 | port: 9999 26 | }) 27 | done() 28 | }) 29 | afterEach(function (done) { 30 | cache.reset() 31 | server.stop({timeout: 5000}) 32 | .then(function () { 33 | done() 34 | }) 35 | .catch(function (err) { 36 | console.error(err.message) 37 | done() 38 | }) 39 | }) 40 | 41 | it('secure endpoint should return 403 when required route role does not match user role', function (done) { 42 | server.register({ 43 | plugin: plugin, 44 | options: { 45 | handler: async function (req) { 46 | return {username: 'cread', roles: ['USER']} 47 | } 48 | } 49 | }) 50 | .then(function () { 51 | server.route({ 52 | method: 'get', 53 | path: '/protected', 54 | config: { 55 | plugins: { 56 | hapiAclAuth: { 57 | roles: ['ADMIN'], 58 | secure: true 59 | } 60 | } 61 | }, 62 | handler: function (req, h) { 63 | return 'protected' 64 | } 65 | }) 66 | return Promise.resolve() 67 | }) 68 | .then(function () { 69 | return server.start() 70 | }) 71 | .then(function () { 72 | request({url, method}, 73 | function (err, httpResponse, body) { 74 | if (err) throw err 75 | httpResponse.statusCode.should.equal(403) 76 | done() 77 | return true 78 | } 79 | ) 80 | }) 81 | .catch(function (err) { 82 | console.error(err.message) 83 | console.error(err.stack) 84 | if (err) throw err 85 | }) 86 | }) 87 | 88 | it('secure endpoint should return 403 when no required route roles match any user roles', function (done) { 89 | server.register({ 90 | plugin: plugin, 91 | options: { 92 | handler: async function (req) { 93 | return {username: 'cread', roles: ['USER', 'REPORTER']} 94 | } 95 | } 96 | }) 97 | .then(function () { 98 | server.route({ 99 | method: 'get', 100 | path: '/protected', 101 | config: { 102 | plugins: { 103 | hapiAclAuth: { 104 | roles: ['ADMIN'], 105 | secure: true 106 | } 107 | } 108 | }, 109 | handler: function (req, h) { 110 | return 'protected' 111 | } 112 | }) 113 | return Promise.resolve() 114 | }) 115 | .then(function () { 116 | return server.start() 117 | }) 118 | .then(function () { 119 | request({url, method}, 120 | function (err, httpResponse, body) { 121 | if (err) throw err 122 | httpResponse.statusCode.should.equal(403) 123 | done() 124 | return true 125 | } 126 | ) 127 | }) 128 | .catch(function (err) { 129 | console.error(err.message) 130 | console.error(err.stack) 131 | if (err) throw err 132 | }) 133 | }) 134 | 135 | it('secure endpoint should return 200 when A required route role DOES match user role', function (done) { 136 | server.register({ 137 | plugin: plugin, 138 | options: { 139 | handler: async function (req) { 140 | return {username: 'cread', roles: ['USER']} 141 | } 142 | } 143 | }) 144 | .then(function () { 145 | server.route({ 146 | method: 'get', 147 | path: '/protected', 148 | config: { 149 | plugins: { 150 | hapiAclAuth: { 151 | roles: ['USER'], 152 | secure: true 153 | } 154 | } 155 | }, 156 | handler: function (req, h) { 157 | return 'protected' 158 | } 159 | }) 160 | return Promise.resolve() 161 | }) 162 | .then(function () { 163 | return server.start() 164 | }) 165 | .then(function () { 166 | request({url, method}, 167 | function (err, httpResponse, body) { 168 | if (err) throw err 169 | httpResponse.statusCode.should.equal(200) 170 | done() 171 | return true 172 | } 173 | ) 174 | }) 175 | .catch(function (err) { 176 | console.error(err.message) 177 | console.error(err.stack) 178 | if (err) throw err 179 | }) 180 | }) 181 | 182 | it('secure endpoint should return 200 when ANY required route role DOES match a user role', function (done) { 183 | server.register({ 184 | plugin: plugin, 185 | options: { 186 | handler: async function (req) { 187 | return {username: 'cread', roles: ['USER']} 188 | } 189 | } 190 | }) 191 | .then(function () { 192 | server.route({ 193 | method: 'get', 194 | path: '/protected', 195 | config: { 196 | plugins: { 197 | hapiAclAuth: { 198 | roles: ['USER', 'ADMIN'], 199 | secure: true, 200 | any: true //default 201 | } 202 | } 203 | }, 204 | handler: function (req, h) { 205 | return 'protected' 206 | } 207 | }) 208 | return Promise.resolve() 209 | }) 210 | .then(function () { 211 | return server.start() 212 | }) 213 | .then(function () { 214 | request({url, method}, 215 | function (err, httpResponse, body) { 216 | if (err) throw err 217 | httpResponse.statusCode.should.equal(200) 218 | done() 219 | return true 220 | } 221 | ) 222 | }) 223 | .catch(function (err) { 224 | console.error(err.message) 225 | console.error(err.stack) 226 | if (err) throw err 227 | }) 228 | }) 229 | 230 | it('secure endpoint should return 200 when ALL required route roles DO match ALL user roles', function (done) { 231 | server.register({ 232 | plugin: plugin, 233 | options: { 234 | handler: async function (req) { 235 | return {username: 'cread', roles: ['USER', 'pizza']} 236 | } 237 | } 238 | }) 239 | .then(function () { 240 | server.route({ 241 | method: 'get', 242 | path: '/protected', 243 | config: { 244 | plugins: { 245 | hapiAclAuth: { 246 | roles: ['USER', 'pizza'], 247 | secure: true, 248 | all: true 249 | } 250 | } 251 | }, 252 | handler: function (req, h) { 253 | return 'protected' 254 | } 255 | }) 256 | return Promise.resolve() 257 | }) 258 | .then(function () { 259 | return server.start() 260 | }) 261 | .then(function () { 262 | request({url, method}, 263 | function (err, httpResponse, body) { 264 | if (err) throw err 265 | httpResponse.statusCode.should.equal(200) 266 | done() 267 | return true 268 | } 269 | ) 270 | }) 271 | .catch(function (err) { 272 | console.error(err.message) 273 | console.error(err.stack) 274 | if (err) throw err 275 | }) 276 | }) 277 | 278 | it('secure endpoint should return 403 when ALL required route roles DO NOT match ALL user roles', function (done) { 279 | server.register({ 280 | plugin: plugin, 281 | options: { 282 | handler: async function (req) { 283 | return {username: 'cread', roles: ['USER', 'pizza']} 284 | } 285 | } 286 | }) 287 | .then(function () { 288 | server.route({ 289 | method: 'get', 290 | path: '/protected', 291 | config: { 292 | plugins: { 293 | hapiAclAuth: { 294 | roles: ['USER', 'pizza', 'cheese'], 295 | secure: true, 296 | all: true 297 | } 298 | } 299 | }, 300 | handler: function (req, h) { 301 | return 'protected' 302 | } 303 | }) 304 | return Promise.resolve() 305 | }) 306 | .then(function () { 307 | return server.start() 308 | }) 309 | .then(function () { 310 | request({url, method}, 311 | function (err, httpResponse, body) { 312 | if (err) throw err 313 | httpResponse.statusCode.should.equal(403) 314 | done() 315 | return true 316 | } 317 | ) 318 | }) 319 | .catch(function (err) { 320 | console.error(err.message) 321 | console.error(err.stack) 322 | if (err) throw err 323 | }) 324 | }) 325 | 326 | it('insecure endpoint should return 200 when policy is allow', function (done) { 327 | server.register({ 328 | plugin: plugin, 329 | options: { 330 | handler: async function (req) { 331 | return {username: 'cread', roles: ['USER', 'pizza']} 332 | }, 333 | policy: 'allow' 334 | } 335 | }) 336 | .then(function () { 337 | server.route({ 338 | method: 'get', 339 | path: '/insecure', 340 | handler: async function (req, h) { 341 | return 'insecure' 342 | } 343 | }) 344 | return Promise.resolve() 345 | }) 346 | .then(function () { 347 | return server.start() 348 | }) 349 | .then(function () { 350 | request({url: 'http://localhost:9999/insecure', method}, 351 | function (err, httpResponse, body) { 352 | if (err) throw err 353 | httpResponse.statusCode.should.equal(200) 354 | done() 355 | return true 356 | } 357 | ) 358 | }) 359 | .catch(function (err) { 360 | console.error(err.message) 361 | console.error(err.stack) 362 | if (err) throw err 363 | }) 364 | }) 365 | 366 | it('insecure endpoint should return 403 when policy is deny', function (done) { 367 | server.register({ 368 | plugin: plugin, 369 | options: { 370 | handler: async function (req) { 371 | return {username: 'cread', roles: ['USER', 'pizza']} 372 | }, 373 | policy: 'deny' 374 | } 375 | }) 376 | .then(function () { 377 | server.route({ 378 | method: 'get', 379 | path: '/insecure', 380 | handler: async function (req, h) { 381 | return 'insecure' 382 | } 383 | }) 384 | return Promise.resolve() 385 | }) 386 | .then(function () { 387 | return server.start() 388 | }) 389 | .then(function () { 390 | request({url: 'http://localhost:9999/insecure', method}, 391 | function (err, httpResponse, body) { 392 | if (err) throw err 393 | httpResponse.statusCode.should.equal(403) 394 | done() 395 | return true 396 | } 397 | ) 398 | }) 399 | .catch(function (err) { 400 | console.error(err.message) 401 | console.error(err.stack) 402 | if (err) throw err 403 | }) 404 | }) 405 | 406 | it('insecure endpoint should return 200 when policy is deny but route has secure as false', function (done) { 407 | server.register({ 408 | plugin: plugin, 409 | options: { 410 | handler: async function (req) { 411 | return {username: 'cread', roles: ['USER', 'pizza']} 412 | }, 413 | policy: 'deny' 414 | } 415 | }) 416 | .then(function () { 417 | server.route({ 418 | method: 'get', 419 | path: '/insecure', 420 | config: { 421 | plugins: { 422 | hapiAclAuth: { 423 | secure: false 424 | } 425 | } 426 | }, 427 | handler: async function (req, h) { 428 | return 'insecure' 429 | } 430 | }) 431 | return Promise.resolve() 432 | }) 433 | .then(function () { 434 | return server.start() 435 | }) 436 | .then(function () { 437 | request({url: 'http://localhost:9999/insecure', method}, 438 | function (err, httpResponse, body) { 439 | if (err) throw err 440 | httpResponse.statusCode.should.equal(200) 441 | done() 442 | return true 443 | } 444 | ) 445 | }) 446 | .catch(function (err) { 447 | console.error(err.message) 448 | console.error(err.stack) 449 | if (err) throw err 450 | }) 451 | }) 452 | 453 | it('when a hierarchy is used a higher privileged role should be able to access a route with a lower privileged role', function (done) { 454 | server.register({ 455 | plugin: plugin, 456 | options: { 457 | handler: async function (req) { 458 | return {username: 'cread', roles: ['ADMIN']} 459 | }, 460 | hierarchy: ['USER', 'ADMIN', 'SUPERUSER'] 461 | } 462 | }) 463 | .then(function () { 464 | server.route({ 465 | method: 'get', 466 | path: '/protected', 467 | config: { 468 | plugins: { 469 | hapiAclAuth: { 470 | roles: ['USER'] 471 | } 472 | } 473 | }, 474 | handler: async function (req, h) { 475 | return 'insecure' 476 | } 477 | }) 478 | return Promise.resolve() 479 | }) 480 | .then(function () { 481 | return server.start() 482 | }) 483 | .then(function () { 484 | request({url, method}, 485 | function (err, httpResponse, body) { 486 | if (err) throw err 487 | httpResponse.statusCode.should.equal(200) 488 | done() 489 | return true 490 | } 491 | ) 492 | }) 493 | .catch(function (err) { 494 | console.error(err.message) 495 | console.error(err.stack) 496 | if (err) throw err 497 | }) 498 | }) 499 | 500 | it('if policy is set to allow then a route with no config should not be secure, even if other options should deny (if not overridden in route)', function (done) { 501 | server.register({ 502 | plugin: plugin, 503 | options: { 504 | handler: async function (req) { 505 | return {username: 'cread', roles: ['USER']} 506 | }, 507 | hierarchy: ['USER', 'ADMIN', 'SUPERUSER'], 508 | policy: 'allow' 509 | } 510 | }) 511 | .then(function () { 512 | server.route({ 513 | method: 'get', 514 | path: '/protected', 515 | config: { 516 | plugins: { 517 | hapiAclAuth: { 518 | roles: ['ADMIN'], 519 | // if secure: true the test should fail 520 | } 521 | } 522 | }, 523 | handler: async function (req, h) { 524 | return 'insecure' 525 | } 526 | }) 527 | return Promise.resolve() 528 | }) 529 | .then(function () { 530 | return server.start() 531 | }) 532 | .then(function () { 533 | request({url, method}, 534 | function (err, httpResponse, body) { 535 | if (err) throw err 536 | httpResponse.statusCode.should.equal(200) 537 | done() 538 | return true 539 | } 540 | ) 541 | }) 542 | .catch(function (err) { 543 | console.error(err.message) 544 | console.error(err.stack) 545 | if (err) throw err 546 | }) 547 | }) 548 | 549 | 550 | it('when a hierarchy is used a lower privileged role should NOT be able to access a route with a lower privileged role', function (done) { 551 | server.register({ 552 | plugin: plugin, 553 | options: { 554 | handler: async function (req) { 555 | return {username: 'cread', roles: ['USER']} 556 | }, 557 | hierarchy: ['USER', 'ADMIN', 'SUPERUSER'] 558 | } 559 | }) 560 | .then(function () { 561 | server.route({ 562 | method: 'get', 563 | path: '/protected', 564 | config: { 565 | plugins: { 566 | hapiAclAuth: { 567 | roles: ['ADMIN'] 568 | } 569 | } 570 | }, 571 | handler: async function (req, h) { 572 | return 'insecure' 573 | } 574 | }) 575 | return Promise.resolve() 576 | }) 577 | .then(function () { 578 | return server.start() 579 | }) 580 | .then(function () { 581 | request({url, method}, 582 | function (err, httpResponse, body) { 583 | if (err) throw err 584 | httpResponse.statusCode.should.equal(403) 585 | done() 586 | return true 587 | } 588 | ) 589 | }) 590 | .catch(function (err) { 591 | console.error(err.message) 592 | console.error(err.stack) 593 | if (err) throw err 594 | }) 595 | }) 596 | 597 | it('if policy is set to allow then a route with no config should be secure if secure: true is in route config: user does NOT have appropriate role', function (done) { 598 | server.register({ 599 | plugin: plugin, 600 | options: { 601 | handler: async function (req) { 602 | return {username: 'cread', roles: ['USER']} 603 | }, 604 | hierarchy: ['USER', 'ADMIN', 'SUPERUSER'], 605 | policy: 'allow' 606 | } 607 | }) 608 | .then(function () { 609 | server.route({ 610 | method: 'get', 611 | path: '/protected', 612 | config: { 613 | plugins: { 614 | hapiAclAuth: { 615 | roles: ['ADMIN'], 616 | secure: true 617 | } 618 | } 619 | }, 620 | handler: async function (req, h) { 621 | return 'insecure' 622 | } 623 | }) 624 | return Promise.resolve() 625 | }) 626 | .then(function () { 627 | return server.start() 628 | }) 629 | .then(function () { 630 | request({url, method}, 631 | function (err, httpResponse, body) { 632 | if (err) throw err 633 | httpResponse.statusCode.should.equal(403) 634 | done() 635 | return true 636 | } 637 | ) 638 | }) 639 | .catch(function (err) { 640 | console.error(err.message) 641 | console.error(err.stack) 642 | if (err) throw err 643 | }) 644 | }) 645 | 646 | it('if policy is set to allow then a route with no config should be secure if secure: true is in route config: user DOES have appropriate role', function (done) { 647 | server.register({ 648 | plugin: plugin, 649 | options: { 650 | handler: async function (req) { 651 | return {username: 'cread', roles: ['USER']} 652 | }, 653 | hierarchy: ['USER', 'ADMIN', 'SUPERUSER'], 654 | policy: 'allow' 655 | } 656 | }) 657 | .then(function () { 658 | server.route({ 659 | method: 'get', 660 | path: '/protected', 661 | config: { 662 | plugins: { 663 | hapiAclAuth: { 664 | roles: ['SUPERUSER'], 665 | secure: true 666 | } 667 | } 668 | }, 669 | handler: async function (req, h) { 670 | return 'insecure' 671 | } 672 | }) 673 | return Promise.resolve() 674 | }) 675 | .then(function () { 676 | return server.start() 677 | }) 678 | .then(function () { 679 | request({url, method}, 680 | function (err, httpResponse, body) { 681 | if (err) throw err 682 | httpResponse.statusCode.should.equal(403) 683 | done() 684 | return true 685 | } 686 | ) 687 | }) 688 | .catch(function (err) { 689 | console.error(err.message) 690 | console.error(err.stack) 691 | if (err) throw err 692 | }) 693 | }) 694 | 695 | 696 | it('enabling the cache shouldn\'t kill everything', function (done) { 697 | server.register({ 698 | plugin: plugin, 699 | options: { 700 | handler: async function (req) { 701 | return {username: 'cread', roles: ['USER']} 702 | }, 703 | hierarchy: ['USER', 'ADMIN', 'SUPERUSER'], 704 | policy: 'allow', 705 | cache: true 706 | } 707 | }) 708 | .then(function () { 709 | server.route({ 710 | method: 'get', 711 | path: '/protected', 712 | config: { 713 | plugins: { 714 | hapiAclAuth: { 715 | roles: ['SUPERUSER'], 716 | secure: true 717 | } 718 | } 719 | }, 720 | handler: async function (req, h) { 721 | return 'insecure' 722 | } 723 | }) 724 | return Promise.resolve() 725 | }) 726 | .then(function () { 727 | return server.start() 728 | }) 729 | .then(function () { 730 | request({url, method}, 731 | function (err, httpResponse, body) { 732 | if (err) throw err 733 | httpResponse.statusCode.should.equal(403) 734 | done() 735 | return true 736 | } 737 | ) 738 | }) 739 | .catch(function (err) { 740 | console.error(err.message) 741 | console.error(err.stack) 742 | if (err) throw err 743 | }) 744 | }) 745 | 746 | it('if cache is enabled it should contain a cached path object', function (done) { 747 | server.register({ 748 | plugin: plugin, 749 | options: { 750 | handler: async function (req) { 751 | return {username: 'cread', roles: ['USER']} 752 | }, 753 | hierarchy: ['USER', 'ADMIN', 'SUPERUSER'], 754 | policy: 'allow', 755 | cache: true 756 | } 757 | }) 758 | .then(function () { 759 | server.route({ 760 | method: 'get', 761 | path: '/protected', 762 | config: { 763 | plugins: { 764 | hapiAclAuth: { 765 | roles: ['SUPERUSER'], 766 | secure: true 767 | } 768 | } 769 | }, 770 | handler: async function (req, h) { 771 | return 'insecure' 772 | } 773 | }) 774 | return Promise.resolve() 775 | }) 776 | .then(function () { 777 | return server.start() 778 | }) 779 | .then(function () { 780 | request({url, method}, 781 | function (err, httpResponse, body) { 782 | if (err) throw err 783 | httpResponse.statusCode.should.equal(403) 784 | cache.get('/protected').should.have.own.property('allowed') 785 | cache.get('/protected').should.have.own.property('actual') 786 | done() 787 | return true 788 | } 789 | ) 790 | }) 791 | .catch(function (err) { 792 | console.error(err.message) 793 | console.error(err.stack) 794 | if (err) throw err 795 | }) 796 | }) 797 | 798 | it('secure endpoint should return 403 when required route role does not match user role and forbiddenPageFunction returns a 403 Boom', function (done) { 799 | server.register({ 800 | plugin: plugin, 801 | options: { 802 | handler: async function (req) { 803 | return {username: 'cread', roles: ['USER']} 804 | }, 805 | forbiddenPageFunction: async function (credentials, req, h) { 806 | return boom.forbidden() 807 | } 808 | } 809 | }) 810 | .then(function () { 811 | server.route({ 812 | method: 'get', 813 | path: '/protected', 814 | config: { 815 | plugins: { 816 | hapiAclAuth: { 817 | roles: ['ADMIN'], 818 | secure: true 819 | } 820 | } 821 | }, 822 | handler: function (req, h) { 823 | return 'protected' 824 | } 825 | }) 826 | return Promise.resolve() 827 | }) 828 | .then(function () { 829 | return server.start() 830 | }) 831 | .then(function () { 832 | request({url, method}, 833 | function (err, httpResponse, body) { 834 | if (err) throw err 835 | httpResponse.statusCode.should.equal(403) 836 | done() 837 | return true 838 | } 839 | ) 840 | }) 841 | .catch(function (err) { 842 | console.error(err.message) 843 | console.error(err.stack) 844 | if (err) throw err 845 | }) 846 | }) 847 | 848 | it('secure endpoint should return 407 when required route role does not match user role and forbiddenPageFunction returns a 407 response', function (done) { 849 | server.register({ 850 | plugin: plugin, 851 | options: { 852 | handler: async function (req) { 853 | return {username: 'cread', roles: ['USER']} 854 | }, 855 | forbiddenPageFunction: async function (credentials, req, h) { 856 | const response = h.response() 857 | response.code(407) 858 | return response.takeover() 859 | } 860 | } 861 | }) 862 | .then(function () { 863 | server.route({ 864 | method: 'get', 865 | path: '/protected', 866 | config: { 867 | plugins: { 868 | hapiAclAuth: { 869 | roles: ['ADMIN'], 870 | secure: true 871 | } 872 | } 873 | }, 874 | handler: function (req, h) { 875 | return 'protected' 876 | } 877 | }) 878 | return Promise.resolve() 879 | }) 880 | .then(function () { 881 | return server.start() 882 | }) 883 | .then(function () { 884 | request({url, method}, 885 | function (err, httpResponse, body) { 886 | if (err) throw err 887 | httpResponse.statusCode.should.equal(407) 888 | done() 889 | return true 890 | } 891 | ) 892 | }) 893 | .catch(function (err) { 894 | console.error(err.message) 895 | console.error(err.stack) 896 | if (err) throw err 897 | }) 898 | }) 899 | 900 | }) --------------------------------------------------------------------------------