Huge update and cleanup on dependencies and test environment among other stuff
This commit is contained in:
parent
8f70c1de67
commit
dc462a3d53
32 changed files with 984 additions and 383 deletions
5
.babelrc
5
.babelrc
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"plugins": [
|
||||
"transform-es2015-modules-commonjs"
|
||||
]
|
||||
}
|
|
@ -2,31 +2,26 @@ version: 2
|
|||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: docker:latest
|
||||
- image: circleci/node:latest
|
||||
environment:
|
||||
- di: "nfpis/storage-upload"
|
||||
- dtag: "latest"
|
||||
working_directory: ~/storage-upload
|
||||
steps:
|
||||
- run:
|
||||
name: Update and install SSH & Git
|
||||
command: apk update && apk upgrade && apk add --no-cache bash git openssh
|
||||
- checkout
|
||||
- setup_remote_docker
|
||||
- run:
|
||||
name: Build docker image
|
||||
name: Install dependencies
|
||||
command: |
|
||||
docker build -t nfp-test .
|
||||
docker build --build-arg NODE=production -t ${di}:build_${CIRCLE_BUILD_NUM} -t ${di}:${CIRCLE_SHA1} -t ${di}:${dtag} .
|
||||
npm install
|
||||
- run:
|
||||
name: Run tests
|
||||
command: |
|
||||
docker run nfp-test npm run test
|
||||
- deploy:
|
||||
name: Push to docker
|
||||
command: |
|
||||
docker login -u $DOCKER_USER -p $DOCKER_PASS
|
||||
docker push ${di}
|
||||
npm test
|
||||
# - deploy:
|
||||
# name: Push to docker
|
||||
# command: |
|
||||
# docker login -u $DOCKER_USER -p $DOCKER_PASS
|
||||
# docker push ${di}
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
'use strict'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import Nconf from 'nconf-lite'
|
||||
|
||||
const nconf = new Nconf()
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
let pckg = JSON.parse(fs.readFileSync(path.resolve(path.join(__dirname, '../package.json'))))
|
||||
|
||||
const nconf = require('nconf')
|
||||
|
||||
// Helper method for global usage.
|
||||
nconf.inTest = () => nconf.get('NODE_ENV') === 'test'
|
||||
|
@ -12,9 +18,6 @@ nconf.inTest = () => nconf.get('NODE_ENV') === 'test'
|
|||
// 4. config/config.default.json
|
||||
|
||||
|
||||
// Load package.json for name and such
|
||||
let pckg = require('./package.json')
|
||||
|
||||
pckg = {
|
||||
name: pckg.name,
|
||||
version: pckg.version,
|
||||
|
@ -34,22 +37,22 @@ nconf.env()
|
|||
|
||||
|
||||
// Load any overrides from the appropriate config file
|
||||
let configFile = 'config/config.json'
|
||||
let configFile = '../config/config.json'
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (nconf.get('NODE_ENV') === 'test') {
|
||||
configFile = 'config/config.test.json'
|
||||
configFile = '../config/config.test.json'
|
||||
}
|
||||
|
||||
/* istanbul ignore if */
|
||||
if (nconf.get('NODE_ENV') === 'production') {
|
||||
configFile = 'config/config.production.json'
|
||||
configFile = '../config/config.production.json'
|
||||
}
|
||||
|
||||
nconf.file('main', configFile)
|
||||
nconf.file('main', path.resolve(path.join(__dirname, configFile)))
|
||||
|
||||
// Load defaults
|
||||
nconf.file('default', 'config/config.default.json')
|
||||
nconf.file('default', path.resolve(path.join(__dirname, '../config/config.default.json')))
|
||||
|
||||
|
||||
// Final sanity checks
|
||||
|
@ -61,4 +64,4 @@ if (typeof global.it === 'function' & !nconf.inTest()) {
|
|||
}
|
||||
|
||||
|
||||
module.exports = nconf
|
||||
export default nconf
|
|
@ -1,20 +0,0 @@
|
|||
|
||||
function defaults(options, def) {
|
||||
options = options || {}
|
||||
|
||||
Object.keys(def).forEach(function(key) {
|
||||
if (typeof options[key] === 'undefined') {
|
||||
// No need to do clone since we mostly deal with
|
||||
// flat objects
|
||||
options[key] = def[key]
|
||||
}
|
||||
else if (typeof options[key] === 'object' &&
|
||||
typeof def[key] === 'object') {
|
||||
options[key] = defaults(options[key], def[key])
|
||||
}
|
||||
})
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
export default defaults
|
34
api/defaults.mjs
Normal file
34
api/defaults.mjs
Normal file
|
@ -0,0 +1,34 @@
|
|||
|
||||
// taken from isobject npm library
|
||||
function isObject(val) {
|
||||
return val != null && typeof val === 'object' && Array.isArray(val) === false
|
||||
}
|
||||
|
||||
export default function defaults(options, def) {
|
||||
let out = { }
|
||||
|
||||
if (options) {
|
||||
Object.keys(options || {}).forEach(key => {
|
||||
out[key] = options[key]
|
||||
|
||||
if (Array.isArray(out[key])) {
|
||||
out[key] = out[key].map(item => {
|
||||
if (isObject(item)) return defaults(item)
|
||||
return item
|
||||
})
|
||||
} else if (out[key] && typeof out[key] === 'object') {
|
||||
out[key] = defaults(options[key], def && def[key])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (def) {
|
||||
Object.keys(def).forEach(function(key) {
|
||||
if (typeof out[key] === 'undefined') {
|
||||
out[key] = def[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
19
api/jwt.js
19
api/jwt.js
|
@ -1,19 +0,0 @@
|
|||
import jwt from 'jsonwebtoken'
|
||||
|
||||
export function sign(value, secret) {
|
||||
return jwt.sign(value, secret)
|
||||
}
|
||||
|
||||
export function verify(token, secret) {
|
||||
return new Promise((resolve, reject) =>
|
||||
jwt.verify(token, secret, (err, res) => {
|
||||
if (err) return reject(err)
|
||||
|
||||
resolve(res)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export function decode(token) {
|
||||
return jwt.decode(token)
|
||||
}
|
21
api/jwt/base64urlsafe.mjs
Normal file
21
api/jwt/base64urlsafe.mjs
Normal file
|
@ -0,0 +1,21 @@
|
|||
|
||||
export function encode(buffer) {
|
||||
return buffer
|
||||
.toString('base64')
|
||||
.replace(/\+/g, '-') // Convert '+' to '-'
|
||||
.replace(/\//g, '_') // Convert '/' to '_'
|
||||
.replace(/=+$/, '') // Remove ending '='
|
||||
}
|
||||
|
||||
export function decode(base64StringUrlSafe) {
|
||||
let base64String = base64StringUrlSafe.replace(/-/g, '+').replace(/_/g, '/')
|
||||
switch (base64String.length % 4) {
|
||||
case 2:
|
||||
base64String += '=='
|
||||
break
|
||||
case 3:
|
||||
base64String += '='
|
||||
break
|
||||
}
|
||||
return Buffer.from(base64String, 'base64')
|
||||
}
|
169
api/jwt/decode.mjs
Normal file
169
api/jwt/decode.mjs
Normal file
|
@ -0,0 +1,169 @@
|
|||
import crypto from 'crypto'
|
||||
import defaults from '../defaults.mjs'
|
||||
import * as base64UrlSafe from './base64urlsafe.mjs'
|
||||
|
||||
const defaultOptions = {
|
||||
expiresSkew: 0,
|
||||
expiresMax: 0,
|
||||
nbfIatSkew: 300,
|
||||
fixup: null
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} jwt
|
||||
* @param {Object} publicKeys
|
||||
* @param {Array<string>} audiences
|
||||
* @param {Object} [options]
|
||||
* @param {Object} [options.expiresSkew=0]
|
||||
* @param {Object} [options.expiresMax=0]
|
||||
* @param {Object} [options.nbfIatSkew=300]
|
||||
* @param {Function<header,body,void>} [options.fixup]
|
||||
*/
|
||||
export default function decode(jwt, publicKeys, audiences, options = defaultOptions) {
|
||||
if (typeof jwt !== 'string') {
|
||||
throw new Error('jwt needs to a string')
|
||||
}
|
||||
|
||||
if (typeof publicKeys !== 'object' || Array.isArray(publicKeys)) {
|
||||
throw new Error(
|
||||
'publicKeys needs to be a map of { issuer: { keyid: "PEM encoded key" }'
|
||||
)
|
||||
}
|
||||
|
||||
if (!Array.isArray(audiences)) {
|
||||
throw new Error('audiences needs to be an array of allowed audiences')
|
||||
}
|
||||
|
||||
if (typeof options !== 'object' || Array.isArray(publicKeys)) {
|
||||
throw new Error('options needs to a map of { nbfIatSkew: 300, ... }')
|
||||
}
|
||||
|
||||
let parts = jwt.split(/\./)
|
||||
if (parts.length !== 3) {
|
||||
throw new Error('JWT does not contain 3 dots')
|
||||
}
|
||||
|
||||
let header = JSON.parse(base64UrlSafe.decode(parts[0]).toString('utf8'))
|
||||
let body = JSON.parse(base64UrlSafe.decode(parts[1]).toString('utf8'))
|
||||
if (typeof options.fixup === 'function') {
|
||||
options.fixup(header, body)
|
||||
}
|
||||
|
||||
let hmacAlgo = null
|
||||
switch (header.alg) {
|
||||
case 'HS256':
|
||||
hmacAlgo = 'sha256'
|
||||
break
|
||||
case 'HS384':
|
||||
hmacAlgo = 'sha384'
|
||||
break
|
||||
case 'HS512':
|
||||
hmacAlgo = 'sha512'
|
||||
break
|
||||
default:
|
||||
throw new Error(
|
||||
'Only alg HS256, HS384 and HS512 are supported'
|
||||
)
|
||||
}
|
||||
|
||||
if (!body.iss) {
|
||||
throw new Error('No issuer set')
|
||||
}
|
||||
|
||||
let issuer = publicKeys[body.iss]
|
||||
if (!issuer) {
|
||||
throw new Error(`Unknown issuer '${body.iss}'`)
|
||||
}
|
||||
|
||||
// Find public key
|
||||
let pubkeyOrSharedKey =
|
||||
typeof header.kid === 'string'
|
||||
? issuer[`${header.kid}@${header.alg}`]
|
||||
: issuer[`default@${header.alg}`]
|
||||
let overrideOptions = {}
|
||||
|
||||
if (!pubkeyOrSharedKey) {
|
||||
throw new Error(
|
||||
`Unknown pubkey id '${header.kid}' for this issuer`
|
||||
)
|
||||
} else if ((typeof(pubkeyOrSharedKey) !== 'object' || Array.isArray(pubkeyOrSharedKey)) && typeof(pubkeyOrSharedKey) !== 'string') {
|
||||
throw new Error(
|
||||
`Pubkey of '${header.kid || 'default'}' for '${header.alg}' for this issuer is misconfigured`
|
||||
)
|
||||
}
|
||||
if (typeof(pubkeyOrSharedKey) === 'object') {
|
||||
if (typeof(pubkeyOrSharedKey.key) !== 'string') {
|
||||
throw new Error(
|
||||
`Pubkey of '${header.kid || 'default'}' for '${header.alg}' for this issuer is misconfigured`
|
||||
)
|
||||
}
|
||||
overrideOptions = pubkeyOrSharedKey
|
||||
pubkeyOrSharedKey = pubkeyOrSharedKey.key
|
||||
}
|
||||
|
||||
let signatureOrHash = base64UrlSafe.decode(parts[2])
|
||||
const hmac = crypto.createHmac(hmacAlgo, pubkeyOrSharedKey)
|
||||
hmac.update(`${parts[0]}.${parts[1]}`, 'utf8')
|
||||
let signatureBuffer = hmac.digest()
|
||||
|
||||
if (signatureOrHash.length !== signatureBuffer.length || !crypto.timingSafeEqual(signatureOrHash, signatureBuffer)) {
|
||||
throw new Error(`Verification failed with alg '${header.alg}'`)
|
||||
}
|
||||
|
||||
let unixNow = Math.floor(Date.now() / 1000)
|
||||
|
||||
let validators = defaults(options.validators, {
|
||||
aud: validateAudience,
|
||||
exp: validateExpires,
|
||||
iat: validateIssuedAt,
|
||||
nbf: validateNotBefore
|
||||
})
|
||||
|
||||
let validationOptions = defaults(overrideOptions, options)
|
||||
|
||||
validators.aud(body, audiences, validationOptions)
|
||||
validators.iat(body, unixNow, validationOptions)
|
||||
validators.nbf(body, unixNow, validationOptions)
|
||||
validators.exp(body, unixNow, validationOptions)
|
||||
|
||||
return body
|
||||
}
|
||||
|
||||
function validateNotBefore(body, unixNow, options) {
|
||||
if (body.nbf && body.nbf > unixNow + options.nbfIatSkew) {
|
||||
throw new Error(
|
||||
`Not before in the future by more than ${options.nbfIatSkew} seconds`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function validateIssuedAt(body, unixNow, options) {
|
||||
if (body.iat && body.iat > unixNow + options.nbfIatSkew) {
|
||||
throw new Error(
|
||||
`Issued at in the future by more than ${options.nbfIatSkew} seconds`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function validateAudience(body, audiences, options) {
|
||||
let auds = Array.isArray(body.aud) ? body.aud : [body.aud]
|
||||
if (!auds.some(aud => audiences.includes(aud))) {
|
||||
throw new Error(`Unknown audience '${auds.join(',')}'`)
|
||||
}
|
||||
}
|
||||
|
||||
function validateExpires(body, unixNow, options) {
|
||||
if (!body.exp) {
|
||||
throw new Error(`No expires set on token`)
|
||||
}
|
||||
let notBefore = body.iat || body.nbf || unixNow
|
||||
if (options.expiresMax && body.exp > notBefore + options.expiresMax) {
|
||||
throw new Error(
|
||||
`Expires in the future by more than ${options.expiresMax} seconds`
|
||||
)
|
||||
}
|
||||
if (body.exp + (options.expiresSkew || 0) <= unixNow) {
|
||||
throw new Error('Token has expired')
|
||||
}
|
||||
}
|
43
api/jwt/encode.mjs
Normal file
43
api/jwt/encode.mjs
Normal file
|
@ -0,0 +1,43 @@
|
|||
import crypto from 'crypto'
|
||||
import * as base64UrlSafe from './base64urlsafe.mjs'
|
||||
|
||||
export default function encode(header, body, privateKeyPassword = null) {
|
||||
if (
|
||||
typeof header !== 'object' ||
|
||||
Array.isArray(header) ||
|
||||
typeof body !== 'object' ||
|
||||
Array.isArray(body)
|
||||
) {
|
||||
throw new Error('both header and body should be of type object')
|
||||
}
|
||||
|
||||
let hmacAlgo = null
|
||||
switch (header.alg) {
|
||||
case 'HS256':
|
||||
hmacAlgo = 'sha256'
|
||||
break
|
||||
case 'HS384':
|
||||
hmacAlgo = 'sha384'
|
||||
break
|
||||
case 'HS512':
|
||||
hmacAlgo = 'sha512'
|
||||
break
|
||||
default:
|
||||
throw new Error(
|
||||
'Only alg HS256, HS384 and HS512 are supported'
|
||||
)
|
||||
}
|
||||
|
||||
// Base64 encode header and body
|
||||
let headerBase64 = base64UrlSafe.encode(Buffer.from(JSON.stringify(header)))
|
||||
let bodyBase64 = base64UrlSafe.encode(Buffer.from(JSON.stringify(body)))
|
||||
let headerBodyBase64 = headerBase64 + '.' + bodyBase64
|
||||
|
||||
const hmac = crypto.createHmac(hmacAlgo, privateKeyPassword)
|
||||
hmac.update(headerBodyBase64)
|
||||
let signatureBuffer = hmac.digest()
|
||||
|
||||
// Construct final JWT
|
||||
let signatureBase64 = base64UrlSafe.encode(signatureBuffer)
|
||||
return headerBodyBase64 + '.' + signatureBase64
|
||||
}
|
29
api/log.mjs
Normal file
29
api/log.mjs
Normal file
|
@ -0,0 +1,29 @@
|
|||
import bunyan from 'bunyan-lite'
|
||||
import config from './config.mjs'
|
||||
import * as defaults from './defaults.mjs'
|
||||
|
||||
|
||||
// Clone the settings as we will be touching
|
||||
// on them slightly.
|
||||
let settings = defaults.default(config.get('bunyan'))
|
||||
|
||||
// Replace any instance of 'process.stdout' with the
|
||||
// actual reference to the process.stdout.
|
||||
for (let i = 0; i < settings.streams.length; i++) {
|
||||
/* istanbul ignore else */
|
||||
if (settings.streams[i].stream === 'process.stdout') {
|
||||
settings.streams[i].stream = process.stdout
|
||||
}
|
||||
}
|
||||
|
||||
// Create our logger.
|
||||
const log = bunyan.createLogger(settings)
|
||||
|
||||
export default log
|
||||
|
||||
log.logMiddleware = () =>
|
||||
(ctx, next) => {
|
||||
ctx.log = log
|
||||
|
||||
return next()
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
const fs = require('fs')
|
||||
import multer from 'multer'
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
/*const storage = multer.diskStorage({
|
||||
destination: function (req, file, cb) {
|
||||
cb(null, '/tmp/my-uploads')
|
||||
},
|
||||
|
@ -9,7 +8,7 @@ const storage = multer.diskStorage({
|
|||
console.log(file)
|
||||
cb(null, file.fieldname + '-' + Date.now())
|
||||
}
|
||||
})
|
||||
})*/
|
||||
|
||||
export function uploadFile(ctx, siteName) {
|
||||
return new Promise((res, rej) => {
|
|
@ -1,6 +1,5 @@
|
|||
import config from '../../config'
|
||||
import { verifyToken } from './security'
|
||||
import { uploadFile, rename } from './multer'
|
||||
import { verifyToken } from './security.mjs'
|
||||
import { uploadFile } from './multer.mjs'
|
||||
|
||||
export async function upload(ctx) {
|
||||
let site = await verifyToken(ctx)
|
|
@ -1,12 +1,12 @@
|
|||
import * as jwt from '../jwt'
|
||||
import config from '../../config'
|
||||
// import * as jwt from '../jwt.mjs'
|
||||
import config from '../config.mjs'
|
||||
|
||||
export async function verifyToken(ctx) {
|
||||
if (!ctx.query.token) {
|
||||
throw new Error('Token is missing in query')
|
||||
}
|
||||
|
||||
let decoded = jwt.decode(ctx.query.token)
|
||||
/*let decoded = jwt.decode(ctx.query.token)
|
||||
|
||||
if (!decoded || !decoded.site) {
|
||||
throw new Error('Token is invalid')
|
||||
|
@ -15,7 +15,7 @@ export async function verifyToken(ctx) {
|
|||
let output = await jwt.verify(
|
||||
ctx.query.token,
|
||||
config.get(`sites:${decoded.site}`)
|
||||
)
|
||||
)*/
|
||||
|
||||
return output.site
|
||||
}
|
|
@ -1,11 +1,12 @@
|
|||
import Router from 'koa-router'
|
||||
|
||||
import * as test from './test/routes'
|
||||
import * as media from './media/routes'
|
||||
import * as test from './test/routes.mjs'
|
||||
import * as media from './media/routes.mjs'
|
||||
|
||||
const router = new Router()
|
||||
|
||||
router.get('/', test.testStatic)
|
||||
router.get('/error', test.testError)
|
||||
router.post('/media', media.upload)
|
||||
|
||||
export default router
|
18
api/server.mjs
Normal file
18
api/server.mjs
Normal file
|
@ -0,0 +1,18 @@
|
|||
import Koa from 'koa-lite'
|
||||
|
||||
import config from './config.mjs'
|
||||
import log from './log.mjs'
|
||||
import router from './router.mjs'
|
||||
import { errorMiddleware } from './error.mjs'
|
||||
|
||||
const app = new Koa()
|
||||
|
||||
app.use(errorMiddleware)
|
||||
app.use(router.routes())
|
||||
app.use(router.allowedMethods())
|
||||
|
||||
const server = app.listen(config.get('server:port'), function(a,b) {
|
||||
log.info(`Server listening at ${config.get('server:port')}`)
|
||||
})
|
||||
|
||||
export default server
|
|
@ -1,4 +1,4 @@
|
|||
import config from '../../config'
|
||||
import config from '../config.mjs'
|
||||
|
||||
export async function testStatic(ctx) {
|
||||
ctx.body = {
|
||||
|
@ -7,3 +7,7 @@ export async function testStatic(ctx) {
|
|||
environment: config.get('NODE_ENV'),
|
||||
}
|
||||
}
|
||||
|
||||
export async function testError(ctx) {
|
||||
throw new Error('This is a test')
|
||||
}
|
3
index.js
3
index.js
|
@ -1,3 +0,0 @@
|
|||
require('babel-register')
|
||||
|
||||
require('./server')
|
26
package.json
26
package.json
|
@ -5,13 +5,8 @@
|
|||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "nodemon index.js",
|
||||
"start": "node index.js",
|
||||
"test": "env NODE_ENV=test mocha --require babel-register --recursive --reporter dot",
|
||||
"docker": "docker run -it --rm --name my-running-script -v \"$PWD\":/usr/src/app -w /usr/src/app node:alpine",
|
||||
"install:test": "npm install --no-optional && npm run test",
|
||||
"install:dev": "npm install --no-optional && npm run dev",
|
||||
"docker:test": "npm run docker -- npm run install:test",
|
||||
"docker:dev": "npm run docker -- npm run install:dev"
|
||||
"start": "node --experimental-modules api/server.mjs",
|
||||
"test": "eltro test/**/*.test.mjs -r dot"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -24,20 +19,13 @@
|
|||
},
|
||||
"homepage": "https://github.com/nfp-projects/storage-upload#readme",
|
||||
"dependencies": {
|
||||
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
|
||||
"babel-register": "^6.26.0",
|
||||
"koa": "^2.3.0",
|
||||
"multer": "^1.3.0",
|
||||
"bunyan-lite": "^1.1.1",
|
||||
"koa-lite": "^2.10.1",
|
||||
"koa-router": "^7.2.1",
|
||||
"jsonwebtoken": "^8.1.0",
|
||||
"nconf": "^0.8.5"
|
||||
"multer": "^1.3.0",
|
||||
"nconf-lite": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"app-root-path": "^2.0.1",
|
||||
"assert-extended": "^1.0.1",
|
||||
"mocha": "^4.0.1",
|
||||
"nodemon": "^1.12.1",
|
||||
"request-json": "^0.6.2",
|
||||
"sinon": "^4.1.3"
|
||||
"eltro": "^1.1.0"
|
||||
}
|
||||
}
|
||||
|
|
15
server.js
15
server.js
|
@ -1,15 +0,0 @@
|
|||
import Koa from 'koa'
|
||||
|
||||
import config from './config'
|
||||
import router from './api/router'
|
||||
import { errorMiddleware } from './api/error'
|
||||
|
||||
const app = new Koa()
|
||||
|
||||
app.use(errorMiddleware)
|
||||
app.use(router.routes())
|
||||
app.use(router.allowedMethods())
|
||||
|
||||
const server = app.listen(config.get('server:port'))
|
||||
|
||||
export default server
|
|
@ -1,32 +0,0 @@
|
|||
import assert from 'assert-extended'
|
||||
import sinon from 'sinon'
|
||||
|
||||
describe('defaults', () => {
|
||||
const defaults = require('../api/defaults').default
|
||||
|
||||
describe('#defaults()', () => {
|
||||
it('should apply defaults to flat objects', () => {
|
||||
let assertOutput = { a: 1 }
|
||||
let output = defaults(null, { a: 1 })
|
||||
|
||||
assert.deepEqual(output, assertOutput)
|
||||
})
|
||||
|
||||
it('should allow overriding defult properties', () => {
|
||||
let assertOutput = { a: 2 }
|
||||
let output = defaults(assertOutput, { a: 1 })
|
||||
|
||||
assert.deepEqual(output, assertOutput)
|
||||
})
|
||||
|
||||
it('should allow nesting through objects', () => {
|
||||
let def = { a: { b: 2 } }
|
||||
let output = defaults({ a: { c: 3} }, def)
|
||||
|
||||
assert.deepEqual(output.a, {
|
||||
b: 2,
|
||||
c: 3,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
31
test/defaults.test.mjs
Normal file
31
test/defaults.test.mjs
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { Eltro as t, assert} from 'eltro'
|
||||
import defaults from '../api/defaults.mjs'
|
||||
|
||||
t.describe('#defaults()', () => {
|
||||
t.test('should apply defaults to flat objects', () => {
|
||||
let assertOutput = { a: 1 }
|
||||
let output = defaults(null, { a: 1 })
|
||||
|
||||
assert.deepStrictEqual(output, assertOutput)
|
||||
output = defaults({ a: 1 })
|
||||
|
||||
assert.deepStrictEqual(output, assertOutput)
|
||||
})
|
||||
|
||||
t.test('should allow overriding defult properties', () => {
|
||||
let assertOutput = { a: 2 }
|
||||
let output = defaults(assertOutput, { a: 1 })
|
||||
|
||||
assert.deepStrictEqual(output, assertOutput)
|
||||
})
|
||||
|
||||
t.test('should allow nesting through objects', () => {
|
||||
let def = { a: { b: 2 } }
|
||||
let output = defaults({ a: { c: 3} }, def)
|
||||
|
||||
assert.deepStrictEqual(output.a, {
|
||||
b: 2,
|
||||
c: 3,
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,54 +0,0 @@
|
|||
import assert from 'assert-extended'
|
||||
import sinon from 'sinon'
|
||||
|
||||
import { createContext } from './helper.server'
|
||||
|
||||
describe('Error (Middleware)', () => {
|
||||
const error = require('../api/error')
|
||||
|
||||
let ctx
|
||||
|
||||
beforeEach(() => {
|
||||
ctx = createContext({ })
|
||||
})
|
||||
|
||||
describe('#errorMiddleware()', () => {
|
||||
let stub
|
||||
|
||||
beforeEach(() => {
|
||||
stub = sinon.stub()
|
||||
})
|
||||
|
||||
it('should call next and not do anything if success', async () => {
|
||||
await error.errorMiddleware(ctx, stub)
|
||||
|
||||
assert.ok(stub.called)
|
||||
assert.strictEqual(ctx.body, undefined)
|
||||
assert.strictEqual(ctx.status, undefined)
|
||||
})
|
||||
|
||||
it('should support stub throwing', async () => {
|
||||
let assertError = new Error('testetytest')
|
||||
stub.throws(assertError)
|
||||
|
||||
await error.errorMiddleware(ctx, stub)
|
||||
|
||||
assert.ok(ctx.body)
|
||||
assert.strictEqual(ctx.status, 422)
|
||||
assert.strictEqual(ctx.body.status, 422)
|
||||
assert.strictEqual(ctx.body.message, assertError.message)
|
||||
})
|
||||
|
||||
it('should support stub resolving false', async () => {
|
||||
let assertError = new Error('herpaderpderp')
|
||||
stub.rejects(assertError)
|
||||
|
||||
await error.errorMiddleware(ctx, stub)
|
||||
|
||||
assert.ok(ctx.body)
|
||||
assert.strictEqual(ctx.status, 422)
|
||||
assert.strictEqual(ctx.body.status, 422)
|
||||
assert.strictEqual(ctx.body.message, assertError.message)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,89 +0,0 @@
|
|||
import request from 'request-json'
|
||||
|
||||
import defaults from '../api/defaults'
|
||||
import config from '../config'
|
||||
|
||||
function parseBody(body, reject) {
|
||||
try {
|
||||
return JSON.parse(body)
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(body)
|
||||
return reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
function callback(resolve, reject) {
|
||||
return (err, res, rawBody) => {
|
||||
let body = rawBody
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
if (typeof body === 'string' && body) {
|
||||
body = parseBody(body, reject)
|
||||
}
|
||||
if (res.statusCode >= 300 ||
|
||||
res.statusCode < 200) {
|
||||
return reject(body)
|
||||
}
|
||||
resolve(body)
|
||||
}
|
||||
}
|
||||
|
||||
export default function createClient(host = config.get('server:port'), opts) {
|
||||
let options = defaults(opts, {})
|
||||
|
||||
let client = request.createClient('', options)
|
||||
let prefix
|
||||
|
||||
prefix = `http://localhost:${host}`
|
||||
client.headers['x-request-id'] = 'asdf'
|
||||
|
||||
client.auth = (user) => {
|
||||
// let m = helperDB.model('user', {
|
||||
// id: user.id,
|
||||
// level: (user.get && user.get('level')) || 1,
|
||||
// institute_id: (user.get && user.get('institute_id')) || null,
|
||||
// password: (user.get && user.get('password')) || null,
|
||||
// })
|
||||
// let token = jwt.createUserToken(m)
|
||||
// client.headers.authorization = `Bearer ${token}`
|
||||
}
|
||||
|
||||
// Simple wrappers to wrap into promises
|
||||
client.getAsync = (path) =>
|
||||
new Promise((resolve, reject) => {
|
||||
if (path.slice(0, 4) === 'http') {
|
||||
return client.get(path, callback(resolve, reject))
|
||||
}
|
||||
client.get(prefix + path, callback(resolve, reject))
|
||||
})
|
||||
|
||||
// Simple wrappers to wrap into promises
|
||||
client.saveFileAsync = (path, destination) =>
|
||||
new Promise((resolve, reject) => {
|
||||
client.saveFile(prefix + path, destination, callback(resolve, reject, true))
|
||||
})
|
||||
|
||||
client.postAsync = (path, data) =>
|
||||
new Promise((resolve, reject) => {
|
||||
client.post(prefix + path, data, callback(resolve, reject))
|
||||
})
|
||||
|
||||
client.putAsync = (path, data) =>
|
||||
new Promise((resolve, reject) => {
|
||||
client.put(prefix + path, data, callback(resolve, reject))
|
||||
})
|
||||
|
||||
client.deleteAsync = (path) =>
|
||||
new Promise((resolve, reject) => {
|
||||
client.del(prefix + path, callback(resolve, reject))
|
||||
})
|
||||
|
||||
client.sendFileAsync = (path, files, data) =>
|
||||
new Promise((resolve, reject) => {
|
||||
client.sendFile(prefix + path, files, data || {}, callback(resolve, reject))
|
||||
})
|
||||
|
||||
return client
|
||||
}
|
123
test/helper.client.mjs
Normal file
123
test/helper.client.mjs
Normal file
|
@ -0,0 +1,123 @@
|
|||
import http from 'http'
|
||||
import { URL } from 'url'
|
||||
import defaults from '../api/defaults.mjs'
|
||||
import config from '../api/config.mjs'
|
||||
|
||||
export default function Client(port = config.get('server:port'), opts) {
|
||||
this.options = defaults(opts, {})
|
||||
this.prefix = `http://localhost:${port}`
|
||||
}
|
||||
|
||||
Client.prototype.customRequest = function(method = 'GET', path, body, options) {
|
||||
if (path.slice(0, 4) !== 'http') {
|
||||
path = this.prefix + path
|
||||
}
|
||||
let urlObj = new URL(path)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const opts = defaults(defaults(options, {
|
||||
method: method,
|
||||
timeout: 500,
|
||||
protocol: urlObj.protocol,
|
||||
username: urlObj.username,
|
||||
password: urlObj.password,
|
||||
host: urlObj.hostname,
|
||||
port: Number(urlObj.port),
|
||||
path: urlObj.pathname + urlObj.search,
|
||||
}))
|
||||
|
||||
const req = http.request(opts)
|
||||
if (body) {
|
||||
req.write(body)
|
||||
}
|
||||
|
||||
req.on('error', reject)
|
||||
req.on('timeout', function() { reject(new Error(`Request ${method} ${path} timed out`)) })
|
||||
req.on('response', res => {
|
||||
res.setEncoding('utf8')
|
||||
let output = ''
|
||||
|
||||
res.on('data', function (chunk) {
|
||||
output += chunk.toString()
|
||||
})
|
||||
|
||||
res.on('end', function () {
|
||||
try {
|
||||
output = JSON.parse(output)
|
||||
} catch (e) {
|
||||
return reject(new Error(`${e.message} while decoding: ${output}`))
|
||||
}
|
||||
if (output.status) {
|
||||
let err = new Error(`Request failed [${output.status}]: ${output.message}`)
|
||||
err.body = output
|
||||
return reject(err)
|
||||
}
|
||||
resolve(output)
|
||||
})
|
||||
})
|
||||
req.end()
|
||||
})
|
||||
}
|
||||
|
||||
Client.prototype.get = function(path = '/') {
|
||||
return this.customRequest('GET', path, null)
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
export function createClient(host = config.get('server:port'), opts) {
|
||||
let options = defaults(opts, {})
|
||||
|
||||
let prefix = `http://localhost:${host}`
|
||||
options.headers['x-request-id'] = 'asdf'
|
||||
|
||||
client.auth = (user) => {
|
||||
// let m = helperDB.model('user', {
|
||||
// id: user.id,
|
||||
// level: (user.get && user.get('level')) || 1,
|
||||
// institute_id: (user.get && user.get('institute_id')) || null,
|
||||
// password: (user.get && user.get('password')) || null,
|
||||
// })
|
||||
// let token = jwt.createUserToken(m)
|
||||
// client.headers.authorization = `Bearer ${token}`
|
||||
}
|
||||
|
||||
// Simple wrappers to wrap into promises
|
||||
client.getAsync = (path) =>
|
||||
new Promise((resolve, reject) => {
|
||||
if (path.slice(0, 4) === 'http') {
|
||||
return client.get(path, callback(resolve, reject))
|
||||
}
|
||||
client.get(prefix + path, callback(resolve, reject))
|
||||
})
|
||||
|
||||
// Simple wrappers to wrap into promises
|
||||
client.saveFileAsync = (path, destination) =>
|
||||
new Promise((resolve, reject) => {
|
||||
client.saveFile(prefix + path, destination, callback(resolve, reject, true))
|
||||
})
|
||||
|
||||
client.postAsync = (path, data) =>
|
||||
new Promise((resolve, reject) => {
|
||||
client.post(prefix + path, data, callback(resolve, reject))
|
||||
})
|
||||
|
||||
client.putAsync = (path, data) =>
|
||||
new Promise((resolve, reject) => {
|
||||
client.put(prefix + path, data, callback(resolve, reject))
|
||||
})
|
||||
|
||||
client.deleteAsync = (path) =>
|
||||
new Promise((resolve, reject) => {
|
||||
client.del(prefix + path, callback(resolve, reject))
|
||||
})
|
||||
|
||||
client.sendFileAsync = (path, files, data) =>
|
||||
new Promise((resolve, reject) => {
|
||||
client.sendFile(prefix + path, files, data || {}, callback(resolve, reject))
|
||||
})
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
*/
|
|
@ -1,12 +1,12 @@
|
|||
// import _ from 'lodash'
|
||||
// import sinon from 'sinon'
|
||||
import server from '../server'
|
||||
import client from './helper.client'
|
||||
import defaults from '../api/defaults'
|
||||
import Client from './helper.client.mjs'
|
||||
import defaults from '../api/defaults.mjs'
|
||||
import '../api/server.mjs'
|
||||
|
||||
after(() => server.close())
|
||||
|
||||
export const createClient = client
|
||||
export function createClient() {
|
||||
return new Client()
|
||||
}
|
||||
|
||||
export function createContext(opts) {
|
||||
return defaults(opts, {
|
|
@ -1,61 +0,0 @@
|
|||
import assert from 'assert-extended'
|
||||
import sinon from 'sinon'
|
||||
|
||||
describe('jwt', () => {
|
||||
const jsonwebtoken = require('jsonwebtoken')
|
||||
const jwt = require('../api/jwt')
|
||||
|
||||
describe('#sign', () => {
|
||||
it('should call security correctly', () => {
|
||||
let token = jwt.sign({ a: 1 }, 'asdf')
|
||||
assert.ok(token)
|
||||
|
||||
let decoded = jsonwebtoken.decode(token)
|
||||
assert.strictEqual(decoded.a, 1)
|
||||
})
|
||||
|
||||
it('should support custom secret', done => {
|
||||
const assertSecret = 'sdfagsda'
|
||||
let token = jwt.sign({ a: 1 }, assertSecret)
|
||||
|
||||
jsonwebtoken.verify(token, assertSecret, done)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#decode()', () => {
|
||||
it('should decode correctly', () => {
|
||||
let data = { a: 1, b: 2 }
|
||||
let token = jwt.sign(data, 'asdf')
|
||||
let decoded = jwt.decode(token)
|
||||
|
||||
assert.strictEqual(decoded.a, data.a)
|
||||
assert.strictEqual(decoded.b, data.b)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#verify', () => {
|
||||
it('should verify correctly', () => {
|
||||
const assertSecret = 'asdfasdf'
|
||||
const assertResult = 23532
|
||||
let token = jwt.sign({ a: assertResult }, assertSecret)
|
||||
|
||||
return assert.isFulfilled(jwt.verify(token, assertSecret))
|
||||
.then(data => assert.strictEqual(data.a, assertResult))
|
||||
})
|
||||
|
||||
it('should fail if secret does not match', () => {
|
||||
const assertSecret = 'asdfasdf'
|
||||
let token = jwt.sign({ a: 1 }, assertSecret)
|
||||
|
||||
return assert.isRejected(jwt.verify(token, assertSecret + 'a'))
|
||||
.then(err => assert.match(err.message, /[Ss]ignature/))
|
||||
})
|
||||
|
||||
it('should fail token has been mishandled', () => {
|
||||
let token = jwt.sign({ a: 1 }, 'asdf')
|
||||
|
||||
return assert.isRejected(jwt.verify(token + 'a', 'asdf'))
|
||||
.then(err => assert.match(err.message, /[Ss]ignature/))
|
||||
})
|
||||
})
|
||||
})
|
72
test/jwt/decode.test.mjs
Normal file
72
test/jwt/decode.test.mjs
Normal file
|
@ -0,0 +1,72 @@
|
|||
import { Eltro as t, assert} from 'eltro'
|
||||
import decode from '../../api/jwt/decode.mjs'
|
||||
|
||||
const pubKeys = {}
|
||||
const audiences = []
|
||||
|
||||
const testJwt =
|
||||
'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ.eyJhdWQiOiJodHRwczovL2hvc3Qvb2F1dGgvdG9rZW4iLCJpc3MiOiJ0ZXN0QHRlc3QuY29tIiwiaWF0IjoxNTAzMzM1MTY5LCJleHAiOjE1MDMzMzU3NjksInNjb3BlIjpbImh0dHA6Ly9zdHVmZiIsImh0dHA6Ly9zdHVmZjIiXX0.zO278VV6NzwsvBrAIc15mOfwza-FkmLCV28NRXnrI550xw1S1145cS1UsZP5zXxcrk5f4oEgB91Jt6ble76yK5nU68fALUXtfe7xPUkhcOUIw92q_x_Iaaw4z6a71NtyishCfJlbmwkXXEq5YCVAvX3KNDtyPf_fQrAqjzsbgQc'
|
||||
|
||||
const testJwtWrongAlg =
|
||||
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzEyOCJ9.eyJhdWQiOiJodHRwczovL2hvc3Qvb2F1dGgvdG9rZW4iLCJpc3MiOiJ0ZXN0QHRlc3QuY29tIiwiaWF0IjoxNTAzMzM2NzU5LCJleHAiOjE1MDMzMzczNTksInNjb3BlIjpbImh0dHA6Ly9zdHVmZiIsImh0dHA6Ly9zdHVmZjIiXX0.12co2gXwBxmZ2uLJecd26bfteCLBx7jgu_9rp2hhKAHWA4qFKm1HcQOZXqDvHkjflQDtNAQ1ZUUf3U8kntUUAmMOjhHx0BspC-xuaTFylZWqj--A2_w9e7JSk46TF_x3e_hZLB3rtyuSEAPMh_nOCsmM-4A2fnQx0Y5p-Bwbt0I'
|
||||
|
||||
t.describe('jwtUtils', function() {
|
||||
t.describe('decode', function() {
|
||||
t.test('invalid jwt input', function() {
|
||||
assert.throws(
|
||||
function() {
|
||||
decode({}, pubKeys, audiences)
|
||||
},
|
||||
/jwt needs to a string/
|
||||
)
|
||||
})
|
||||
t.test('invalid pubKeys input', function() {
|
||||
assert.throws(
|
||||
function() {
|
||||
decode(testJwt, [], audiences)
|
||||
},
|
||||
/publicKeys needs to be a map of { issuer: { keyid: "PEM encoded key" }/
|
||||
)
|
||||
})
|
||||
t.test('invalid audiences input', function() {
|
||||
assert.throws(
|
||||
function() {
|
||||
decode(testJwt, pubKeys, '')
|
||||
},
|
||||
/audiences needs to be an array of allowed audiences/
|
||||
)
|
||||
})
|
||||
t.test('invalid options input', function() {
|
||||
assert.throws(
|
||||
function() {
|
||||
decode(testJwt, pubKeys, audiences, '')
|
||||
},
|
||||
/options needs to a map of { nbfIatSkew: 300, ... }/
|
||||
)
|
||||
})
|
||||
t.test('too few spaces', function() {
|
||||
assert.throws(
|
||||
function() {
|
||||
decode('hello.test', pubKeys, audiences)
|
||||
},
|
||||
/JWT does not contain 3 dots/
|
||||
)
|
||||
})
|
||||
t.test('invalid json', function() {
|
||||
assert.throws(
|
||||
function() {
|
||||
decode(testJwt.substr(10), pubKeys, audiences)
|
||||
},
|
||||
/Unexpected token \$ in JSON at position 0/
|
||||
)
|
||||
})
|
||||
t.test('wrong alg', function() {
|
||||
assert.throws(
|
||||
function() {
|
||||
decode(testJwtWrongAlg, pubKeys, audiences)
|
||||
},
|
||||
/Only alg HS256, HS384 and HS512 are supported/
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
21
test/jwt/encode.test.mjs
Normal file
21
test/jwt/encode.test.mjs
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { Eltro as t, assert} from 'eltro'
|
||||
import encode from '../../api/jwt/encode.mjs'
|
||||
|
||||
t.describe('encode', function() {
|
||||
t.test('should faile with invalid header and body', function() {
|
||||
assert.throws(
|
||||
function() {
|
||||
encode('', '')
|
||||
},
|
||||
/both header and body should be of type object/
|
||||
)
|
||||
})
|
||||
t.test('should faile with empty header and body', function() {
|
||||
assert.throws(
|
||||
function() {
|
||||
encode({}, {})
|
||||
},
|
||||
/Only alg HS256, HS384 and HS512 are supported/
|
||||
)
|
||||
})
|
||||
})
|
343
test/jwt/jwt.test.mjs
Normal file
343
test/jwt/jwt.test.mjs
Normal file
|
@ -0,0 +1,343 @@
|
|||
import { Eltro as t, assert} from 'eltro'
|
||||
import encode from '../../api/jwt/encode.mjs'
|
||||
import decode from '../../api/jwt/decode.mjs'
|
||||
import defaults from '../../api/defaults.mjs'
|
||||
|
||||
const unixNow = Math.floor(Date.now() / 1000)
|
||||
const jwtHeader = {
|
||||
typ: 'JWT',
|
||||
alg: 'HS256',
|
||||
kid: '2'
|
||||
}
|
||||
|
||||
const jwtBody = {
|
||||
aud: 'https://host/oauth/token',
|
||||
iss: 'test@test.com',
|
||||
iat: unixNow,
|
||||
exp: unixNow + 600,
|
||||
scope: ['http://stuff', 'http://stuff2']
|
||||
}
|
||||
|
||||
const pubKeys = {
|
||||
'test@test.com': {
|
||||
'default@HS256': 'sharedkey',
|
||||
'2@HS256': 'sharedkey',
|
||||
'2@HS384': 'sharedkey',
|
||||
'2@HS512': 'sharedkey',
|
||||
'5@HS256': 'wrongkey'
|
||||
},
|
||||
'test@custom.com': {
|
||||
'2@HS256': {
|
||||
key: 'sharedkey',
|
||||
expiresSkew: 600,
|
||||
expiresMax: 86400
|
||||
},
|
||||
'3@HS256': {
|
||||
},
|
||||
'4@HS256': [],
|
||||
'5@HS256': { key: 'sharedkey' },
|
||||
},
|
||||
}
|
||||
|
||||
t.describe('encode/decode', function() {
|
||||
['HS256', 'HS384', 'HS512'].forEach(function(algo) {
|
||||
t.test('success with ' + algo, function() {
|
||||
let customJwtHeader = defaults(jwtHeader)
|
||||
customJwtHeader.kid = '2'
|
||||
customJwtHeader.alg = algo
|
||||
let jwt = encode(customJwtHeader, jwtBody, 'sharedkey')
|
||||
let decodedJwtBody = decode(jwt, pubKeys, [
|
||||
'https://host/oauth/token'
|
||||
])
|
||||
assert.deepStrictEqual(jwtBody, decodedJwtBody)
|
||||
})
|
||||
})
|
||||
t.test('success without kid', function() {
|
||||
let customJwtHeader = defaults(jwtHeader)
|
||||
delete customJwtHeader.kid
|
||||
let jwt = encode(customJwtHeader, jwtBody, 'sharedkey')
|
||||
let decodedJwtBody = decode(jwt, pubKeys, [
|
||||
'https://host/oauth/token'
|
||||
])
|
||||
assert.deepStrictEqual(jwtBody, decodedJwtBody)
|
||||
})
|
||||
t.test('success with object key', function() {
|
||||
let customJwtBody = defaults(jwtBody)
|
||||
customJwtBody.kid = '5'
|
||||
customJwtBody.iss = 'test@custom.com'
|
||||
let jwt = encode(jwtHeader, jwtBody, 'sharedkey')
|
||||
let decodedJwtBody = decode(jwt, pubKeys, [
|
||||
'https://host/oauth/token'
|
||||
])
|
||||
assert.deepStrictEqual(jwtBody, decodedJwtBody)
|
||||
})
|
||||
t.test('success with array aud', function() {
|
||||
let customJwtBody = defaults(jwtBody)
|
||||
customJwtBody.aud = [
|
||||
'https://myhost/oauth/token',
|
||||
'https://host/oauth/token'
|
||||
]
|
||||
let jwt = encode(jwtHeader, customJwtBody, 'sharedkey')
|
||||
let decodedJwtBody = decode(jwt, pubKeys, [
|
||||
'https://host/oauth/token'
|
||||
])
|
||||
assert.deepStrictEqual(customJwtBody, decodedJwtBody)
|
||||
})
|
||||
t.test('success with expired token', function() {
|
||||
let customJwtBody = defaults(jwtBody)
|
||||
customJwtBody.iss = 'test@custom.com'
|
||||
customJwtBody.exp -= 600
|
||||
let jwt = encode(jwtHeader, customJwtBody, 'sharedkey')
|
||||
let decodedJwtBody = decode(jwt, pubKeys, [
|
||||
'https://host/oauth/token'
|
||||
])
|
||||
assert.deepStrictEqual(customJwtBody, decodedJwtBody)
|
||||
})
|
||||
t.test('token outside maximum expires', function() {
|
||||
let customJwtBody = defaults(jwtBody)
|
||||
customJwtBody.iss = 'test@custom.com'
|
||||
customJwtBody.exp += 172800
|
||||
let jwt = encode(jwtHeader, customJwtBody, 'sharedkey')
|
||||
assert.throws(
|
||||
function() {
|
||||
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
||||
},
|
||||
/Expires in the future by more than 86400 seconds/
|
||||
)
|
||||
})
|
||||
t.test('token outside maximum expires using decode options', function() {
|
||||
let customJwtBody = defaults(jwtBody)
|
||||
customJwtBody.exp += 172800
|
||||
let jwt = encode(jwtHeader, customJwtBody, 'sharedkey')
|
||||
assert.throws(
|
||||
function() {
|
||||
decode(jwt, pubKeys, ['https://host/oauth/token'], {
|
||||
expiresMax: 600
|
||||
})
|
||||
},
|
||||
/Expires in the future by more than 600 seconds/
|
||||
)
|
||||
})
|
||||
t.test('token outside maximum expires using nbf', function() {
|
||||
let customJwtBody = defaults(jwtBody)
|
||||
customJwtBody.exp += 172800
|
||||
customJwtBody.nbf = customJwtBody.iat
|
||||
delete customJwtBody.iat
|
||||
let jwt = encode(jwtHeader, customJwtBody, 'sharedkey')
|
||||
assert.throws(
|
||||
function() {
|
||||
decode(jwt, pubKeys, ['https://host/oauth/token'], {
|
||||
expiresMax: 600
|
||||
})
|
||||
},
|
||||
/Expires in the future by more than 600 seconds/
|
||||
)
|
||||
})
|
||||
t.test('token outside maximum expires using unixNow', function() {
|
||||
let customJwtBody = defaults(jwtBody)
|
||||
customJwtBody.exp += 172800
|
||||
delete customJwtBody.iat
|
||||
let jwt = encode(jwtHeader, customJwtBody, 'sharedkey')
|
||||
assert.throws(
|
||||
function() {
|
||||
decode(jwt, pubKeys, ['https://host/oauth/token'], {
|
||||
expiresMax: 600
|
||||
})
|
||||
},
|
||||
/Expires in the future by more than 600 seconds/
|
||||
)
|
||||
})
|
||||
t.test('unknown aud', function() {
|
||||
let jwt = encode(jwtHeader, jwtBody, 'sharedkey')
|
||||
assert.throws(
|
||||
function() {
|
||||
decode(jwt, pubKeys, ['https://myhost/oauth/token'])
|
||||
},
|
||||
/Unknown audience 'https:\/\/host\/oauth\/token'/
|
||||
)
|
||||
})
|
||||
t.test('expired', function() {
|
||||
let customJwtBody = defaults(jwtBody)
|
||||
customJwtBody.iat -= 1200
|
||||
customJwtBody.exp -= 800
|
||||
let jwt = encode(jwtHeader, customJwtBody, 'sharedkey')
|
||||
assert.throws(
|
||||
function() {
|
||||
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
||||
},
|
||||
/Token has expired/
|
||||
)
|
||||
})
|
||||
t.test('missing exp', function() {
|
||||
let customJwtBody = defaults(jwtBody)
|
||||
delete customJwtBody.exp
|
||||
let jwt = encode(jwtHeader, customJwtBody, 'sharedkey')
|
||||
assert.throws(
|
||||
function() {
|
||||
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
||||
},
|
||||
/No expires set on token/
|
||||
)
|
||||
})
|
||||
t.test('missing iss', function() {
|
||||
let customJwtBody = defaults(jwtBody)
|
||||
delete customJwtBody.iss
|
||||
let jwt = encode(jwtHeader, customJwtBody, 'sharedkey')
|
||||
assert.throws(
|
||||
function() {
|
||||
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
||||
},
|
||||
/No issuer set/
|
||||
)
|
||||
})
|
||||
t.test('iat invalid', function() {
|
||||
let customJwtBody = defaults(jwtBody)
|
||||
customJwtBody.iat += 1200
|
||||
let jwt = encode(jwtHeader, customJwtBody, 'sharedkey')
|
||||
assert.throws(
|
||||
function() {
|
||||
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
||||
},
|
||||
/Issued at in the future by more than 300 seconds/
|
||||
)
|
||||
})
|
||||
t.test('nbf invalid', function() {
|
||||
let customJwtBody = defaults(jwtBody)
|
||||
customJwtBody.nbf = customJwtBody.iat + 1200
|
||||
let jwt = encode(jwtHeader, customJwtBody, 'sharedkey')
|
||||
assert.throws(
|
||||
function() {
|
||||
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
||||
},
|
||||
/Not before in the future by more than 300 seconds/
|
||||
)
|
||||
})
|
||||
t.test('unknown issuer', function() {
|
||||
let customJwtBody = defaults(jwtBody)
|
||||
customJwtBody.iss = 'unknown@test.com'
|
||||
let jwt = encode(jwtHeader, customJwtBody, 'sharedkey')
|
||||
assert.throws(
|
||||
function() {
|
||||
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
||||
},
|
||||
/Unknown issuer 'unknown@test.com'/
|
||||
)
|
||||
})
|
||||
t.test('wrong alg', function() {
|
||||
let customJwtHeader = defaults(jwtHeader)
|
||||
customJwtHeader.alg = 'HS128'
|
||||
assert.throws(
|
||||
function() {
|
||||
encode(customJwtHeader, jwtBody, 'sharedkey')
|
||||
},
|
||||
/Only alg HS256, HS384 and HS512 are supported/
|
||||
)
|
||||
})
|
||||
t.test('unknown kid', function() {
|
||||
let customJwtHeader = defaults(jwtHeader)
|
||||
customJwtHeader.kid = '3'
|
||||
let jwt = encode(customJwtHeader, jwtBody, 'sharedkey')
|
||||
assert.throws(
|
||||
function() {
|
||||
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
||||
},
|
||||
/Unknown pubkey id '3' for this issuer/
|
||||
)
|
||||
})
|
||||
t.test('invalid signature', function() {
|
||||
let customJwtHeader = defaults(jwtHeader)
|
||||
customJwtHeader.kid = '2'
|
||||
let jwt = encode(customJwtHeader, jwtBody, 'sharedkey')
|
||||
let backup = jwt
|
||||
if (jwt[jwt.length - 2] === 'A') {
|
||||
jwt = jwt.slice(0, -2) + 'BB'
|
||||
} else {
|
||||
jwt = jwt.slice(0, -2) + 'AA'
|
||||
}
|
||||
try {
|
||||
assert.throws(
|
||||
function() {
|
||||
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
||||
},
|
||||
/Verification failed with alg 'HS256'/
|
||||
)
|
||||
} catch (err) {
|
||||
console.log('-----')
|
||||
console.log(backup)
|
||||
console.log(jwt)
|
||||
throw err
|
||||
}
|
||||
jwt = encode(customJwtHeader, jwtBody, 'sharedkey')
|
||||
jwt += 'a'
|
||||
assert.throws(
|
||||
function() {
|
||||
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
||||
},
|
||||
/Verification failed with alg 'HS256'/
|
||||
)
|
||||
})
|
||||
t.test('invalid shared key', function() {
|
||||
let customJwtHeader = defaults(jwtHeader)
|
||||
customJwtHeader.kid = '5'
|
||||
customJwtHeader.alg = 'HS256'
|
||||
let jwt = encode(customJwtHeader, jwtBody, 'sharedkey')
|
||||
assert.throws(
|
||||
function() {
|
||||
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
||||
},
|
||||
/Verification failed with alg 'HS256'/
|
||||
)
|
||||
})
|
||||
t.test('invalid pubkey', function() {
|
||||
let customJwtHeader = defaults(jwtHeader)
|
||||
customJwtHeader.kid = '4'
|
||||
let jwt = encode(customJwtHeader, jwtBody, 'sharedkey')
|
||||
assert.throws(
|
||||
function() {
|
||||
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
||||
}
|
||||
)
|
||||
let customJwtBody = defaults(jwtBody)
|
||||
customJwtBody.iss = 'test@custom.com'
|
||||
customJwtHeader.kid = '3'
|
||||
jwt = encode(customJwtHeader, customJwtBody, 'sharedkey')
|
||||
assert.throws(
|
||||
function() {
|
||||
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
||||
},
|
||||
/'3'.+misconfigured/
|
||||
)
|
||||
customJwtHeader.kid = '4'
|
||||
jwt = encode(customJwtHeader, customJwtBody, 'sharedkey')
|
||||
assert.throws(
|
||||
function() {
|
||||
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
||||
},
|
||||
/'4'.+misconfigured/
|
||||
)
|
||||
})
|
||||
t.test('success with broken token', function() {
|
||||
let expectedJwtBody = {
|
||||
id: 1,
|
||||
exp: 1519802991,
|
||||
iat: 1519802691,
|
||||
iss: 'test@test.com',
|
||||
aud: 'https://host/oauth/token'
|
||||
}
|
||||
let decodedJwtBody = decode(
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNTE5ODAyNjkxfQ.p6t378Ri2JpOCm9WtC36ttyH8ILzG9-OWT_kgMrrRfo',
|
||||
pubKeys,
|
||||
['https://host/oauth/token'],
|
||||
{
|
||||
fixup: (header, body) => {
|
||||
header.kid = '2'
|
||||
body.iss = 'test@test.com'
|
||||
body.aud = 'https://host/oauth/token'
|
||||
body.exp = body.iat + 300
|
||||
},
|
||||
expiresSkew: 307584000
|
||||
}
|
||||
)
|
||||
assert.deepStrictEqual(decodedJwtBody, expectedJwtBody)
|
||||
})
|
||||
})
|
|
@ -1,22 +0,0 @@
|
|||
import assert from 'assert-extended'
|
||||
|
||||
import * as server from './helper.server'
|
||||
|
||||
describe('Server', () => {
|
||||
let client
|
||||
|
||||
beforeEach(() => {
|
||||
client = server.createClient()
|
||||
})
|
||||
|
||||
it('should run', () =>
|
||||
assert.isFulfilled(
|
||||
client.getAsync('/')
|
||||
)
|
||||
.then(data => {
|
||||
assert.ok(data)
|
||||
assert.ok(data.name)
|
||||
assert.ok(data.version)
|
||||
})
|
||||
)
|
||||
})
|
28
test/server.test.mjs
Normal file
28
test/server.test.mjs
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { Eltro as t, assert} from 'eltro'
|
||||
|
||||
import * as server from './helper.server.mjs'
|
||||
|
||||
t.describe('Server', function() {
|
||||
let client
|
||||
|
||||
t.before(function() {
|
||||
client = server.createClient()
|
||||
})
|
||||
|
||||
t.test('should run', async function() {
|
||||
let data = await client.get('/')
|
||||
|
||||
assert.ok(data)
|
||||
assert.ok(data.name)
|
||||
assert.ok(data.version)
|
||||
})
|
||||
|
||||
t.test('should handle errors fine', async function() {
|
||||
let data = await assert.isRejected(client.get('/error'))
|
||||
|
||||
assert.ok(data)
|
||||
assert.ok(data.body)
|
||||
assert.strictEqual(data.body.status, 422)
|
||||
assert.match(data.body.message, /test/)
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue