ACL - Implement acl together with passport
Asked Answered
D

1

8

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!

Designate answered 24/11, 2017 at 9:26 Comment(1)
what does your /status route returns to you when you are logged in with user. After some test i got HttpError: Insufficient permissions to access resource when i tried to hit /admin logged in as userInstrument
I
5

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
Instrument answered 30/11, 2017 at 10:58 Comment(4)
Thx for your reply! One more question, why have you added the setTimout function in the accessControl function?Designate
Furthermore, why have you added: ` req.logIn(user, (err) => { // <-- Log user in next() }), shouldnt the user be already logged in via passport? I appreciate your reply!Designate
Ignore setTimeout, I wanted to check if users were in the correct groups. I dont know 100% how acl works so I waited a bitInstrument
I dont have your render model and etc so instead of render i did send the name of the view. If i didnt do req.login then passport.serializeUser was never called and the cookie wasnt correct resulting in bad session.Instrument

© 2022 - 2024 — McMap. All rights reserved.