Add a jwt token authorization #200

Closed
neptunish wants to merge 4 commits from add_a_jwt_token_authorization into master
11 changed files with 448 additions and 20 deletions
+10
View File
@@ -31,6 +31,14 @@ const argv = optimist
default: 10, default: 10,
describe: 'maximum number of tcp sockets each client is allowed to establish at one time (the tunnels)' describe: 'maximum number of tcp sockets each client is allowed to establish at one time (the tunnels)'
}) })
.options('range', {
default: null,
describe: 'will bind incoming connections only on ports in range xxx:xxxx'
})
.options('jwt-shared-secret', {
default: null,
describe: 'JWT shared secret used to encode tokens'
})
.argv; .argv;
if (argv.help) { if (argv.help) {
@@ -42,6 +50,8 @@ const server = CreateServer({
max_tcp_sockets: argv['max-sockets'], max_tcp_sockets: argv['max-sockets'],
secure: argv.secure, secure: argv.secure,
domain: argv.domain, domain: argv.domain,
range: argv.range,
jwt_shared_secret: argv['jwt-shared-secret']
}); });
server.listen(argv.port, argv.address, () => { server.listen(argv.port, argv.address, () => {
+18 -1
View File
@@ -2,6 +2,7 @@ import http from 'http';
import Debug from 'debug'; import Debug from 'debug';
import pump from 'pump'; import pump from 'pump';
import EventEmitter from 'events'; import EventEmitter from 'events';
import jwt from 'jsonwebtoken';
// A client encapsulates req/res handling using an agent // A client encapsulates req/res handling using an agent
// //
@@ -13,6 +14,7 @@ class Client extends EventEmitter {
const agent = this.agent = options.agent; const agent = this.agent = options.agent;
const id = this.id = options.id; const id = this.id = options.id;
this.securityToken = options.securityToken;
this.debug = Debug(`lt:Client[${this.id}]`); this.debug = Debug(`lt:Client[${this.id}]`);
@@ -45,6 +47,21 @@ class Client extends EventEmitter {
}); });
} }
isSecurityTokenEqual(securityToken) {
const decodeJWT = (token) => {
return jwt.decode(token.replace(/Bearer /, ''), {complete: false});
}
if (this.securityToken === null) {
return false;
}
try{
return decodeJWT(this.securityToken).name === decodeJWT(securityToken).name;
}catch(error){
this.debug('error with jwt %s %s', securityToken, error);
return false;
}
}
stats() { stats() {
return this.agent.stats(); return this.agent.stats();
} }
@@ -129,4 +146,4 @@ class Client extends EventEmitter {
} }
} }
export default Client; export default Client;
+6 -1
View File
@@ -3,6 +3,7 @@ import Debug from 'debug';
import Client from './Client'; import Client from './Client';
import TunnelAgent from './TunnelAgent'; import TunnelAgent from './TunnelAgent';
import PortManager from "./PortManager";
// Manage sets of clients // Manage sets of clients
// //
@@ -13,6 +14,7 @@ class ClientManager {
// id -> client instance // id -> client instance
this.clients = new Map(); this.clients = new Map();
this.portManager = new PortManager({range: this.opt.range||null})
// statistics // statistics
this.stats = { this.stats = {
@@ -28,7 +30,7 @@ class ClientManager {
// create a new tunnel with `id` // create a new tunnel with `id`
// if the id is already used, a random id is assigned // if the id is already used, a random id is assigned
// if the tunnel could not be created, throws an error // if the tunnel could not be created, throws an error
async newClient(id) { async newClient(id, securityToken) {
const clients = this.clients; const clients = this.clients;
const stats = this.stats; const stats = this.stats;
@@ -39,6 +41,7 @@ class ClientManager {
const maxSockets = this.opt.max_tcp_sockets; const maxSockets = this.opt.max_tcp_sockets;
const agent = new TunnelAgent({ const agent = new TunnelAgent({
portManager: this.portManager,
clientId: id, clientId: id,
maxSockets: 10, maxSockets: 10,
}); });
@@ -46,6 +49,7 @@ class ClientManager {
const client = new Client({ const client = new Client({
id, id,
agent, agent,
securityToken
}); });
// add to clients map immediately // add to clients map immediately
@@ -79,6 +83,7 @@ class ClientManager {
if (!client) { if (!client) {
return; return;
} }
this.portManager.release(client.agent.port);
--this.stats.tunnels; --this.stats.tunnels;
delete this.clients[id]; delete this.clients[id];
client.close(); client.close();
+8 -8
View File
@@ -11,22 +11,22 @@ describe('ClientManager', () => {
it('should create a new client with random id', async () => { it('should create a new client with random id', async () => {
const manager = new ClientManager(); const manager = new ClientManager();
const client = await manager.newClient(); const client = await manager.newClient(null, null);
assert(manager.hasClient(client.id)); assert(manager.hasClient(client.id));
manager.removeClient(client.id); manager.removeClient(client.id);
}); });
it('should create a new client with id', async () => { it('should create a new client with id', async () => {
const manager = new ClientManager(); const manager = new ClientManager();
const client = await manager.newClient('foobar'); const client = await manager.newClient('foobar', null);
assert(manager.hasClient('foobar')); assert(manager.hasClient('foobar'));
manager.removeClient('foobar'); manager.removeClient('foobar');
}); });
it('should create a new client with random id if previous exists', async () => { it('should create a new client with random id if previous exists', async () => {
const manager = new ClientManager(); const manager = new ClientManager();
const clientA = await manager.newClient('foobar'); const clientA = await manager.newClient('foobar', null);
const clientB = await manager.newClient('foobar'); const clientB = await manager.newClient('foobar', null);
assert(clientA.id, 'foobar'); assert(clientA.id, 'foobar');
assert(manager.hasClient(clientB.id)); assert(manager.hasClient(clientB.id));
assert(clientB.id != clientA.id); assert(clientB.id != clientA.id);
@@ -36,7 +36,7 @@ describe('ClientManager', () => {
it('should remove client once it goes offline', async () => { it('should remove client once it goes offline', async () => {
const manager = new ClientManager(); const manager = new ClientManager();
const client = await manager.newClient('foobar'); const client = await manager.newClient('foobar', null);
const socket = await new Promise((resolve) => { const socket = await new Promise((resolve) => {
const netClient = net.createConnection({ port: client.port }, () => { const netClient = net.createConnection({ port: client.port }, () => {
@@ -57,8 +57,8 @@ describe('ClientManager', () => {
it('should remove correct client once it goes offline', async () => { it('should remove correct client once it goes offline', async () => {
const manager = new ClientManager(); const manager = new ClientManager();
const clientFoo = await manager.newClient('foo'); const clientFoo = await manager.newClient('foo', null);
const clientBar = await manager.newClient('bar'); const clientBar = await manager.newClient('bar', null);
const socket = await new Promise((resolve) => { const socket = await new Promise((resolve) => {
const netClient = net.createConnection({ port: clientFoo.port }, () => { const netClient = net.createConnection({ port: clientFoo.port }, () => {
@@ -80,7 +80,7 @@ describe('ClientManager', () => {
it('should remove clients if they do not connect within 5 seconds', async () => { it('should remove clients if they do not connect within 5 seconds', async () => {
const manager = new ClientManager(); const manager = new ClientManager();
const clientFoo = await manager.newClient('foo'); const clientFoo = await manager.newClient('foo', null);
assert(manager.hasClient('foo')); assert(manager.hasClient('foo'));
// wait past grace period (1s) // wait past grace period (1s)
+60
View File
@@ -0,0 +1,60 @@
import Debug from 'debug';
class PortManager {
constructor(opt) {
this.debug = Debug('lt:PortManager');
this.range = opt.range || null;
this.first = null;
this.last = null;
this.pool = {};
this.initializePool();
}
initializePool() {
if (this.range === null) {
return;
}
if (!/^[0-9]+:[0-9]+$/.test(this.range)) {
throw new Error('Bad range expression: ' + this.range);
}
[this.first, this.last] = this.range.split(':').map((port) => parseInt(port));
if (this.first > this.last) {
throw new Error('Bad range expression min > max: ' + this.range);
}
for (let port = this.first; port <= this.last; port++) {
this.pool['_' + port] = null;
}
this.debug = Debug('lt:PortManager');
this.debug('Pool initialized ' + JSON.stringify(this.pool));
}
release(port) {
if (this.range === null) {
return;
}
this.debug('Release port ' + port);
this.pool['_' + port] = null;
}
getNextAvailable(clientId) {
if (this.range === null) {
return null;
}
for (let port = this.first; port <= this.last; port++) {
if (this.pool['_' + port] === null) {
this.pool['_' + port] = clientId;
this.debug('Port found ' + port);
return port;
}
}
this.debug('No more ports available ');
throw new Error('No more ports available in range ' + this.range);
}
}
export default PortManager;
+51
View File
@@ -0,0 +1,51 @@
import assert from 'assert';
import PortManager from './PortManager';
describe('PortManager', () => {
it('should construct with no range', () => {
const portManager = new PortManager({});
assert.equal(portManager.range, null);
assert.equal(portManager.first, null);
assert.equal(portManager.last, null);
});
it('should construct with range', () => {
const portManager = new PortManager({range: '10:20'});
assert.equal(portManager.range, '10:20');
assert.equal(portManager.first, 10);
assert.equal(portManager.last, 20);
});
it('should not construct with bad range expression', () => {
assert.throws(()=>{
new PortManager({range: 'a1020'});
}, /Bad range expression: a1020/)
});
it('should not construct with bad range max>min', () => {
assert.throws(()=>{
new PortManager({range: '20:10'});
}, /Bad range expression min > max: 20:10/)
});
it('should work has expected', async () => {
const portManager = new PortManager({range: '10:12'});
assert.equal(10,portManager.getNextAvailable('a'));
assert.equal(11,portManager.getNextAvailable('b'));
assert.equal(12,portManager.getNextAvailable('c'));
assert.throws(()=>{
portManager.getNextAvailable();
}, /No more ports available in range 10:12/)
portManager.release(11);
assert.equal(11,portManager.getNextAvailable('bb'));
portManager.release(10);
portManager.release(12);
assert.equal(10,portManager.getNextAvailable('cc'));
assert.equal(12,portManager.getNextAvailable('dd'));
});
});
+11 -4
View File
@@ -20,6 +20,9 @@ class TunnelAgent extends Agent {
// sockets we can hand out via createConnection // sockets we can hand out via createConnection
this.availableSockets = []; this.availableSockets = [];
this.port = null;
this.clientId = options.clientId
this.portManager = options.portManager || null;
// when a createConnection cannot return a socket, it goes into a queue // when a createConnection cannot return a socket, it goes into a queue
// once a socket is available it is handed out to the next callback // once a socket is available it is handed out to the next callback
@@ -63,13 +66,14 @@ class TunnelAgent extends Agent {
}); });
return new Promise((resolve) => { return new Promise((resolve) => {
server.listen(() => { const port = this.portManager ? this.portManager.getNextAvailable(this.options.clientId) : null;
const port = server.address().port; server.listen(port,() => {
this.debug('tcp server listening on port: %d', port); this.port = server.address().port
this.debug('tcp server listening on port: %d (%s)', this.port, this.clientId);
resolve({ resolve({
// port for lt client tcp connections // port for lt client tcp connections
port: port, port: this.port,
}); });
}); });
}); });
@@ -115,6 +119,9 @@ class TunnelAgent extends Agent {
socket.once('error', (err) => { socket.once('error', (err) => {
// we do not log these errors, sessions can drop from clients for many reasons // we do not log these errors, sessions can drop from clients for many reasons
// these are not actionable errors for our server // these are not actionable errors for our server
if(this.portManager){
this.portManager.release(this.port);
}
socket.destroy(); socket.destroy();
}); });
+2 -1
View File
@@ -14,6 +14,7 @@
"esm": "3.0.34", "esm": "3.0.34",
"human-readable-ids": "1.0.3", "human-readable-ids": "1.0.3",
"koa": "2.5.1", "koa": "2.5.1",
"koa-jwt": "4.0.0",
"koa-router": "7.4.0", "koa-router": "7.4.0",
"localenv": "0.2.2", "localenv": "0.2.2",
"optimist": "0.6.1", "optimist": "0.6.1",
@@ -29,6 +30,6 @@
"scripts": { "scripts": {
"test": "mocha --check-leaks --require esm './**/*.test.js'", "test": "mocha --check-leaks --require esm './**/*.test.js'",
"start": "./bin/server", "start": "./bin/server",
"dev": "node-dev bin/server --port 3000" "dev": "node-dev bin/server --require esm --port 3000"
} }
} }
+51 -2
View File
@@ -5,11 +5,18 @@ import Debug from 'debug';
import http from 'http'; import http from 'http';
import { hri } from 'human-readable-ids'; import { hri } from 'human-readable-ids';
import Router from 'koa-router'; import Router from 'koa-router';
import jwt from'koa-jwt';
import ClientManager from './lib/ClientManager'; import ClientManager from './lib/ClientManager';
const debug = Debug('localtunnel:server'); const debug = Debug('localtunnel:server');
function addJwtMiddleware(app, opt) {
app.use(jwt({
secret: opt.jwt_shared_secret
}));
}
export default function(opt) { export default function(opt) {
opt = opt || {}; opt = opt || {};
@@ -28,6 +35,10 @@ export default function(opt) {
const app = new Koa(); const app = new Koa();
const router = new Router(); const router = new Router();
if (opt.jwt_shared_secret){
addJwtMiddleware(app, opt);
}
router.get('/api/status', async (ctx, next) => { router.get('/api/status', async (ctx, next) => {
const stats = manager.stats; const stats = manager.stats;
ctx.body = { ctx.body = {
@@ -50,6 +61,44 @@ export default function(opt) {
}; };
}); });
router.get('/api/tunnels/:id/kill', async (ctx, next) => {
const clientId = ctx.params.id;
if (!opt.jwt_shared_secret){
debug('disconnecting client with id %s, error: jwt_shared_secret is not used', clientId);
ctx.throw(403, {
success: false,
message: 'jwt_shared_secret is not used'
});
return;
}
if (!manager.hasClient(clientId)) {
debug('disconnecting client with id %s, error: client is not connected', clientId);
ctx.throw(404, {
success: false,
message: `client with id ${clientId} is not connected`
});
}
const securityToken = ctx.request.headers.authorization;
if (!manager.getClient(clientId).isSecurityTokenEqual(securityToken)) {
debug('disconnecting client with id %s, error: securityToken is not equal ', clientId);
ctx.throw(403, {
success: false,
message: `client with id ${clientId} has not the same securityToken than ${securityToken}`
});
}
debug('disconnecting client with id %s', clientId);
manager.removeClient(clientId);
ctx.statusCode = 200;
ctx.body = {
success: true,
message: `client with id ${clientId} is disconected`
};
});
app.use(router.routes()); app.use(router.routes());
app.use(router.allowedMethods()); app.use(router.allowedMethods());
@@ -67,7 +116,7 @@ export default function(opt) {
if (isNewClientRequest) { if (isNewClientRequest) {
const reqId = hri.random(); const reqId = hri.random();
debug('making new client with id %s', reqId); debug('making new client with id %s', reqId);
const info = await manager.newClient(reqId); const info = await manager.newClient(reqId, opt.jwt_shared_secret ? ctx.request.headers.authorization : null);
const url = schema + '://' + info.id + '.' + ctx.request.host; const url = schema + '://' + info.id + '.' + ctx.request.host;
info.url = url; info.url = url;
@@ -105,7 +154,7 @@ export default function(opt) {
} }
debug('making new client with id %s', reqId); debug('making new client with id %s', reqId);
const info = await manager.newClient(reqId); const info = await manager.newClient(reqId, opt.jwt_shared_secret ? ctx.request.headers.authorization : null);
const url = schema + '://' + info.id + '.' + ctx.request.host; const url = schema + '://' + info.id + '.' + ctx.request.host;
info.url = url; info.url = url;
+77 -3
View File
@@ -1,8 +1,9 @@
import request from 'supertest'; import request from 'supertest';
import assert from 'assert'; import assert from 'assert';
import { Server as WebSocketServer } from 'ws'; import WebSocket, {Server as WebSocketServer} from 'ws';
import WebSocket from 'ws';
import net from 'net'; import net from 'net';
import jwt from 'jsonwebtoken';
import createServer from './server'; import createServer from './server';
@@ -34,6 +35,30 @@ describe('Server', () => {
assert.equal(res.body.message, 'Invalid subdomain. Subdomains must be lowercase and between 4 and 63 alphanumeric characters.'); assert.equal(res.body.message, 'Invalid subdomain. Subdomains must be lowercase and between 4 and 63 alphanumeric characters.');
}); });
it('reject request without jwt if required', async () => {
const server = createServer({jwt_shared_secret: 'thekey'});
const res = await request(server).get('/subdomain');
assert.equal(res.status, 401);
});
it('reject request with invalid jwt if required', async () => {
const server = createServer({jwt_shared_secret: 'thekey'});
const jwtoken = jwt.sign({
foo: 'bar'
}, 'thebadkey');
const res = await request(server).get('/subdomain').set('Authorization', `Bearer ${jwtoken}`);
assert.equal(res.status, 401);
});
it('accept request with valid jwt if required', async () => {
const server = createServer({jwt_shared_secret: 'thekey'});
const jwtoken = jwt.sign({
foo: 'bar'
}, 'thekey');
const res = await request(server).get('/subdomain').set('Authorization', `Bearer ${jwtoken}`);
assert.equal(res.status, 200);
});
it('should upgrade websocket requests', async () => { it('should upgrade websocket requests', async () => {
const hostname = 'websocket-test'; const hostname = 'websocket-test';
const server = createServer({ const server = createServer({
@@ -106,4 +131,53 @@ describe('Server', () => {
await new Promise(resolve => server.close(resolve)); await new Promise(resolve => server.close(resolve));
}); });
});
it('should not support the /api/tunnels/:id/kill endpoint if jwt authorization is not enable on server', async () => {
const server = createServer();
await new Promise(resolve => server.listen(resolve));
const res = await request(server).get('/api/tunnels/foobar-test/kill');
assert.equal(res.statusCode, 403);
assert.equal(res.text, 'jwt_shared_secret is not used');
await new Promise(resolve => server.close(resolve));
});
it('should throw error when calling /api/tunnels/:id/kill endpoint if id does not exists', async () => {
const server = createServer({jwt_shared_secret: 'thekey'});
await new Promise(resolve => server.listen(resolve));
{
const jwtoken = jwt.sign({
name: 'bar'
}, 'thekey');
await request(server).get('/foobar-test').set('Authorization', `Bearer ${jwtoken}`);
// no such tunnel yet
const res = await request(server).get('/api/tunnels/foobar-test2/kill').set('Authorization', `Bearer ${jwtoken}`);
assert.equal(res.statusCode, 404);
assert.equal(res.text, 'client with id foobar-test2 is not connected');
}
await new Promise(resolve => server.close(resolve));
});
it('should disconnect client when calling /api/tunnels/:id/kill endpoint', async () => {
const server = createServer({jwt_shared_secret: 'thekey'});
await new Promise(resolve => server.listen(resolve));
{
const jwtoken = jwt.sign({
name: 'bar'
}, 'thekey');
await request(server).get('/foobar-test').set('Authorization', `Bearer ${jwtoken}`);
const res = await request(server).get('/api/tunnels/foobar-test/kill').set('Authorization', `Bearer ${jwtoken}`);
assert.equal(res.statusCode, 200);
assert.equal(res.text, '{"success":true,"message":"client with id foobar-test is disconected"}');
const statusResult = await request(server).get('/api/tunnels/foobar-test/status').set('Authorization', `Bearer ${jwtoken}`);
assert.equal(statusResult.text, 'Not Found');
}
await new Promise(resolve => server.close(resolve));
});
});
+154
View File
@@ -9,6 +9,14 @@ accepts@^1.2.2:
mime-types "~2.1.18" mime-types "~2.1.18"
negotiator "0.6.1" negotiator "0.6.1"
aggregate-error@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a"
integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==
dependencies:
clean-stack "^2.0.0"
indent-string "^4.0.0"
ansi-regex@^2.0.0: ansi-regex@^2.0.0:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
@@ -58,6 +66,11 @@ [email protected]:
version "1.3.1" version "1.3.1"
resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
[email protected]:
version "1.0.1"
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=
builtin-modules@^1.0.0: builtin-modules@^1.0.0:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
@@ -90,6 +103,11 @@ chalk@^1.1.3:
strip-ansi "^3.0.0" strip-ansi "^3.0.0"
supports-color "^2.0.0" supports-color "^2.0.0"
clean-stack@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==
cli-table@^0.3.1: cli-table@^0.3.1:
version "0.3.1" version "0.3.1"
resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23" resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23"
@@ -213,6 +231,13 @@ dynamic-dedupe@^0.2.0:
dependencies: dependencies:
xtend "~2.0.6" xtend "~2.0.6"
[email protected]:
version "1.0.11"
resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
dependencies:
safe-buffer "^5.0.1"
[email protected]: [email protected]:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@@ -369,6 +394,11 @@ indent-string@^2.1.0:
dependencies: dependencies:
repeating "^2.0.0" repeating "^2.0.0"
indent-string@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
indexof@~0.0.1: indexof@~0.0.1:
version "0.0.1" version "0.0.1"
resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
@@ -428,6 +458,39 @@ isexe@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
jsonwebtoken@^8.5.1:
version "8.5.1"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==
dependencies:
jws "^3.2.2"
lodash.includes "^4.3.0"
lodash.isboolean "^3.0.3"
lodash.isinteger "^4.0.4"
lodash.isnumber "^3.0.3"
lodash.isplainobject "^4.0.6"
lodash.isstring "^4.0.1"
lodash.once "^4.0.0"
ms "^2.1.1"
semver "^5.6.0"
jwa@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a"
integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==
dependencies:
buffer-equal-constant-time "1.0.1"
ecdsa-sig-formatter "1.0.11"
safe-buffer "^5.0.1"
jws@^3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
dependencies:
jwa "^1.4.1"
safe-buffer "^5.0.1"
keygrip@~1.0.2: keygrip@~1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.2.tgz#ad3297c557069dea8bcfe7a4fa491b75c5ddeb91" resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.2.tgz#ad3297c557069dea8bcfe7a4fa491b75c5ddeb91"
@@ -457,6 +520,15 @@ koa-is-json@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/koa-is-json/-/koa-is-json-1.0.0.tgz#273c07edcdcb8df6a2c1ab7d59ee76491451ec14" resolved "https://registry.yarnpkg.com/koa-is-json/-/koa-is-json-1.0.0.tgz#273c07edcdcb8df6a2c1ab7d59ee76491451ec14"
[email protected]:
version "4.0.0"
resolved "https://registry.yarnpkg.com/koa-jwt/-/koa-jwt-4.0.0.tgz#4cac70cde5e225bc961a266303d4b6e873cba9bc"
integrity sha512-n56AG98tWQDtvVZwtVFKuPn1pGPOvtkKFEotSPRsdqKmZJqRdppDRD0toiiK7kefMLnVBzFbocaPyaI5WK/iyQ==
dependencies:
jsonwebtoken "^8.5.1"
koa-unless "^1.0.7"
p-any "^2.1.0"
[email protected]: [email protected]:
version "7.4.0" version "7.4.0"
resolved "https://registry.yarnpkg.com/koa-router/-/koa-router-7.4.0.tgz#aee1f7adc02d5cb31d7d67465c9eacc825e8c5e0" resolved "https://registry.yarnpkg.com/koa-router/-/koa-router-7.4.0.tgz#aee1f7adc02d5cb31d7d67465c9eacc825e8c5e0"
@@ -468,6 +540,11 @@ [email protected]:
path-to-regexp "^1.1.1" path-to-regexp "^1.1.1"
urijs "^1.19.0" urijs "^1.19.0"
koa-unless@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/koa-unless/-/koa-unless-1.0.7.tgz#b9df375e2b4da3043918d48622520c2c0b79f032"
integrity sha1-ud83XitNowQ5GNSGIlIMLAt58DI=
[email protected]: [email protected]:
version "2.5.1" version "2.5.1"
resolved "https://registry.yarnpkg.com/koa/-/koa-2.5.1.tgz#79f8b95f8d72d04fe9a58a8da5ebd6d341103f9c" resolved "https://registry.yarnpkg.com/koa/-/koa-2.5.1.tgz#79f8b95f8d72d04fe9a58a8da5ebd6d341103f9c"
@@ -566,6 +643,11 @@ lodash.clonedeep@^3.0.0:
lodash._baseclone "^3.0.0" lodash._baseclone "^3.0.0"
lodash._bindcallback "^3.0.0" lodash._bindcallback "^3.0.0"
lodash.includes@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=
lodash.isarguments@^3.0.0: lodash.isarguments@^3.0.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
@@ -574,6 +656,31 @@ lodash.isarray@^3.0.0:
version "3.0.4" version "3.0.4"
resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55"
lodash.isboolean@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=
lodash.isinteger@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343"
integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=
lodash.isnumber@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc"
integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=
lodash.isplainobject@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=
lodash.isstring@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=
lodash.keys@^3.0.0: lodash.keys@^3.0.0:
version "3.1.2" version "3.1.2"
resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a"
@@ -582,6 +689,11 @@ lodash.keys@^3.0.0:
lodash.isarguments "^3.0.0" lodash.isarguments "^3.0.0"
lodash.isarray "^3.0.0" lodash.isarray "^3.0.0"
lodash.once@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
lodash.toarray@^4.4.0: lodash.toarray@^4.4.0:
version "4.4.0" version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561" resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561"
@@ -692,6 +804,11 @@ [email protected]:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
ms@^2.1.1:
version "2.1.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
[email protected]: [email protected]:
version "0.6.1" version "0.6.1"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
@@ -773,6 +890,28 @@ [email protected]:
minimist "~0.0.1" minimist "~0.0.1"
wordwrap "~0.0.2" wordwrap "~0.0.2"
p-any@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/p-any/-/p-any-2.1.0.tgz#719489408e14f5f941a748f1e817f5c71cab35cb"
integrity sha512-JAERcaMBLYKMq+voYw36+x5Dgh47+/o7yuv2oQYuSSUml4YeqJEFznBrY2UeEkoSHqBua6hz518n/PsowTYLLg==
dependencies:
p-cancelable "^2.0.0"
p-some "^4.0.0"
type-fest "^0.3.0"
p-cancelable@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf"
integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==
p-some@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/p-some/-/p-some-4.1.0.tgz#28e73bc1e0d62db54c2ed513acd03acba30d5c04"
integrity sha512-MF/HIbq6GeBqTrTIl5OJubzkGU+qfFhAFi0gnTAK6rgEIJIknEiABHOTtQu4e6JiXjIwuMPMUFQzyHh5QjCl1g==
dependencies:
aggregate-error "^3.0.0"
p-cancelable "^2.0.0"
parse-json@^2.2.0: parse-json@^2.2.0:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
@@ -896,6 +1035,11 @@ resolve@^1.0.0:
dependencies: dependencies:
path-parse "^1.0.5" path-parse "^1.0.5"
safe-buffer@^5.0.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
safe-buffer@~5.1.0, safe-buffer@~5.1.1: safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2" version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
@@ -904,6 +1048,11 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.5.0" version "5.5.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"
semver@^5.6.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
[email protected]: [email protected]:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
@@ -1012,6 +1161,11 @@ trim-newlines@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
type-fest@^0.3.0:
version "0.3.1"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1"
integrity sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==
type-is@^1.5.5: type-is@^1.5.5:
version "1.6.16" version "1.6.16"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194"