Skip to content
This repository has been archived by the owner on Apr 5, 2023. It is now read-only.

Commit

Permalink
handling roles (v2.0.0)
Browse files Browse the repository at this point in the history
  • Loading branch information
max-lt committed Jan 25, 2018
1 parent d6ebc56 commit 6c4f834
Show file tree
Hide file tree
Showing 17 changed files with 642 additions and 629 deletions.
3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
language: node_js
node_js:
- "6"
- "7"
- "8"
- "9"
- "node"
after_script: "npm run test-cov"
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ Note that the "_**userLogin**_" parameter must **match** the **expected basic au
token: {
filter* : function(user) or var, // Data to put in the token.user attribute
// (default is the whole user param without the pass attribute)
// must return a "role" attribute in order to be compared with the
// auth.hasRole(...) method.
decode* : function(token) or var, // Data to put in the req.user attribute
// (default is the token.user data)
exp : function(user) or var,
Expand All @@ -37,11 +39,13 @@ Note that the "_**userLogin**_" parameter must **match** the **expected basic au
session: {
filter* : function(user), // Data to put in the req.user attribute
// (default is the whole user param without the pass attribute)
// must return a "role" attribute in order to be compared with the
// auth.hasRole(...) method.
},
password: {
compare*: function(user, pass):boolean // function used to compare
// the user password (user.pass) and the provided credential (pass).
// Default is (user.pass == pass)
// Default is "user.pass == pass"
},
unauthorized: function(error, req, res, next), // method )
login: {
Expand All @@ -64,12 +68,13 @@ your-auth-config.js
function userGetter(userLogin) {
return {
email: userLogin,
pass: 'password'
pass: 'password',
roles: ['user']
}
}
// OR //
function userGetter(userLogin) {
return Promise.resolve({email: userLogin, pass: 'password'});
return Promise.resolve({email: userLogin, pass: 'password', roles: ['user']});
}

const app = require('express')();
Expand All @@ -86,7 +91,7 @@ express entry point
```js
/// require ... ///
const auth = require('./your-auth-config');
app.use(auth.core);
app.use(auth.default);

const routeA = require('./routes/routeA');
const routeB = require('./routes/routeB');
Expand All @@ -97,9 +102,14 @@ app.get('/userinfo', auth.user, yourFunction);
app.use('/a', routeA);
app.use('/b', auth.user, routeB);
app.use('/c', auth.admin, routeC);
app.use('/d', auth.hasRole('custom'), routeD);

app.use(auth.unauthorized); // catch errors that are instance of AuthenticationError

```
Note that `auth.user` and `auth.admin` are just aliases of `auth.hasRole('user')` and `auth.hasRole('admin')`.
RouteA.js
```js
/// require ... ///
Expand Down
216 changes: 1 addition & 215 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,215 +1 @@
"use strict";

const basicAuth = require('basic-auth');
const tokenAuth = require('./bearer-auth');
const jwt = require('jsonwebtoken');
const parseUrl = require('parseurl');
const {promiseOrVar, funcOrVar} = require('./util');

class AuthenticationError extends Error {
constructor(message) {
if (message instanceof Error) {
super(message.message);
Object.assign(this, message);
}
else super(message);
this.name = 'AuthenticationError';
}
}

/**
* @param secret
* @param userGetter
* @param options
* @return {{any: anyLevel, user: userLevel, admin: adminLevel, core: [logout, authBasic, login, authJWT, anyLevel]}}
*/
module.exports = (secret, userGetter, options) => {
if (!secret) throw new Error("No secret set");
if (!userGetter) throw new Error("No userGetter set");
const opts = Object.assign({token: {}, login: {}, password: {}, session: {}}, options);

function pack(req, res, next) {
if (req._user) promiseOrVar(funcOrVar(opts.session.filter || defaultFilter, req._user))
.then((user) => {
req.user = user;
delete req._user;
next();
});
else next();
}

/** @type {Promise<string|buffer>} */
let secretPromise = promiseOrVar(secret);

/** @private */
function unauthorized(error, req, res, next) {

if (!(error instanceof AuthenticationError))
return next(error);

if (opts.unauthorized) {
return opts.unauthorized(error, req, res, next);
} else {
res.set('WWW-Authenticate', 'Basic realm="Authorization Required"');
res.status(401);
return res.send({message: error.message || 'Unauthorized'});
}
}

function defaultFilter(user) {
let _u = Object.assign(user);
delete _u.pass; //hiding user pass in response
return _u;
}

function defaultTokenTransform(token) {
return token.user || token.iss || null;
}

function defaultPassCompare(user, pass) {
return user.pass === pass;
}

function authBasic(req, res, next) {

//if previous auth succeed
if (req.authenticated) return next();

/** @type {{name:string, pass:string}} */
let basic = basicAuth(req);

//if basicAuth attempted
if (basic && basic.name && basic.pass) {
promiseOrVar(userGetter(basic.name))
.then((user) => {
if (user) {
return promiseOrVar(funcOrVar(opts.password.compare || defaultPassCompare, user, basic.pass))
.then((validated) => {
if (validated) {
req.authenticated = true;
req._user = user;
return next();
}
else throw new AuthenticationError('Bad user or Password');
})
}
else throw new AuthenticationError('No user match')
})
.catch(next);
}
else return next();
}

function logout(req, res, next) {

/**@type {{pathname:string}} */
let url = parseUrl(req);

if (url.pathname !== '/logout') {
return next();
} else {
req.authenticated = false;
return res.send({message: 'goodbye'});
}
}

function anyLevel(req, res, next) {
if (!req.authenticated) {
req.authenticated = false;
req.user = {name: 'anonymous'};
}
next();
}

function userLevel(req, res, next) {
if (!req.authenticated) next(new AuthenticationError());
else next();
}

function adminLevel(req, res, next) {
if (!req.authenticated || !req.user.admin) next(new AuthenticationError());
else next();
}

/**
* Return jwt token if matches /login (POST)
*/
function login(req, res, next) {

let url = parseUrl(req);

if (url.pathname == (opts.login.path || '/login')
&& req.method == (opts.login.method || 'POST')) {
secretPromise
.then((secret) => {
if (!secret) throw new Error("No secret set");
if (req.authenticated) {
return promiseOrVar(funcOrVar(opts.token.filter || defaultFilter, req._user))
.then((user) => new Promise((resolve, reject) => jwt.sign(Object.assign(
{},
{user},
{
exp: funcOrVar(opts.token.exp, req._user), // expiration date
iss: funcOrVar(opts.token.iss, req._user),
sub: funcOrVar(opts.token.sub, req._user), // user id
aud: funcOrVar(opts.token.aud, req._user) //client id
}),
secret, {}, (err, token) => {
if (err) {
err.type = 'jwt';
reject(err);
}
else resolve({user, token});
})
))
}
else throw new AuthenticationError('Bad user or Password');
})
.then((tokenAndUser) => res.json(tokenAndUser))
.catch(next);
}
else return next();

}

function authJWT(req, res, next) {
//if previous auth succeed
if (req.authenticated) return next();

let token = tokenAuth(req);

//else if JWT auth attempted
if (token) secretPromise
.then((secret) => {
if (!secret) throw new Error("No secret set");
// for errors see:
// https://github.com/auth0/node-jsonwebtoken#jsonwebtokenerror
// https://github.com/auth0/node-jsonwebtoken#tokenexpirederror
return new Promise((resolve, reject) => {
jwt.verify(token, secret, (err, decoded) => {
if (err) reject(err);
else resolve(decoded)
});
})
})
.catch((err) => {
throw new AuthenticationError(err || 'jwt error');
})
.then((decoded) => promiseOrVar(funcOrVar(opts.token.decode || defaultTokenTransform, decoded)))
.then((user) => {
req.authenticated = true;
req.user = user;
next();
})
.catch(next);
else next();
}

return {
any: anyLevel,
user: userLevel,
admin: adminLevel,
unauthorized: unauthorized,
core: [logout, authBasic, login, pack, authJWT, anyLevel]
};
};
module.exports = require('./lib');
File renamed without changes.
12 changes: 12 additions & 0 deletions lib/error/AuthenticationError.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class AuthenticationError extends Error {
constructor(message) {
if (message instanceof Error) {
super(message.message);
Object.assign(this, message);
}
else super(message);
this.name = 'AuthenticationError';
}
}

module.exports = AuthenticationError;
60 changes: 60 additions & 0 deletions lib/filters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
const AuthenticationError = require('./error/AuthenticationError');
const parseUrl = require('parseurl');

function logout(req, res, next) {

/**@type {{pathname:string}} */
let url = parseUrl(req);

if (url.pathname !== '/logout') {
return next();
} else {
req.authenticated = false;
return res.send({message: 'goodbye'});
}
}

function anyLevel(req, res, next) {
if (!req.authenticated) {
req.authenticated = false;
req.user = {name: 'anonymous'};
}
next();
}

function hasRole(role) {

if (typeof role !== 'string')
throw new Error('role must be a string');

return function (req, res, next) {

if (!req.authenticated
|| !req.user
|| !req.user.roles
|| !req.user.roles.includes(role))
return next(new AuthenticationError());

next();
}
}

function userLevel(req, res, next) {
return hasRole('user')(req, res, next)
}

function adminLevel(req, res, next) {
return hasRole('admin')(req, res, next)
}

function unauthorized(error, req, res, next) {

if (!(error instanceof AuthenticationError))
return next(error);

res.set('WWW-Authenticate', 'Basic realm="Authorization Required"');
res.status(401);
return res.send({message: error.message || 'Unauthorized'});
}

module.exports = {logout, anyLevel, userLevel, adminLevel, unauthorized, hasRole};

0 comments on commit 6c4f834

Please sign in to comment.