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
douglasrafael committed Aug 2, 2019
2 parents d986831 + f9a813c commit 90b321e
Show file tree
Hide file tree
Showing 17 changed files with 534 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# webstorm settings
.idea

# Logs
logs
*.log
Expand Down
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# express-apigw-base-plugin

This plugin adds custom policies to the Express Gateway. For more information about Express Gateway plug-ins, see the documentation [Express Gateway - Plugins](https://www.express-gateway.io/docs/plugins/)

### How to manually install
1. Execute:
```
npm install https://github.com/nutes-uepb/express-apigw-base-plugin --save
```
2. Add the installed plugin to the gateway system settings in the file "system.config.yml". As in the example below:

```javascript
plugins:
express-apigw-base-plugin:
package: 'express-apigw-base-plugin/manifest.js'
```
4. Okay, done that it is already possible to use the policies in "gateway.config.yml"

### How to install automatically

Plugins are bundled as Node modules and distributed through npm. The Express Gateway CLI is used to install and configure plugins.
Installed plugins are declared in the system.config.yml and are then ready to be used. Express Gateway CLI is a convenient way to install and enable plugins.
```bash
$ eg plugin install https://github.com/nutes-uepb/express-apigw-base-plugin
```

### Description of policies

* body-parser-policy: Converts the request into an object using the body-parser.
+ No configuration parameters
* auth-policy: Performs authentication and then validates the consumer at the express gateway.
+ Configuration parameters:
- urlauthservice: Indicates authentication service url. (ie 'http://localhost:5000/api/users/auth')
- secretOrPublicKey: String with the secret to validate JWT token. (ie 'mysecret')
- secretOrPublicKeyFile: File with the secret to validate JWT token. (ie 'key.pem')
- issuer: Valid JWT token issuer. (ie 'myapp')
* jwt-policy: Validates token jwt based on expiration time and emitter. In addition to verifying the existence of the respective consumer. NOTE: In the case of the jwt secret, it can be informed using one of two properties: "secretOrPublicKey" or "secretOrPublicKeyFile".
+ Configuration parameters:
- secretOrPublicKey: String with the secret to validate JWT token. (ie 'mysecret')
- secretOrPublicKeyFile: File with the secret to validate JWT token. (ie 'key.pem')
- issuer: Valid JWT token issuer. (ie 'myapp')
* jwtScopes-policy: Performs the validation of the necessary scopes in the requests. Remembering that each scope must be configuring in each apiPublica path. In case of multiple scopes, the policy verifies the existence of at least one of the scopes.
+ No configuration parameters
27 changes: 27 additions & 0 deletions conditions/regex-path-method.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Condition to verify that the requested meets the regular expression and method configured.
* Only the regexpath parameter is required.
*/
module.exports = {
name: 'regex-path-method',
handler: function (req, conditionConfig) {
const regex = new RegExp(conditionConfig.regexpath);
if (conditionConfig.method) {
return (regex.test(req.url) && req.method === conditionConfig.method);
}
return regex.test(req.url);
},
schema: {
$id: 'http://express-gateway.io/schemas/conditions/regex-path-method.json',
type: 'object',
properties: {
regexPath: {
type: 'string'
},
method: {
type: 'string'
}
},
required: ['regexPath']
}
};
25 changes: 25 additions & 0 deletions manifest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'use strict'

/**
* Plug-in export file
* Where all plug-in policies, conditions, and routes are logged
*/
const fs = require('fs')
const apiReferencePathFile = process.env.API_REFERENCE_PATH_FILE
module.exports = {
version: '1.0.1',
init: function (pluginContext) {
pluginContext.registerPolicy(require('./policies/authentication/jwt-policy'))
pluginContext.registerPolicy(require('./policies/authorization/jwt-scopes-policy'))
pluginContext.registerPolicy(require('./policies/auth/auth-policy'))
pluginContext.registerPolicy(require('./policies/body-parser/body-parser-policy'))
pluginContext.registerPolicy(require('./policies/delete-user/delete-user-policy'))
pluginContext.registerCondition(require('./conditions/regex-path-method'))
pluginContext.registerGatewayRoute(require('./routes/middlewares'))
if (fs.existsSync(apiReferencePathFile)) {
pluginContext.registerGatewayRoute(require(apiReferencePathFile))
}
},
// this is for CLI to automatically add to "policies" whitelist in gateway.config
policies: ['jwt-policy', 'jwtScopes-policy', 'auth-policy', 'body-parser-policy', 'delete-user-policy']
}
22 changes: 22 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "express-apigw-base-plugin",
"version": "1.0.0",
"description": "Express Gateway Base Plugin",
"main": "manifest.js",
"author": "NUTES/UEPB",
"license": "Apache-2.0",
"dependencies": {
"axios": "^0.19.0",
"cors": "^2.8.5",
"body-parser": "^1.19.0",
"jsonwebtoken": "^8.5.1",
"passport": "^0.4.0",
"passport-jwt": "^4.0.0"
},
"devDependencies": {
"chai": "^4.2.0",
"mocha": "^6.2.0",
"sinon": "^7.3.2",
"supertest": "^4.0.2"
}
}
29 changes: 29 additions & 0 deletions policies/auth/auth-policy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Login Policy
*
*/
module.exports = {
name: 'auth-policy',
policy: require('./auth'),
schema: {
name: 'auth-policy',
$id: 'http://express-gateway.io/schemas/policies/auth-policy.json',
type: 'object',
properties: {
urlAuthService: {
type: 'string'
},
secretOrPublicKey: {
type: 'string'
},
secretOrPublicKeyFile: {
type: 'string'
},
issuer: {
type: 'string'
}
},
required: ['urlAuthService', 'issuer'],
oneOf: [{required: ['secretOrPublicKey']}, {required: ['secretOrPublicKeyFile']}]
}
}
77 changes: 77 additions & 0 deletions policies/auth/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* Login Policy
*/
const jwt = require('jsonwebtoken')
const fs = require('fs')
let userService = require('../../services/user-service')
let gatewayService = require('express-gateway/lib/services')

module.exports = function (actionParams, authServiceTest, localServicesTest) {
return async (req, res, next) => {
// Test context, Mocks authService and services.
if (authServiceTest && localServicesTest) {
userService = authServiceTest
gatewayService = localServicesTest
}

try {
// 1. Run authentication on the account-service
const authResponse = await userService.auth(actionParams.urlAuthService, req.body)
const accessToken = authResponse.data.access_token

// 2. Read JWT public key
const secretOrKey = actionParams.secretOrPublicKeyFile ?
fs.readFileSync(actionParams.secretOrPublicKeyFile, 'utf8') : actionParams.secretOrPublicKey

// 3. Checks whether the token obtained matches the public key.
const decode = await jwt.verify(accessToken, secretOrKey, {
algorithms: ['RS256'],
issuer: actionParams.issuer
})
// 3.1 Checks if the user ID to whom the token belongs is present. 'sub' is required.
if (!decode.sub) throw new Error('UNAUTHORIZED')

// 4. Get user in local database.
// If the user exists, there is nothing else to do.
if (await gatewayService.user.find(decode.sub)) return res.status(200).send(authResponse.data)

// 5. If the login was successful and the user is not yet registered at the local bank, the same is created.
const userGateway = await gatewayService.user.insert({username: decode.sub})
if (!userGateway) throw new Error('INTERNAL_ERROR')

return res.status(200).send(authResponse.data)
} catch (err) {
/* Report in logs that JWT has an invalid signature */
if (err.name === 'JsonWebTokenError' && err.message === 'invalid signature') {
console.error(`[auth-policy] error: Invalid JWT Signature!`)
}
if (err.response && err.response.data) {
return res.status(err.response.status).send(err.response.data)
}

if (err.message === 'UNAUTHORIZED') return res.status(401).send(handlerMessageError(401))
return res.status(500).send(handlerMessageError(500))
}
}
}

/**
* Handler of general error messages.
*
* @param code
* @returns {{code: number, message: string, description: string}}
*/
function handlerMessageError(code) {
if (code === 401) return {
'code': 401,
'message': 'UNAUTHORIZED',
'description': 'The token user is not properly registered as a consumer at the gateway.'
}

return {
'code': 500,
'message': 'INTERNAL SERVER ERROR',
'description': 'An internal server error has occurred.'
}
}

24 changes: 24 additions & 0 deletions policies/authentication/jwt-policy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Policy to validate JWT
*/
module.exports = {
name: 'jwt-policy',
policy: require('./jwt'),
schema: {
$id: 'http://express-gateway.io/schemas/policies/jwt-policy.json',
type: 'object',
properties: {
secretOrPublicKey: {
type: 'string'
},
secretOrPublicKeyFile: {
type: 'string'
},
issuer: {
type: 'string'
}
},
required: ['issuer'],
oneOf: [{required: ['secretOrPublicKey']}, {required: ['secretOrPublicKeyFile']}]
}
}
95 changes: 95 additions & 0 deletions policies/authentication/jwt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
'use strict'
/**
* Policy to validate JWT
*/
const passportJWT = require('passport-jwt')
const fs = require('fs')
let passport = require('passport')
let gatewayService = require('express-gateway/lib/services')

const JWTStrategy = passportJWT.Strategy
const ExtractJWT = passportJWT.ExtractJwt

module.exports = function (actionParams, testContext) {
// Test context. In testContext we have mocks functions.
if (testContext && testContext.isTest) {
passport = testContext.passport
gatewayService = testContext.services
}

const secretOrKey = actionParams.secretOrPublicKeyFile ?
fs.readFileSync(actionParams.secretOrPublicKeyFile, 'utf8') : actionParams.secretOrPublicKey

passport.use(new JWTStrategy({
jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken(),
secretOrKey: secretOrKey,
issuer: actionParams.issuer
}, (jwtPayload, done) => {
// At this point both the jwt signature, issuer and experation were validated
// In addition, we have the jwt payload decoded and we can access its attributes
// console.log(`JWT payload: ${JSON.stringify(jwtPayload)}`);

// User validation. We expect to receive the username in the jwt 'sub' field
if (!jwtPayload.sub) return done(null, false)

gatewayService.auth
.validateConsumer(jwtPayload.sub, {checkUsername: true})
.then((consumer) => {
if (!consumer) {
// invalid username or inactive user
return done(null, false, {message: 'Invalid or inactive user'})
}
return done(null, jwtPayload) // jwt successfully authenticated
})
.catch((err) => {
if (err.message === 'CREDENTIAL_NOT_FOUND') {
return done(null, false, {message: 'User not found'})
}
return done(err)
})
}))

return (req, res, next) => {
passport.authenticate('jwt', {session: false}, (err, user, info) => {
if (user) {
req.user = user
next()
}

if (info && info.message === 'No auth token') return res.status(401).send({
'code': 401,
'message': 'UNAUTHORIZED',
'description': 'Authentication failed for lack of authentication credentials.',
'redirect_link': '/auth'
})

if (info && info.message === 'Invalid or inactive user') return res.status(401).send({
'code': 401,
'message': 'UNAUTHORIZED',
'description': 'The token user is not properly registered as a consumer at the gateway.',
'redirect_link': '/auth'
})

if (info && info.message === 'jwt expired') return res.status(401).send({
'code': 401,
'message': 'UNAUTHORIZED',
'description': 'Authentication failed because access token is expired.',
'redirect_link': '/auth'
})

if (info && info.message === 'jwt issuer invalid. expected: ocariot') return res.status(401).send({
'code': 401,
'message': 'UNAUTHORIZED',
'description': 'Authentication failed because the access token contains invalid parameters.',
'redirect_link': '/auth'
})

if (info && info.message) return res.status(401).send({
'code': 401,
'message': 'UNAUTHORIZED',
'description': 'Authentication failed due to access token issues.',
'redirect_link': '/auth'
})
})(req, res, next)
}
}
12 changes: 12 additions & 0 deletions policies/authorization/jwt-scopes-policy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Policy to validate scopes
*/
module.exports = {
name: 'jwtScopes-policy',
policy: require('./jwt-scopes'),
schema: {
$id: 'http://express-gateway.io/schemas/policies/jwtScopes-policy.json',
type: 'object',
properties: {}
}
}
Loading

0 comments on commit 90b321e

Please sign in to comment.