From 9ec3f475af8533a19ef19da3b74e1f99b5658d78 Mon Sep 17 00:00:00 2001 From: juancarrillodev Date: Fri, 15 Jul 2022 09:18:52 -0600 Subject: [PATCH] test --- dashboard-server/middleware/auth.js | 18 +- dashboard-server/middleware/auth.test.js | 396 ++++++++++++++++++ dashboard-server/package.json | 5 + .../utils/test-jwt/helpers.jwt.js | 57 +++ 4 files changed, 462 insertions(+), 14 deletions(-) create mode 100644 dashboard-server/middleware/auth.test.js create mode 100644 dashboard-server/utils/test-jwt/helpers.jwt.js diff --git a/dashboard-server/middleware/auth.js b/dashboard-server/middleware/auth.js index f3c0c78d..359bf2fb 100644 --- a/dashboard-server/middleware/auth.js +++ b/dashboard-server/middleware/auth.js @@ -1,18 +1,8 @@ -const jwt = require('express-jwt'); -const { JWT: clientConfig, SERVER_JWT: serverConfig } = require('../config'); +const { MIDDLEWARE_JWT: clientConfig } = require('../config'); +const { auth } = require('express-oauth2-jwt-bearer'); // make middleware that tries auth0 client then if that fails // tries auth0 server application... -function auth(req, res, next) { - jwt(clientConfig)(req, res, (err, res) => { - if (!err) return next(); - if (err && err.name === 'UnauthorizedError') { +const checkJwt = auth(clientConfig); - return jwt(serverConfig)(req, res, next); - } else { - return next(err); - } - }); -} - -module.exports = auth; +module.exports = checkJwt; diff --git a/dashboard-server/middleware/auth.test.js b/dashboard-server/middleware/auth.test.js new file mode 100644 index 00000000..4a170ded --- /dev/null +++ b/dashboard-server/middleware/auth.test.js @@ -0,0 +1,396 @@ +var { randomBytes } = require('crypto'); +var express = require('express'); +var nock = require('nock'); +var got = require('got'); +var { + expect, + fail, +} = require('chai'); +var { createJwt } = require('../utils/test-jwt/helpers.jwt'); +var { + auth, + claimCheck, + claimEquals, + claimIncludes, + requiredScopes, + UnauthorizedError, + InvalidRequestError, + InvalidTokenError, + InsufficientScopeError, +} = require('express-oauth2-jwt-bearer'); + +const expectFailsWith = async function ( + promise, + status, + code, + description, + scopes +) { + try { + await promise; + fail('Request should fail'); + } catch (e) { + const error = code ? `, error="${code}"` : ''; + const errorDescription = description + ? `, error_description="${description}"` + : ''; + expect(e.response.statusCode).to.equal(status); + expect(e.response.headers['www-authenticate']).to.equal( + `Bearer realm="api"${error}${errorDescription}${ + (scopes && ', scope="' + scopes + '"') || '' + }` + ); + } +}; + +describe('index', () => { + let server; + + afterEach((done) => { + nock.cleanAll(); + (server && server.listening && server.close(done)) || done(); + }); + + const setup = ( + opts = {} + ) => { + const app = express(); + const { middleware, ...authOpts } = opts; + app.use(express.urlencoded({ extended: false })); + + app.use( + auth({ + issuerBaseURL: 'https://issuer.example.com/', + audience: 'https://api/', + ...authOpts, + }) + ); + + if (middleware) { + app.use(middleware); + } + + app.all('/', (req, res, next) => { + try { + res.json(req.auth); + next(); + } catch (e) { + next(e); + } + }); + + return new Promise((resolve) => { + server = app.listen(0, () => + resolve(`http://localhost:${(server.address()).port}`) + ); + }); + }; + + it('should fail for anonymous requests', async () => { + const baseUrl = await setup(); + await expectFailsWith(got(baseUrl), 401); + }); + + it('should succeed for authenticated requests', async () => { + const jwt = await createJwt(); + const baseUrl = await setup(); + const response = await got(baseUrl, { + headers: { authorization: `Bearer ${jwt}` }, + responseType: 'json', + }); + expect(response.statusCode).to.equal(200); + expect(response.body).to.haveOwnProperty( + 'payload' + ); + expect(response.body.payload).to.haveOwnProperty( + 'iss', 'https://issuer.example.com/' + ); + }); + + it('should succeed for authenticated requests signed with symmetric keys', async () => { + const secret = randomBytes(32).toString('hex'); + const jwt = await createJwt({ secret }); + const baseUrl = await setup({ + secret, + tokenSigningAlg: 'HS256', + }); + const response = await got(baseUrl, { + headers: { authorization: `Bearer ${jwt}` }, + responseType: 'json', + }); + expect(response.statusCode).to.equal(200); + expect(response.body).to.haveOwnProperty( + 'payload' + ); + expect(response.body.payload).to.haveOwnProperty( + 'iss', 'https://issuer.example.com/' + ); + }); + + it('should fail for requests signed with invalid symmetric keys', async () => { + const jwt = await createJwt({ secret: randomBytes(32).toString('hex') }); + const baseUrl = await setup({ + secret: randomBytes(32).toString('hex'), + tokenSigningAlg: 'HS256', + }); + await expectFailsWith( + got(baseUrl, { + headers: { + authorization: `Bearer ${jwt}`, + }, + responseType: 'json', + }), + 401, + 'invalid_token', + 'signature verification failed' + ); + }); + + it('should fail for audience mismatch', async () => { + const jwt = await createJwt({ audience: 'bar' }); + const baseUrl = await setup({ + audience: 'foo', + }); + await expectFailsWith( + got(baseUrl, { + headers: { + authorization: `Bearer ${jwt}`, + }, + responseType: 'json', + }), + 401, + 'invalid_token', + `Unexpected 'aud' value` + ); + }); + + it('should fail when custom validator fails', async () => { + const jwt = await createJwt(); + const baseUrl = await setup({ + validators: { + foo: () => false, + }, + }); + await expectFailsWith( + got(baseUrl, { + headers: { + authorization: `Bearer ${jwt}`, + }, + responseType: 'json', + }), + 401, + 'invalid_token', + `Unexpected 'foo' value` + ); + }); + + it('should succeed for POST requests with custom character encoding', async () => { + const jwt = await createJwt(); + const baseUrl = await setup(); + const response = await got(baseUrl, { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', + }, + form: { access_token: jwt }, + responseType: 'json', + }); + expect(response.statusCode).to.equal(200); + }); + + it('should fail with custom claim check for anonymous request', async () => { + const app = express(); + app.use(claimCheck(() => false)); + const baseUrl = await new Promise((resolve) => { + server = app.listen(0, () => + resolve(`http://localhost:${(server.address()).port}`) + ); + }); + try { + await got(baseUrl); + } catch ({ response }) { + expect(response.statusCode).to.equal(401); + expect(response.headers).to.haveOwnProperty( + 'www-authenticate' + ); + } + }); + + it('should fail when custom claim check returns false', async () => { + const jwt = await createJwt({ payload: { num: 2 } }); + const baseUrl = await setup({ + middleware: claimCheck( + ({ num }) => typeof num === 'number' && num > 3, + "'num' too small" + ), + }); + await expectFailsWith( + got(baseUrl, { + headers: { + authorization: `Bearer ${jwt}`, + }, + responseType: 'json', + }), + 401, + 'invalid_token', + "'num' too small" + ); + }); + + it('should succeed when custom claim check returns true', async () => { + const jwt = await createJwt({ payload: { num: 4 } }); + const baseUrl = await setup({ + middleware: claimCheck( + ({ num }) => typeof num === 'number' && num > 3, + '"num" too small' + ), + }); + const response = await got(baseUrl, { + headers: { authorization: `Bearer ${jwt}` }, + responseType: 'json', + }); + expect(response.statusCode).to.equal(200); + expect(response.body).to.haveOwnProperty( + 'payload' + ); + expect(response.body.payload).to.haveOwnProperty( + 'num', 4 + ); + }); + + it('should fail when actual claim does not equal expected claim', async () => { + const jwt = await createJwt({ payload: { foo: 'baz' } }); + const baseUrl = await setup({ middleware: claimEquals('foo', 'bar') }); + await expectFailsWith( + got(baseUrl, { + headers: { + authorization: `Bearer ${jwt}`, + }, + responseType: 'json', + }), + 401, + 'invalid_token', + "Unexpected 'foo' value" + ); + }); + + it('should succeed when actual claim does equals expected claim', async () => { + const jwt = await createJwt({ payload: { foo: 'bar' } }); + const baseUrl = await setup({ middleware: claimEquals('foo', 'bar') }); + const response = await got(baseUrl, { + headers: { authorization: `Bearer ${jwt}` }, + responseType: 'json', + }); + expect(response.statusCode).to.equal(200); + expect(response.body).to.haveOwnProperty( + 'payload' + ); + expect(response.body.payload).to.haveOwnProperty( + 'foo', 'bar' + ); + }); + + it('should fail when actual claim does not include expected claims', async () => { + const jwt = await createJwt({ payload: { foo: 'bar qux' } }); + const baseUrl = await setup({ + middleware: claimIncludes('foo', 'bar', 'baz'), + }); + await expectFailsWith( + got(baseUrl, { + headers: { + authorization: `Bearer ${jwt}`, + }, + responseType: 'json', + }), + 401, + 'invalid_token', + "Unexpected 'foo' value" + ); + }); + + it('should succeed when actual claim includes expected claims', async () => { + const jwt = await createJwt({ payload: { foo: ['bar', 'baz'] } }); + const baseUrl = await setup({ + middleware: claimIncludes('foo', 'bar', 'baz'), + }); + const response = await got(baseUrl, { + headers: { authorization: `Bearer ${jwt}` }, + responseType: 'json', + }); + expect(response.statusCode).to.equal(200); + expect(response.body).to.haveOwnProperty( + 'payload' + ); + expect(response.body.payload).to.haveOwnProperty( + 'foo' + ); + }); + + it('should fail when required scopes are not included', async () => { + const jwt = await createJwt({ payload: { scope: 'foo bar' } }); + const baseUrl = await setup({ + middleware: requiredScopes(['foo', 'bar', 'baz']), + }); + await expectFailsWith( + got(baseUrl, { + headers: { + authorization: `Bearer ${jwt}`, + }, + responseType: 'json', + }), + 403, + 'insufficient_scope', + 'Insufficient Scope', + 'foo bar baz' + ); + }); + + it('should succeed when required scopes are included', async () => { + const jwt = await createJwt({ payload: { scope: ['foo', 'bar', 'baz'] } }); + const baseUrl = await setup({ + middleware: requiredScopes('foo bar'), + }); + const response = await got(baseUrl, { + headers: { authorization: `Bearer ${jwt}` }, + responseType: 'json', + }); + expect(response.statusCode).to.equal(200); + expect(response.body).to.haveOwnProperty( + 'payload' + ); + expect(response.body.payload).to.haveOwnProperty( + 'scope' + ); + }); + + it('should replace double quotes in header with single quotes', async () => { + const jwt = await createJwt({ payload: { nbf: false } }); + const baseUrl = await setup(); + await expectFailsWith( + got(baseUrl, { + headers: { + authorization: `Bearer ${jwt}`, + }, + responseType: 'json', + }), + 401, + 'invalid_token', + "'nbf' claim must be a number" + ); + }); + + it('should export errors', () => { + expect(() => { + throw new UnauthorizedError(); + }).to.throw(UnauthorizedError); + expect(() => { + throw new InvalidRequestError(); + }).to.throw(InvalidRequestError); + expect(() => { + throw new InvalidTokenError(); + }).to.throw(InvalidTokenError); + expect(() => { + throw new InsufficientScopeError(); + }).to.throw(InsufficientScopeError); + }); +}); \ No newline at end of file diff --git a/dashboard-server/package.json b/dashboard-server/package.json index 39f2095c..09be3381 100644 --- a/dashboard-server/package.json +++ b/dashboard-server/package.json @@ -23,6 +23,7 @@ "homepage": "https://github.com/vlab-research/dashboard-server#readme", "devDependencies": { "chai": "4.2.0", + "crypto": "^1.0.1", "eslint": "5.15.1", "eslint-config-prettier": "4.1.0", "eslint-config-standard": "12.0.0", @@ -31,8 +32,11 @@ "eslint-plugin-prettier": "3.0.1", "eslint-plugin-promise": "4.0.1", "eslint-plugin-standard": "4.0.0", + "got": "11.8.2", "husky": "^1.3.1", + "jose": "^4.8.3", "mocha": "6.0.2", + "nock": "^13.2.8", "prettier": "1.16.4" }, "dependencies": { @@ -44,6 +48,7 @@ "dotenv": "6.2.0", "express": "4.16.4", "express-jwt": "5.3.1", + "express-oauth2-jwt-bearer": "^1.1.0", "joi": "14.3.1", "jwks-rsa": "1.4.0", "morgan": "^1.9.1", diff --git a/dashboard-server/utils/test-jwt/helpers.jwt.js b/dashboard-server/utils/test-jwt/helpers.jwt.js new file mode 100644 index 00000000..c9274648 --- /dev/null +++ b/dashboard-server/utils/test-jwt/helpers.jwt.js @@ -0,0 +1,57 @@ +var { Buffer } = require('buffer'); +var { createSecretKey } = require('crypto'); +var { SignJWT, generateKeyPair, exportJWK } = require('jose'); +var nock = require('nock'); + +const now = (Date.now() / 1000) | 0; +const day = 60 * 60 * 24; +exports.now = now; + +const createJwt = async ({ + payload = {}, + issuer = 'https://issuer.example.com/', + subject = 'me', + audience = 'https://api/', + jwksUri = '/.well-known/jwks.json', + discoveryUri = '/.well-known/openid-configuration', + iat = now, + exp = now + day, + kid = 'kid', + jwksSpy = undefined, + discoverSpy = undefined, + secret, +} = {}) => { + const { publicKey, privateKey } = await generateKeyPair('RS256'); + const publicJwk = await exportJWK(publicKey); + nock(issuer) + .persist() + .get(jwksUri) + .reply(200, (...args) => { + // jwksSpy(...args); + return { keys: [{ kid, ...publicJwk }] }; + }) + .get(discoveryUri) + .reply(200, (...args) => { + // discoverSpy(...args); + return { + issuer, + jwks_uri: (issuer + jwksUri).replace('//.well-known', '/.well-known'), + }; + }); + + const secretKey = secret && createSecretKey(Buffer.from(secret)); + + return new SignJWT(payload) + .setProtectedHeader({ + alg: secretKey ? 'HS256' : 'RS256', + typ: 'JWT', + kid, + }) + .setIssuer(issuer) + .setSubject(subject) + .setAudience(audience) + .setIssuedAt(iat) + .setExpirationTime(exp) + .sign(secretKey || privateKey); +}; +exports.createJwt = createJwt; \ No newline at end of file