Search code examples
javascriptnode.jsexpressacl

ACL - Implement acl together with passport


I am trying out node_acl with passport-local. When I run my code I cannot secure the route for the admin-user '/admin' and I am redirected to the /login page.

Find below my minimum runnable example:

require('dotenv').config()
const express = require('express')
// const fs = require('fs')
const path = require('path')
const logger = require('morgan')
const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser')
const session = require('express-session')
const passport = require('passport')
const LocalStrategy = require('passport-local').Strategy
const ACL = require('acl')

// load user.json file
// const d = fs.readFileSync(path.join(__dirname, '/../data/user.json'))
// const userObj = JSON.parse(d)
const userObj = [{
  id: 1,
  username: 'admin',
  password: 'admin',
  email: '[email protected]',
  role: 'admin',
},
{
  id: 2,
  username: 'user',
  password: 'user',
  email: '[email protected]',
  role: 'user',
},
]

const app = express()

// view engine setup
app.set('views', path.join(__dirname, 'views'))
app.set('view engine', 'pug')
app.use(logger(process.env.LOG_ENV))
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({
  extended: false,
}))
app.use(express.static(path.join(__dirname, '/../public')))
app.use(cookieParser())

app.use(session({
  secret: 'super-mega-hyper-secret',
  resave: false,
  saveUninitialized: false,
}))

/**
 * Passport Local
 */
app.use(passport.initialize())
app.use(passport.session())

function authenticate() {
  passport.serializeUser((user, done) => {
    done(null, user.id)
  })

  passport.deserializeUser(async(id, done) => {
    //        const user = await serviceAuth.findById(id)
    const user = userObj.find(item => item.id === id)
    done(null, user)
  })

  // Sign in with username and Password
  passport.use('local', new LocalStrategy({
    usernameField: 'username',
  }, async(username, password, done) => {
    const user = userObj.find(item => item.username === username)
    done(null, user)
  }))
}

const isAuthenticated = (req, res, next) => {
  if (req.isAuthenticated()) {
    res.locals.user = req.session.user
    return next()
  }
  res.redirect('login')
}
authenticate()

/**
 * Node ACL
 */

function accessControl() {
  const nodeAcl = new ACL(new ACL.memoryBackend())

  nodeAcl.allow([{
    roles: 'admin',
    allows: [{
      resources: '/admin',
      permissions: '*',
    }],
  }, {
    roles: 'user',
    allows: [{
      resources: '/dashboard',
      permissions: 'get',
    }],
  }, {
    roles: 'guest',
    allows: [],
  }])

  // Inherit roles
  //  Every user is allowed to do what guests do
  //  Every admin is allowed to do what users do
  nodeAcl.addRoleParents('user', 'guest')
  nodeAcl.addRoleParents('admin', 'user')

  nodeAcl.addUserRoles(1, 'admin')
  nodeAcl.addUserRoles(2, 'user')
  nodeAcl.addUserRoles(0, 'guest')

  return nodeAcl
}

/*
function checkPermission(resource, action) {
  const access = accessControl()

  return (req, res, next) => {
    const uid = req.session.user.id
    access.isAllowed(uid, resource, action, (err, result) => {
      if (result) {
        next()
      } else {
        const checkError = new Error('User does not have permission to perform this action on this resource')
        next(checkError)
      }
    })
  }
} */

const getCurrentUserId = (req) => {
  console.log(req)
  req.user && req.user.id.toString() || false
}

const access = accessControl()

// Routes
app.get('/login', (req, res) => {
  res.render('login')
})

app.post('/login', (req, res, next) => {
  passport.authenticate('local', (err, user) => {
    if (err) return next(err)
    if (!user) {
      return res.status(401).json({
        error: 'Email or password is incorrect.',
      })
    }

    return res.render('dashboard')
  })(req, res, next)
})

app.get('/dashboard', [isAuthenticated, access.middleware()], (req, res) => {
  res.render('dashboard')
})

app.get('/admin', [isAuthenticated, access.middleware()], (req, res) => {
  res.render('admin')
})

app.get('/status', (request, response) => {
  access.userRoles(getCurrentUserId(request), (error, roles) => {
    response.send(`User: ${JSON.stringify(request.user)} Roles: ${JSON.stringify(roles)}`)
  })
})

// Start Server
const port = process.env.APP_PORT || 8080
const host = process.env.APP_URL || 'localhost'

app.listen(port, host, () => {
  console.log(`Listening on ${host}:${port}`)
})

module.exports = app

Any suggestions why the route, /admin cannot be called only be the admin user?

Thank you in advance for your replies!


Solution

  • I couldn't run your "runnable" code so i changed it a bit to check it out. So after some tests it seems that it works just fine. Can you check it too?

    Using POSTMAN I did a POST on /login?username=user&password=user

    After that I did a GET on `/status' and I got

    User: {"id":2,"username":"user","password":"user","email":"[email protected]","role":"user"} Roles: []
    

    Then I did a GET on /admin and i got

    <!DOCTYPE html>
    <html lang="en">
        <head>
            <meta charset="utf-8">
            <title>Error</title>
        </head>
        <body>
            <pre>HttpError: Insufficient permissions to access resource
                <br> &nbsp; &nbsp;at C:\Users\stamoulis.zamanis\Desktop\aclTest\node_modules\acl\lib\acl.js:705:14
                <br> &nbsp; &nbsp;at tryCatcher (C:\Users\stamoulis.zamanis\Desktop\aclTest\node_modules\bluebird\js\release\util.js:16:23)
                <br> &nbsp; &nbsp;at Promise.successAdapter [as _fulfillmentHandler0] (C:\Users\stamoulis.zamanis\Desktop\aclTest\node_modules\bluebird\js\release\nodeify.js:23:30)
                <br> &nbsp; &nbsp;at Promise._settlePromise (C:\Users\stamoulis.zamanis\Desktop\aclTest\node_modules\bluebird\js\release\promise.js:566:21)
                <br> &nbsp; &nbsp;at Promise._settlePromise0 (C:\Users\stamoulis.zamanis\Desktop\aclTest\node_modules\bluebird\js\release\promise.js:614:10)
                <br> &nbsp; &nbsp;at Promise._settlePromises (C:\Users\stamoulis.zamanis\Desktop\aclTest\node_modules\bluebird\js\release\promise.js:693:18)
                <br> &nbsp; &nbsp;at Async._drainQueue (C:\Users\stamoulis.zamanis\Desktop\aclTest\node_modules\bluebird\js\release\async.js:133:16)
                <br> &nbsp; &nbsp;at Async._drainQueues (C:\Users\stamoulis.zamanis\Desktop\aclTest\node_modules\bluebird\js\release\async.js:143:10)
                <br> &nbsp; &nbsp;at Immediate.Async.drainQueues (C:\Users\stamoulis.zamanis\Desktop\aclTest\node_modules\bluebird\js\release\async.js:17:14)
                <br> &nbsp; &nbsp;at runCallback (timers.js:789:20)
                <br> &nbsp; &nbsp;at tryOnImmediate (timers.js:751:5)
                <br> &nbsp; &nbsp;at processImmediate [as _immediateCallback] (timers.js:722:5)
            </pre>
        </body>
    </html>
    

    Then I logged in as admin and when I called /admin again i got admin

    I had to change app.post('/login'). If i didnt do req.login then passport.serializeUser was never called, the cookie wasn't correct resulting in bad session.

    app.post('/login', (req, res, next) => {
      passport.authenticate('local', (err, user) => {
        if (err) return next(err)
        if (!user) {
          return res.status(401).json({
            error: 'Email or password is incorrect.',
          })
        }
         req.logIn(user, function (err) { // <-- Log user in
           next();
        });
    
    
      })(req, res, next)
    },function(req,res){
     res.send('dashboard')
    })
    

    All code:

    require('dotenv').config()
    const express = require('express')
    // const fs = require('fs')
    const path = require('path')
    const logger = require('morgan')
    const bodyParser = require('body-parser')
    const cookieParser = require('cookie-parser')
    const session = require('express-session')
    const passport = require('passport')
    const LocalStrategy = require('passport-local').Strategy
    const ACL = require('acl')
    
    // load user.json file
    // const d = fs.readFileSync(path.join(__dirname, '/../data/user.json'))
    // const userObj = JSON.parse(d)
    const userObj = [{
      id: 1,
      username: 'admin',
      password: 'admin',
      email: '[email protected]',
      role: 'admin',
    },
    {
      id: 2,
      username: 'user',
      password: 'user',
      email: '[email protected]',
      role: 'user',
    },
    ]
    
    const app = express()
    
    // view engine setup
    app.set('views', path.join(__dirname, 'views'))
    app.set('view engine', 'pug')
    app.use(logger(process.env.LOG_ENV))
    app.use(bodyParser.json())
    app.use(bodyParser.urlencoded({
      extended: false,
    }))
    app.use(express.static(path.join(__dirname, '/../public')))
    app.use(cookieParser())
    
    app.use(session({
      secret: 'super-mega-hyper-secret',
      resave: false,
      saveUninitialized: false,
    }))
    
    
    
    function authenticate() {
      passport.serializeUser((user, done) => {
        done(null, user.id)
      })
    
      passport.deserializeUser((id, done) => {
        //        const user = await serviceAuth.findById(id)
        const user = userObj.find(item => item.id === id)
        done(null, user)
      })
    
      // Sign in with username and Password
      passport.use('local', new LocalStrategy({
        usernameField : 'username',
            passwordField : 'password'
      }, async(username, password, done) => {
        const user = userObj.find(item => item.username === username)
        done(null, user)
      }))
    }
    
    const isAuthenticated = (req, res, next) => {
      if (req.isAuthenticated()) {
        res.locals.user = req.session.user
        return next()
      }
      res.redirect('login')
    }
    authenticate()
    
    
    /**
     * Passport Local
     */
    app.use(passport.initialize())
    app.use(passport.session())
    
    /**
     * Node ACL
     */
    
    function accessControl() {
      const nodeAcl = new ACL(new ACL.memoryBackend())
    
      nodeAcl.allow([{
        roles: 'admin',
        allows: [{
          resources: '/admin',
          permissions: '*',
        }],
      }, {
        roles: 'user',
        allows: [{
          resources: '/dashboard',
          permissions: 'get',
        }],
      }, {
        roles: 'guest',
        allows: [],
      }])
    
      // Inherit roles
      //  Every user is allowed to do what guests do
      //  Every admin is allowed to do what users do
      nodeAcl.addRoleParents('user', 'guest')
      nodeAcl.addRoleParents('admin', 'user')
    
      nodeAcl.addUserRoles(1, 'admin')
      nodeAcl.addUserRoles(2, 'user')
      nodeAcl.addUserRoles(0, 'guest')
    
      return nodeAcl
    }
    
    /*
    function checkPermission(resource, action) {
      const access = accessControl()
    
      return (req, res, next) => {
        const uid = req.session.user.id
        access.isAllowed(uid, resource, action, (err, result) => {
          if (result) {
            next()
          } else {
            const checkError = new Error('User does not have permission to perform this action on this resource')
            next(checkError)
          }
        })
      }
    } */
    
    const getCurrentUserId = (req) => {
      console.log(req)
      req.user && req.user.id.toString() || false
    }
    
    const access = accessControl()
    
    // Routes
    app.get('/login', (req, res) => {
      res.send('login')
    })
    
    app.post('/login', (req, res, next) => {
      passport.authenticate('local', (err, user) => {
        if (err) return next(err)
        if (!user) {
          return res.status(401).json({
            error: 'Email or password is incorrect.',
          })
        }
         req.logIn(user, function (err) { // <-- Log user in
           next();
        });
    
    
      })(req, res, next)
    },function(req,res){
     res.send('dashboard')
    })
    
    app.get('/dashboard', [isAuthenticated, access.middleware()], (req, res) => {
      res.send('dashboard')
    })
    
    app.get('/admin', [isAuthenticated, access.middleware()], (req, res) => {
      res.send('admin')
    })
    
    app.get('/status', (request, response) => {
      access.userRoles(getCurrentUserId(request), (error, roles) => {
        response.send(`User: ${JSON.stringify(request.user)} Roles: ${JSON.stringify(roles)}`)
      })
    })
    
    // Start Server
    const port = process.env.APP_PORT || 3335
    const host = process.env.APP_URL || 'localhost'
    
    app.listen(port, host, () => {
      console.log(`Listening on ${host}:${port}`)
    })
    
    module.exports = app