Lots of development and cleanup of dependencies
This commit is contained in:
parent
dc462a3d53
commit
2b53b48a5d
8 changed files with 145 additions and 139 deletions
|
@ -44,8 +44,14 @@ const defaultOptions = {
|
||||||
throw new Error('JWT does not contain 3 dots')
|
throw new Error('JWT does not contain 3 dots')
|
||||||
}
|
}
|
||||||
|
|
||||||
let header = JSON.parse(base64UrlSafe.decode(parts[0]).toString('utf8'))
|
let header
|
||||||
let body = JSON.parse(base64UrlSafe.decode(parts[1]).toString('utf8'))
|
let body
|
||||||
|
try {
|
||||||
|
header = JSON.parse(base64UrlSafe.decode(parts[0]).toString('utf8'))
|
||||||
|
body = JSON.parse(base64UrlSafe.decode(parts[1]).toString('utf8'))
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error('Token was invalid')
|
||||||
|
}
|
||||||
if (typeof options.fixup === 'function') {
|
if (typeof options.fixup === 'function') {
|
||||||
options.fixup(header, body)
|
options.fixup(header, body)
|
||||||
}
|
}
|
||||||
|
@ -85,7 +91,7 @@ const defaultOptions = {
|
||||||
|
|
||||||
if (!pubkeyOrSharedKey) {
|
if (!pubkeyOrSharedKey) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unknown pubkey id '${header.kid}' for this issuer`
|
`Unknown pubkey id '${header.kid || 'default'}' on this issuer`
|
||||||
)
|
)
|
||||||
} else if ((typeof(pubkeyOrSharedKey) !== 'object' || Array.isArray(pubkeyOrSharedKey)) && typeof(pubkeyOrSharedKey) !== 'string') {
|
} else if ((typeof(pubkeyOrSharedKey) !== 'object' || Array.isArray(pubkeyOrSharedKey)) && typeof(pubkeyOrSharedKey) !== 'string') {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@ -147,6 +153,8 @@ function validateIssuedAt(body, unixNow, options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateAudience(body, audiences, options) {
|
function validateAudience(body, audiences, options) {
|
||||||
|
if (!body.aud && audiences.length === 0) return
|
||||||
|
|
||||||
let auds = Array.isArray(body.aud) ? body.aud : [body.aud]
|
let auds = Array.isArray(body.aud) ? body.aud : [body.aud]
|
||||||
if (!auds.some(aud => audiences.includes(aud))) {
|
if (!auds.some(aud => audiences.includes(aud))) {
|
||||||
throw new Error(`Unknown audience '${auds.join(',')}'`)
|
throw new Error(`Unknown audience '${auds.join(',')}'`)
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import crypto from 'crypto'
|
import crypto from 'crypto'
|
||||||
import * as base64UrlSafe from './base64urlsafe.mjs'
|
import * as base64UrlSafe from './base64urlsafe.mjs'
|
||||||
|
import defaults from '../defaults.mjs'
|
||||||
|
|
||||||
export default function encode(header, body, privateKeyPassword = null) {
|
export default function encode(header, orgBody, privateKeyPassword = null, disableAutofill = false) {
|
||||||
|
let body = defaults(orgBody)
|
||||||
if (
|
if (
|
||||||
typeof header !== 'object' ||
|
typeof header !== 'object' ||
|
||||||
Array.isArray(header) ||
|
Array.isArray(header) ||
|
||||||
|
@ -28,6 +30,13 @@ export default function encode(header, body, privateKeyPassword = null) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!body.iat && !disableAutofill) {
|
||||||
|
body.iat = Math.floor(Date.now() / 1000)
|
||||||
|
}
|
||||||
|
if (!body.exp && !disableAutofill) {
|
||||||
|
body.exp = body.iat + 300
|
||||||
|
}
|
||||||
|
|
||||||
// Base64 encode header and body
|
// Base64 encode header and body
|
||||||
let headerBase64 = base64UrlSafe.encode(Buffer.from(JSON.stringify(header)))
|
let headerBase64 = base64UrlSafe.encode(Buffer.from(JSON.stringify(header)))
|
||||||
let bodyBase64 = base64UrlSafe.encode(Buffer.from(JSON.stringify(body)))
|
let bodyBase64 = base64UrlSafe.encode(Buffer.from(JSON.stringify(body)))
|
||||||
|
|
|
@ -1,21 +1,13 @@
|
||||||
// import * as jwt from '../jwt.mjs'
|
// import * as jwt from '../jwt.mjs'
|
||||||
|
import decode from '../jwt/decode.mjs'
|
||||||
import config from '../config.mjs'
|
import config from '../config.mjs'
|
||||||
|
|
||||||
export async function verifyToken(ctx) {
|
export function verifyToken(ctx) {
|
||||||
if (!ctx.query.token) {
|
if (!ctx.query.token) {
|
||||||
throw new Error('Token is missing in query')
|
throw new Error('Token is missing in query')
|
||||||
}
|
}
|
||||||
|
|
||||||
/*let decoded = jwt.decode(ctx.query.token)
|
let decoded = decode(ctx.query.token, config.get('sites'), [])
|
||||||
|
|
||||||
if (!decoded || !decoded.site) {
|
return decoded.iss
|
||||||
throw new Error('Token is invalid')
|
|
||||||
}
|
|
||||||
|
|
||||||
let output = await jwt.verify(
|
|
||||||
ctx.query.token,
|
|
||||||
config.get(`sites:${decoded.site}`)
|
|
||||||
)*/
|
|
||||||
|
|
||||||
return output.site
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,7 @@ t.describe('jwtUtils', function() {
|
||||||
function() {
|
function() {
|
||||||
decode(testJwt.substr(10), pubKeys, audiences)
|
decode(testJwt.substr(10), pubKeys, audiences)
|
||||||
},
|
},
|
||||||
/Unexpected token \$ in JSON at position 0/
|
/Token was invalid/
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
t.test('wrong alg', function() {
|
t.test('wrong alg', function() {
|
||||||
|
|
|
@ -61,6 +61,22 @@ t.describe('encode/decode', function() {
|
||||||
])
|
])
|
||||||
assert.deepStrictEqual(jwtBody, decodedJwtBody)
|
assert.deepStrictEqual(jwtBody, decodedJwtBody)
|
||||||
})
|
})
|
||||||
|
t.test('success without iat or iss', function() {
|
||||||
|
let customJwtBody = defaults(jwtBody)
|
||||||
|
delete customJwtBody.iat
|
||||||
|
delete customJwtBody.exp
|
||||||
|
let jwt = encode(jwtHeader, customJwtBody, 'sharedkey')
|
||||||
|
let decodedJwtBody = decode(jwt, pubKeys, [
|
||||||
|
'https://host/oauth/token'
|
||||||
|
])
|
||||||
|
assert.ok(decodedJwtBody.iat)
|
||||||
|
assert.ok(decodedJwtBody.iat >= unixNow)
|
||||||
|
assert.ok(decodedJwtBody.iat < unixNow + 2)
|
||||||
|
assert.ok(decodedJwtBody.exp)
|
||||||
|
assert.ok(decodedJwtBody.exp >= unixNow)
|
||||||
|
assert.ok(decodedJwtBody.exp > unixNow + 200)
|
||||||
|
assert.ok(decodedJwtBody.exp < unixNow + 400)
|
||||||
|
})
|
||||||
t.test('success with object key', function() {
|
t.test('success with object key', function() {
|
||||||
let customJwtBody = defaults(jwtBody)
|
let customJwtBody = defaults(jwtBody)
|
||||||
customJwtBody.kid = '5'
|
customJwtBody.kid = '5'
|
||||||
|
@ -83,6 +99,13 @@ t.describe('encode/decode', function() {
|
||||||
])
|
])
|
||||||
assert.deepStrictEqual(customJwtBody, decodedJwtBody)
|
assert.deepStrictEqual(customJwtBody, decodedJwtBody)
|
||||||
})
|
})
|
||||||
|
t.test('success with no aud and empty audience list', function() {
|
||||||
|
let customJwtBody = defaults(jwtBody)
|
||||||
|
delete customJwtBody.aud
|
||||||
|
let jwt = encode(jwtHeader, customJwtBody, 'sharedkey')
|
||||||
|
let decodedJwtBody = decode(jwt, pubKeys, [])
|
||||||
|
assert.deepStrictEqual(customJwtBody, decodedJwtBody)
|
||||||
|
})
|
||||||
t.test('success with expired token', function() {
|
t.test('success with expired token', function() {
|
||||||
let customJwtBody = defaults(jwtBody)
|
let customJwtBody = defaults(jwtBody)
|
||||||
customJwtBody.iss = 'test@custom.com'
|
customJwtBody.iss = 'test@custom.com'
|
||||||
|
@ -168,10 +191,18 @@ t.describe('encode/decode', function() {
|
||||||
/Token has expired/
|
/Token has expired/
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
t.test('invalid', function() {
|
||||||
|
assert.throws(
|
||||||
|
function() {
|
||||||
|
decode('asdfasdgassdga.asdfasdg.sadfsadfas', pubKeys, ['https://host/oauth/token'])
|
||||||
|
},
|
||||||
|
/Token was invalid/
|
||||||
|
)
|
||||||
|
})
|
||||||
t.test('missing exp', function() {
|
t.test('missing exp', function() {
|
||||||
let customJwtBody = defaults(jwtBody)
|
let customJwtBody = defaults(jwtBody)
|
||||||
delete customJwtBody.exp
|
delete customJwtBody.exp
|
||||||
let jwt = encode(jwtHeader, customJwtBody, 'sharedkey')
|
let jwt = encode(jwtHeader, customJwtBody, 'sharedkey', true)
|
||||||
assert.throws(
|
assert.throws(
|
||||||
function() {
|
function() {
|
||||||
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
||||||
|
@ -241,7 +272,7 @@ t.describe('encode/decode', function() {
|
||||||
function() {
|
function() {
|
||||||
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
||||||
},
|
},
|
||||||
/Unknown pubkey id '3' for this issuer/
|
/Unknown pubkey id '3'/
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
t.test('invalid signature', function() {
|
t.test('invalid signature', function() {
|
||||||
|
|
|
@ -1,38 +1,38 @@
|
||||||
|
import { Eltro as t, assert} from 'eltro'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import assert from 'assert'
|
import { fileURLToPath } from 'url'
|
||||||
import sinon from 'sinon'
|
import path from 'path'
|
||||||
import 'assert-extended'
|
|
||||||
import appRoot from 'app-root-path'
|
|
||||||
|
|
||||||
import createClient from '../helper.client'
|
import createClient from '../helper.client.mjs'
|
||||||
|
import config from '../../api/config.mjs'
|
||||||
|
import jwt from '../../api/jwt.mjs'
|
||||||
|
|
||||||
describe('Media (API)', () => {
|
let __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
let config = require('../../config')
|
|
||||||
let jwt = require('../../api/jwt')
|
console.log(__dirname)
|
||||||
|
|
||||||
|
t.describe('Media (API)', () => {
|
||||||
let testFile
|
let testFile
|
||||||
let client
|
let client = createClient()
|
||||||
|
|
||||||
before(() => {
|
t.before(() => {
|
||||||
config.set('sites', {
|
config.set('sites', {
|
||||||
development: 'hello-world'
|
development: 'hello-world'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
after(done => {
|
t.after(done => {
|
||||||
if (testFile) {
|
if (testFile) {
|
||||||
|
// path.resolve(path.join(__dirname, 'fixtures', file))
|
||||||
return fs.unlink( appRoot.resolve(`/public${testFile}`), done)
|
return fs.unlink( appRoot.resolve(`/public${testFile}`), done)
|
||||||
}
|
}
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
t.describe('POST /media', function temp() {
|
||||||
client = createClient()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('POST /media', function temp() {
|
|
||||||
this.timeout(10000)
|
this.timeout(10000)
|
||||||
|
|
||||||
it('should require authentication', async () => {
|
t.test('should require authentication', async () => {
|
||||||
let err = await assert.isRejected(
|
let err = await assert.isRejected(
|
||||||
client.sendFileAsync('/media',
|
client.sendFileAsync('/media',
|
||||||
appRoot.resolve('/test/media/test.png')))
|
appRoot.resolve('/test/media/test.png')))
|
||||||
|
@ -41,7 +41,7 @@ describe('Media (API)', () => {
|
||||||
assert.match(err.message, /[Tt]oken/)
|
assert.match(err.message, /[Tt]oken/)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should upload file and create file', async () => {
|
t.test('should upload file and create file', async () => {
|
||||||
let token = jwt.sign({ site: 'development' }, 'hello-world')
|
let token = jwt.sign({ site: 'development' }, 'hello-world')
|
||||||
|
|
||||||
let data = await assert.isFulfilled(
|
let data = await assert.isFulfilled(
|
||||||
|
|
|
@ -1,101 +0,0 @@
|
||||||
import assert from 'assert-extended'
|
|
||||||
import sinon from 'sinon'
|
|
||||||
|
|
||||||
import { createContext } from '../helper.server'
|
|
||||||
|
|
||||||
describe('Media (Security)', () => {
|
|
||||||
const security = require('../../api/media/security')
|
|
||||||
const jwt = require('../../api/jwt')
|
|
||||||
const config = require('../../config')
|
|
||||||
|
|
||||||
let sandbox
|
|
||||||
let ctx
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
sandbox = sinon.sandbox.create()
|
|
||||||
config.set('sites', {
|
|
||||||
test: 'secret',
|
|
||||||
})
|
|
||||||
ctx = createContext({
|
|
||||||
query: {
|
|
||||||
token: 'asdf',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
sandbox.restore()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#verifyToken()', () => {
|
|
||||||
let stubVerify
|
|
||||||
let stubDecode
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
stubVerify = sandbox.stub(jwt, 'verify')
|
|
||||||
stubDecode = sandbox.stub(jwt, 'decode').returns({ site: 1 })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should fail if query token is missing', async () => {
|
|
||||||
delete ctx.query.token
|
|
||||||
|
|
||||||
let err = await assert.isRejected(security.verifyToken(ctx))
|
|
||||||
|
|
||||||
assert.ok(err)
|
|
||||||
assert.match(err.message, /[tT]oken/)
|
|
||||||
assert.match(err.message, /[Mm]issing/)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should fail if token is invalid', async () => {
|
|
||||||
const assertToken = 'asdfasdfas'
|
|
||||||
ctx.query.token = assertToken
|
|
||||||
stubDecode.returns(null)
|
|
||||||
|
|
||||||
let err = await assert.isRejected(security.verifyToken(ctx))
|
|
||||||
|
|
||||||
assert.ok(err)
|
|
||||||
assert.ok(stubDecode.called)
|
|
||||||
assert.strictEqual(stubDecode.firstCall.args[0], assertToken)
|
|
||||||
assert.match(err.message, /[tT]oken/)
|
|
||||||
assert.match(err.message, /[Ii]nvalid/)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should fail if token does not have site', async () => {
|
|
||||||
stubDecode.returns({ s: 1 })
|
|
||||||
|
|
||||||
let err = await assert.isRejected(security.verifyToken(ctx))
|
|
||||||
|
|
||||||
assert.ok(err)
|
|
||||||
assert.ok(stubDecode.called)
|
|
||||||
assert.match(err.message, /[tT]oken/)
|
|
||||||
assert.match(err.message, /[Ii]nvalid/)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should fail if secret does not match one in config', async () => {
|
|
||||||
const assertError = new Error('lethal')
|
|
||||||
const assertToken = 'ewgowae'
|
|
||||||
ctx.query.token = assertToken
|
|
||||||
config.set('sites', { herp: 'derp' })
|
|
||||||
|
|
||||||
|
|
||||||
stubDecode.returns({ site: 'herp' })
|
|
||||||
stubVerify.rejects(assertError)
|
|
||||||
|
|
||||||
let err = await assert.isRejected(security.verifyToken(ctx))
|
|
||||||
|
|
||||||
assert.ok(stubVerify.called)
|
|
||||||
assert.strictEqual(err, assertError)
|
|
||||||
assert.strictEqual(stubVerify.firstCall.args[0], assertToken)
|
|
||||||
assert.strictEqual(stubVerify.firstCall.args[1], 'derp')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should otherwise return the site name', async () => {
|
|
||||||
const assertSiteName = 'asdfasdfasdf'
|
|
||||||
stubVerify.resolves({ site: assertSiteName })
|
|
||||||
|
|
||||||
let site = await security.verifyToken(ctx)
|
|
||||||
|
|
||||||
assert.strictEqual(site, assertSiteName)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
67
test/media/security.test.mjs
Normal file
67
test/media/security.test.mjs
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import { Eltro as t, assert} from 'eltro'
|
||||||
|
|
||||||
|
import { createContext } from '../helper.server.mjs'
|
||||||
|
import { verifyToken } from '../../api/media/security.mjs'
|
||||||
|
import encode from '../../api/jwt/encode.mjs'
|
||||||
|
import config from '../../api/config.mjs'
|
||||||
|
|
||||||
|
t.describe('#verifyToken()', function() {
|
||||||
|
t.before(function() {
|
||||||
|
config.set('sites', {
|
||||||
|
justatest: {
|
||||||
|
'default@HS512': 'mysharedkey'
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.test('should fail if query token is missing', function() {
|
||||||
|
let ctx = createContext({ })
|
||||||
|
delete ctx.query.token
|
||||||
|
|
||||||
|
assert.throws(function() { verifyToken(ctx) }, /[Mm]issing/)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.test('should fail if token is invalid', function() {
|
||||||
|
let ctx = createContext({ })
|
||||||
|
ctx.query.token = 'asdfasdgassdga'
|
||||||
|
|
||||||
|
assert.throws(function() { verifyToken(ctx) })
|
||||||
|
|
||||||
|
ctx.query.token = 'asdfasdgassdga.asdfasdg.sadfsadfas'
|
||||||
|
|
||||||
|
assert.throws(function() { verifyToken(ctx) }, /invalid/)
|
||||||
|
|
||||||
|
ctx.query.token = encode(
|
||||||
|
{ typ: 'JWT', alg: 'HS256' },
|
||||||
|
{ iss: 'justatest' },
|
||||||
|
'mysharedkey'
|
||||||
|
)
|
||||||
|
assert.throws(function() { verifyToken(ctx) }, /pubkey/)
|
||||||
|
|
||||||
|
ctx.query.token = encode(
|
||||||
|
{ typ: 'JWT', alg: 'HS512' },
|
||||||
|
{ iss: 'notexist' },
|
||||||
|
'mysharedkey'
|
||||||
|
)
|
||||||
|
assert.throws(function() { verifyToken(ctx) }, /notexist/)
|
||||||
|
|
||||||
|
ctx.query.token = encode(
|
||||||
|
{ typ: 'JWT', alg: 'HS512' },
|
||||||
|
{ iss: 'justatest' },
|
||||||
|
'mysharedkey2'
|
||||||
|
)
|
||||||
|
assert.throws(function() { verifyToken(ctx) }, /HS512/)
|
||||||
|
assert.throws(function() { verifyToken(ctx) }, /[vV]erification/)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.test('should otherwise return the issuer', function() {
|
||||||
|
let ctx = createContext({ })
|
||||||
|
ctx.query.token = encode(
|
||||||
|
{ typ: 'JWT', alg: 'HS512' },
|
||||||
|
{ iss: 'justatest' },
|
||||||
|
'mysharedkey'
|
||||||
|
)
|
||||||
|
let site = verifyToken(ctx)
|
||||||
|
assert.strictEqual(site, 'justatest')
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in a new issue