Lots of development and cleanup of dependencies

This commit is contained in:
Jonatan Nilsson 2021-10-09 00:11:52 +00:00
parent dc462a3d53
commit 2b53b48a5d
8 changed files with 145 additions and 139 deletions

View file

@ -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(',')}'`)

View file

@ -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)))

View file

@ -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
} }

View file

@ -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() {

View file

@ -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() {

View file

@ -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(

View file

@ -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)
})
})
})

View 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')
})
})