Skip to content

Commit

Permalink
Merge branch 'release/1.0.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
adalcinojunior committed Aug 24, 2019
2 parents 8cb42a5 + fc294d4 commit e6b49e1
Show file tree
Hide file tree
Showing 16 changed files with 1,213 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,6 @@ typings/

# next.js build output
.next

#package-lock.json
package-lock.json
9 changes: 9 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

language: node_js
node_js:
- "10"
- "11"
- "12"
cache:
directories:
- node_modules
144 changes: 143 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,145 @@
# simple-express-authorization
# Simple Express Authorization -
Scopes based authorization middleware.
Ideal for app express or derivative such as express-gateway

[![node](https://img.shields.io/badge/node-v10.16.2-red.svg?style=?style=flat-square&logo=node.js)](https://nodejs.org/)
[![npm](https://img.shields.io/badge/npm-v6.10.3-red.svg?style=flat-square&logo=npm)](https://nodejs.org/)
[![mocha](https://img.shields.io/badge/mocha-v6.2.0-brown.svg?style=flat-square&logo=mocha)](https://www.npmjs.com/package/mocha)
[![chai](https://img.shields.io/badge/chai-v4.2.0-orange.svg?style=flat-square&logo=chai)](https://www.npmjs.com/package/chai)
[![supertest](https://img.shields.io/badge/supertest-v4.2.0-green.svg?style=flat-square&logo=supertest)](https://www.npmjs.com/package/supertest)
[![nyc](https://img.shields.io/badge/nyc-v14.1.1-blue.svg?style=flat-square&logo=nyc)](https://www.npmjs.com/package/nyc)
[![Coverage Status](https://coveralls.io/repos/github/adalcinojunior/simple-express-authorization/badge.svg?branch=develop)](https://coveralls.io/github/adalcinojunior/simple-express-authorization?branch=develop)
[![Build Status](https://travis-ci.com/adalcinojunior/simple-express-authorization.svg?branch=develop)](https://travis-ci.com/adalcinojunior/simple-express-authorization.svg?branch=develop)


## Installation

$ npm i simple-express-authorization

## Usage the simple-express-authorization
### When there is a single setting
```javascript
const app = require('express')
const guard = require('simple-express-authorization')

const settings = {
responseCaseError: {
code: 403,
message: "FORBIDDEN",
description: "Authorization failed due to insufficient permissions.",
redirect_link: "/auth"
},
logicalStrategy: 'AND',
flowStrategy: "NEXTWITHERROR"
};

guard.config(settings)

app.get('/users', guard.check(['users:read', 'users:readAll']), () => {
return [];
}))

app.get('/users/:userId', guard.check(['users:read']), () => {
return {};
}))
...
```
### When there are local settings
```javascript
const app = require('express')
const guard = require('simple-express-authorization')

const settingsGetAll = {
responseCaseError: {
code: 403,
message: "FORBIDDEN",
description: "Authorization failed due to insufficient permissions.",
redirect_link: "/auth"
},
logicalStrategy: 'AND',
flowStrategy: "NEXTWITHERROR"
};

const settingsGet = {
responseCaseError: {
code: 403,
message: "FORBIDDEN",
description: "Authorization failed due to insufficient permissions.",
redirect_link: "/auth"
},
logicalStrategy: 'AND',
flowStrategy: "RETURNRESPONSE"
};

guard.config(options)

app.get('/users', guard.check(['users:read', 'users:readAll'],settingsGetAll), () => {
return [];
}))

app.get('/users/:userId', guard.check(['users:read'],settingsGet), () => {
return {};
}))
...
```
### Possibles settings
```javascript
settings = {
/** Specific where we find user scopes
* By default we use -> req.user.scope
* Observation:
* - userScopesLocation is a string
* - req.user.scope is expected to be of type Array.
*
* When informed "a.b.c" we use -> req['a']['b']['c']
*/
userScopesLocation: "DEFAULT",

/** Specifies the logical strategy used to evaluate user scopes
* By default we use -> OR
* Observation:
* - logicalStrategy is a string
* - We currently only support "OR" and "AND".
*/
logicalStrategy: "OR",

/** Specifies the return object if the user does not have the expected scopes.
* responseCaseError is the content returned in the response body when flowStrategy
* is not modified, or when it is set to the default value "RETURNRESPONSE"
*/
responseCaseError: {
code: 403,
message: "FORBIDDEN",
description: "Authorization failed due to insufficient permissions.",
redirect_link: "/auth"
},

/** Specifies the flow strategy used when the user does not have the expected scopes
* By default we use -> RETURNRESPONSE
* Observation:
* - flowStrategy is a string
* - "RETURNRESPONSE"-> When the user does not have the required scopes,
* the object responseCaseError is returned.
* - "NEXTWITHERROR"-> When the user does not have the required scopes,
* the next() function is called passing the responseCaseError object.
* - We currently only support "RETURNRESPONSE" and "NEXTWITHERROR".
*/
flowStrategy: "RETURNRESPONSE"
}
```

## Running tests

### Unitary tests

Run `npm run test:unit` to execute the unit tests.

### Integration tests

Run `npm run test:integration` to execute the integration tests.

### Coverage tests

Run `npm run test:coverage` to execute the coverage tests.


21 changes: 21 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
declare var simpleAuthorization: simpleAuthorization.SimpleAuthorization
export = simpleAuthorization

declare namespace simpleAuthorization {

export interface SimpleAuthorization {
(options?: IOptions): any

config(options?: IOptions): any

check(expectedScopes: Array<string>, options?: IOptions): any

}

export interface IOptions {
userScopesLocation?: string
logicalStrategy?: string
responseCaseError?: object
flowStrategy?: string
}
}
11 changes: 11 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use strict'

const simpleAuthorization = require('./lib/simple.authorization')

module.exports.config = function (configurations) {
return simpleAuthorization.config(configurations)
}

module.exports.check = function (expectedScopes, localConfigurations) {
return simpleAuthorization.check(expectedScopes, localConfigurations)
}
46 changes: 46 additions & 0 deletions lib/config/default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const USERSCOPESLOCATION = require('./enums').USERSCOPESLOCATION
const LOGICALSTRATEGY = require('./enums').LOGICALSTRATEGY
const FLOWSTRATEGY = require('./enums').FLOWSTRATEGY
/**
* File with default settings
*/
module.exports = {
/**
* By default we use -> req.user.scope
* Observation:
* - userScopesLocation is a string
* - req.user.scope is expected to be of type Array.
*
* When informed "a.b.c" we use -> req['a']['b']['c']
*/
userScopesLocation: USERSCOPESLOCATION.DEFAULT,

/**
* By default we use -> OR
* Observation:
* - logicalStrategy is a string
* - We currently only support "OR" and "AND".
*/
logicalStrategy: LOGICALSTRATEGY.OR,

/**
* responseCaseError is the content returned in the response body when flowStrategy is not modified,
* or when it is set to the default value "RETURNRESPONSE"
*/
responseCaseError: {
code: 403,
message: "FORBIDDEN",
description: "Authorization failed due to insufficient permissions.",
redirect_link: "/auth"
},

/**
* By default we use -> RETURNRESPONSE
* Observation:
* - flowStrategy is a string
* - "RETURNRESPONSE"-> When the user does not have the required scopes, the object responseCaseError is returned.
* - "NEXTWITHERROR"-> When the user does not have the required scopes, the next() function is called passing the responseCaseError object.
* - We currently only support "RETURNRESPONSE" and "NEXTWITHERROR".
*/
flowStrategy: FLOWSTRATEGY.RETURNRESPONSE
}
13 changes: 13 additions & 0 deletions lib/config/enums.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module.exports.USERSCOPESLOCATION = {
DEFAULT: "DEFAULT"
}

module.exports.LOGICALSTRATEGY = {
AND: "AND",
OR: "OR",
}

module.exports.FLOWSTRATEGY = {
RETURNRESPONSE: "RETURNRESPONSE",
NEXTWITHERROR: "NEXTWITHERROR",
}
29 changes: 29 additions & 0 deletions lib/property.extractor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use strict'

/**
* Function to extract the value of a property of an object from a string with its properties.
* Example:
* - object={req:{user:{scope:['scope1', 'scope2']}}}
* - str='req.user.scope'
* The expected return when applying the function is ['scope1', 'scope2'] *
* @param {*} object
* @param {*} str
*/
module.exports = (object, str) => {
if (typeof str !== 'string') {
const msgError = `The userScopeLocation property must be of type string. Check the settings passed to the simple-express-jwt-authorization middleware.`;
throw Error(msgError);
}
const propeties = str.split('.')
let property = object;
propeties.forEach((key) => {
if (property === undefined || property === null) {
console.warn(new Date().toISOString() + ' warn [simple-express-jwt-authorization] The property set on userScopeLocation returned undefined!');
return undefined;
}
if (!key) return property;
property = property[key];
});
if (property === undefined || property === null) console.warn(new Date().toISOString() + ' warn [simple-express-jwt-authorization] The property set on userScopeLocation returned undefined!');
return property;
}
63 changes: 63 additions & 0 deletions lib/simple.authorization.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
'use strict'

const USERSCOPESLOCATION = require('./config/enums').USERSCOPESLOCATION
const LOGICALSTRATEGY = require('./config/enums').LOGICALSTRATEGY
const FLOWSTRATEGY = require('./config/enums').FLOWSTRATEGY
const configurationsDefault = require('./config/default')
const validator = require('./validator')
const propertyExtract = require('./property.extractor')

let configurations = configurationsDefault

const config = (localConfigurations) => {
configurations = validator(localConfigurations)
}

const check = (expectedScopes, localConfigurations) => {
if (localConfigurations) {
configurations = validator(localConfigurations)
}
return (req, res, next) => {
if (!expectedScopes) {
return next()
}
if (Array.isArray(expectedScopes) && expectedScopes.length === 0) {
return next()
}
if (!Array.isArray(expectedScopes)) {
throw new Error('Expected scopes must be passed in the form of a array, verify the check() function!')
}

let userScopes = []
if (configurations.userScopesLocation === USERSCOPESLOCATION.DEFAULT) {
if (!req.user || !req.user.scope) {
throw new Error('You are using the default userScopeLocation, but req.user.scope is undefined.')
}
userScopes = req.user.scope
}
else {
userScopes = propertyExtract(req, configurations.userScopesLocation)
}

let accepted = false;

if (configurations.logicalStrategy === LOGICALSTRATEGY.AND) {
accepted = expectedScopes.every(scope => userScopes.includes(scope))
} else {
accepted = expectedScopes.some(scope => userScopes.includes(scope))
}

if (!accepted) {
if (configurations.flowStrategy === FLOWSTRATEGY.NEXTWITHERROR) {
return next(configurations.responseCaseError)
}
return res.status(403).send(configurations.responseCaseError)
}
return next()
}
}

module.exports = {
config,
check
}
Loading

0 comments on commit e6b49e1

Please sign in to comment.