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')
|
||||
}
|
||||
|
||||
let header = JSON.parse(base64UrlSafe.decode(parts[0]).toString('utf8'))
|
||||
let body = JSON.parse(base64UrlSafe.decode(parts[1]).toString('utf8'))
|
||||
let header
|
||||
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') {
|
||||
options.fixup(header, body)
|
||||
}
|
||||
|
@ -85,7 +91,7 @@ const defaultOptions = {
|
|||
|
||||
if (!pubkeyOrSharedKey) {
|
||||
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') {
|
||||
throw new Error(
|
||||
|
@ -147,6 +153,8 @@ function validateIssuedAt(body, unixNow, options) {
|
|||
}
|
||||
|
||||
function validateAudience(body, audiences, options) {
|
||||
if (!body.aud && audiences.length === 0) return
|
||||
|
||||
let auds = Array.isArray(body.aud) ? body.aud : [body.aud]
|
||||
if (!auds.some(aud => audiences.includes(aud))) {
|
||||
throw new Error(`Unknown audience '${auds.join(',')}'`)
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import crypto from 'crypto'
|
||||
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 (
|
||||
typeof header !== 'object' ||
|
||||
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
|
||||
let headerBase64 = base64UrlSafe.encode(Buffer.from(JSON.stringify(header)))
|
||||
let bodyBase64 = base64UrlSafe.encode(Buffer.from(JSON.stringify(body)))
|
||||
|
|
|
@ -1,21 +1,13 @@
|
|||
// import * as jwt from '../jwt.mjs'
|
||||
import decode from '../jwt/decode.mjs'
|
||||
import config from '../config.mjs'
|
||||
|
||||
export async function verifyToken(ctx) {
|
||||
export function verifyToken(ctx) {
|
||||
if (!ctx.query.token) {
|
||||
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) {
|
||||
throw new Error('Token is invalid')
|
||||
}
|
||||
|
||||
let output = await jwt.verify(
|
||||
ctx.query.token,
|
||||
config.get(`sites:${decoded.site}`)
|
||||
)*/
|
||||
|
||||
return output.site
|
||||
return decoded.iss
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ t.describe('jwtUtils', function() {
|
|||
function() {
|
||||
decode(testJwt.substr(10), pubKeys, audiences)
|
||||
},
|
||||
/Unexpected token \$ in JSON at position 0/
|
||||
/Token was invalid/
|
||||
)
|
||||
})
|
||||
t.test('wrong alg', function() {
|
||||
|
|
|
@ -61,6 +61,22 @@ t.describe('encode/decode', function() {
|
|||
])
|
||||
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() {
|
||||
let customJwtBody = defaults(jwtBody)
|
||||
customJwtBody.kid = '5'
|
||||
|
@ -83,6 +99,13 @@ t.describe('encode/decode', function() {
|
|||
])
|
||||
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() {
|
||||
let customJwtBody = defaults(jwtBody)
|
||||
customJwtBody.iss = 'test@custom.com'
|
||||
|
@ -168,10 +191,18 @@ t.describe('encode/decode', function() {
|
|||
/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() {
|
||||
let customJwtBody = defaults(jwtBody)
|
||||
delete customJwtBody.exp
|
||||
let jwt = encode(jwtHeader, customJwtBody, 'sharedkey')
|
||||
let jwt = encode(jwtHeader, customJwtBody, 'sharedkey', true)
|
||||
assert.throws(
|
||||
function() {
|
||||
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
||||
|
@ -241,7 +272,7 @@ t.describe('encode/decode', function() {
|
|||
function() {
|
||||
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
||||
},
|
||||
/Unknown pubkey id '3' for this issuer/
|
||||
/Unknown pubkey id '3'/
|
||||
)
|
||||
})
|
||||
t.test('invalid signature', function() {
|
||||
|
|
|
@ -1,38 +1,38 @@
|
|||
import { Eltro as t, assert} from 'eltro'
|
||||
import fs from 'fs'
|
||||
import assert from 'assert'
|
||||
import sinon from 'sinon'
|
||||
import 'assert-extended'
|
||||
import appRoot from 'app-root-path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import path from '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 config = require('../../config')
|
||||
let jwt = require('../../api/jwt')
|
||||
let __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
console.log(__dirname)
|
||||
|
||||
t.describe('Media (API)', () => {
|
||||
let testFile
|
||||
let client
|
||||
let client = createClient()
|
||||
|
||||
before(() => {
|
||||
t.before(() => {
|
||||
config.set('sites', {
|
||||
development: 'hello-world'
|
||||
})
|
||||
})
|
||||
|
||||
after(done => {
|
||||
t.after(done => {
|
||||
if (testFile) {
|
||||
return fs.unlink(appRoot.resolve(`/public${testFile}`), done)
|
||||
// path.resolve(path.join(__dirname, 'fixtures', file))
|
||||
return fs.unlink( appRoot.resolve(`/public${testFile}`), done)
|
||||
}
|
||||
done()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
client = createClient()
|
||||
})
|
||||
|
||||
describe('POST /media', function temp() {
|
||||
t.describe('POST /media', function temp() {
|
||||
this.timeout(10000)
|
||||
|
||||
it('should require authentication', async () => {
|
||||
t.test('should require authentication', async () => {
|
||||
let err = await assert.isRejected(
|
||||
client.sendFileAsync('/media',
|
||||
appRoot.resolve('/test/media/test.png')))
|
||||
|
@ -41,7 +41,7 @@ describe('Media (API)', () => {
|
|||
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 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