Add a jwt token authorization #154
+18
-1
@@ -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;
|
||||||
|
|||||||
@@ -30,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;
|
||||||
|
|
||||||
@@ -49,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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -61,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());
|
||||||
|
|
||||||
@@ -78,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;
|
||||||
@@ -116,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;
|
||||||
|
|||||||
@@ -131,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));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user