Skip to content

Commit

Permalink
jwt: allow multiple issuers to post
Browse files Browse the repository at this point in the history
We identify our callers with the `tag` query parameter:

/v1/post_events?tag=dinum

To allow multiple callers to post to our app without relying on a
single secret (the current JWT_SECURITY_SALT), introduce a new
JWT_TAGS_AND_SALTS which contains issuer:salt pairs in a
comma-separated string.

On an incoming request, find the matching issuer and sign against
their secret, throwing an error if the issuer isn't found.

Note: if the JWT_TAGS_AND_SALTS isn't set, fallback to the previous
implementation with the single JWT_SECURITY_SALT secret).
  • Loading branch information
freesteph committed Apr 20, 2022
1 parent 2061580 commit 46e7b7a
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 66 deletions.
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ services:
DATABASE_URL: postgres://bbbanalytics:bbbanalytics@db:5432/bbbanalytics
HOSTNAME: localhost
JWT_SECURITY_SALT: SecretThatShouldChangedInProduction
JWT_TAGS_AND_SALTS: dinum:SecretThatShouldChangedInProduction
ports:
- "8100:8100"
- "9229:9229"
Expand Down
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module.exports = {
secret: process.env.JWT_SECURITY_SALT,
tagsAndSalts: process.env.JWT_TAGS_AND_SALTS,
host: process.env.HOSTNAME,
port: process.env.PORT || 8100,
};
4 changes: 2 additions & 2 deletions src/controllers/indexController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ module.exports.postEvents = async function (req, res) {
catch (err) {
return res.status(500).send(`${err}`);
}
return res.json({
return res.status(200).json({
status: 'ok'
}, 200)
});
};
31 changes: 30 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,41 @@ const app = express();
app.use(expressSanitizer());
app.use(bodyParser.urlencoded({ extended: false }));

const getSecretForTag = function(req, payload, done){
const { tag } = req.query;

// fallback to the default JWT_SECURITY_SALT if the new variable isn't set up
if (!config.tagsAndSalts) {
done(null, config.secret);
return;
}

const issuer = config
.tagsAndSalts
.split(",")
.map(i => i.split(":"))
.find(([t]) => t === tag);

if (!issuer) throw new Error(`No matching issuer was found for tag '${tag}'.`)

const [, secret] = issuer;

done(null, secret);
}

app.use(
expressJWT({
secret: config.secret,
secret: getSecretForTag,
algorithms: ['HS512'],
typ: "JWT",
}),
function(err, req, res, next) {
if (err) {
res.status(403).send('No matching issuer was found');
} else {
next();
}
},
);

app.use(express.json());
Expand Down
173 changes: 110 additions & 63 deletions tests/test-event.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,75 +5,122 @@ const config = require('../src/config');
const jwt = require('jsonwebtoken');

const apiResponse = {
"version": "1.0",
"meeting_id": "meeting-persistent-3--c308049",
"internal_meeting_id": "3e677e88dl7698",
"data": {
"metadata": {
"analytics_callback_url": "https://webhook.site/f519038c-b956-4fa3-9b5c-148e8df09b47",
"is_breakout": "false",
"meeting_name": "Arawa3"
version: '1.0',
meeting_id: 'meeting-persistent-c308049',
internal_meeting_id: '3e677e88dl7698',
data: {
metadata: {
analytics_callback_url:
'https://webhook.site/f519038c-b956-4fa3-9b5c-148e8df09b47',
is_breakout: 'false',
meeting_name: 'Arawa3',
},
"duration": 20,
"start": "2021-06-27 18:52:12 +0200",
"finish": "2021-06-27 18:52:32 +0200",
"attendees": [
duration: 20,
start: '2021-06-27 18:52:12 +0200',
finish: '2021-06-27 18:52:32 +0200',
attendees: [
{
"ext_user_id": "w_hll5o9o",
"name": "Lucas Pla",
"moderator": true,
"joins": [
"2021-06-23 18:52:21 +0200"
],
"leaves": [
"2021-06-23 18:52:32 +0200"
],
"duration": 11,
"recent_talking_time": "",
"engagement": {
"chats": 0,
"talks": 0,
"raisehand": 0,
"emojis": 0,
"poll_votes": 0,
"talk_time": 0
}
}
ext_user_id: 'w_hll5o9o',
name: 'Lucas Pla',
moderator: true,
joins: ['2021-06-23 18:52:21 +0200'],
leaves: ['2021-06-23 18:52:32 +0200'],
duration: 11,
recent_talking_time: '',
engagement: {
chats: 0,
talks: 0,
raisehand: 0,
emojis: 0,
poll_votes: 0,
talk_time: 0,
},
},
],
"files": [
"default.pdf"
],
"polls": [],
}}
files: ['default.pdf'],
polls: [],
},
};

const signForSecret = function (secret) {
return jwt.sign(
{
exp: Math.floor(Date.now() / 1000) + 60 * 60,
},
secret,
{
algorithm: 'HS512',
header: {
typ: 'JWT',
},
}
);
};

const sendResponseForTag = async function (tag, secret) {
return await chai
.request(app)
.post(`/v1/post_events?tag=${tag}`)
.set('content-type', 'application/json')
.set('Authorization', 'Bearer ' + signForSecret(secret))
.set('user-agent', 'BigBlueButton Analytics Callback')
.send(apiResponse);
};

describe('Meetings', async () => {
beforeEach(async () => {
config.tagsAndSalts = 'dinum:somefancysecret';

await knex.raw('truncate table meetings cascade');
});

describe('POST /v1/post_events', async () => {
describe("when the JWT_TAGS_AND_SALTS variable isn't set", () => {
beforeEach(() => {
config.tagsAndSalts = null;
});

it('defaults to JWT_SECURITY_SALT', async () => {
const res = await sendResponseForTag('some org', config.secret);

res.should.have.status(200);
});
});

describe('when the issuer is unknown', () => {
it('throws an error', async () => {
const res = await sendResponseForTag('some org', 'somefancysecret');

res.should.have.status(403);
});
});

describe('when the issuer is known but the secret is wrong', () => {
it('throws an error', async () => {
const res = await sendResponseForTag('dinum', 'some other secret');

res.should.have.status(403);
});
});

describe('when the issuer is known and the secret is correct', () => {
it('should create a new entry in meetings', async () => {
const res = await sendResponseForTag('dinum', 'somefancysecret');

res.should.have.status(200);

const stats = await knex('meetings')
.orderBy('created_at', 'desc')
.first();

describe('POST /v1/post_events unauthenticated', async () => {

it('should create a new entry in meetings', async () => {
let res
try {
const token = jwt.sign({
exp: Math.floor(Date.now() / 1000) + (60 * 60),
}, config.secret, { algorithm: 'HS512', header: {
typ: 'JWT'
}})
res = await chai.request(app)
.post('/v1/post_events?tag=dinum')
.set('content-type', 'application/json')
.set("Authorization", "Bearer " + token)
.set('user-agent', 'BigBlueButton Analytics Callback')
.send(apiResponse)
} catch (e) {
console.log(e)
}
res.should.have.status(200);
const stats = await knex('meetings').orderBy('created_at', 'desc').first()
stats.duration.should.be.equal(apiResponse.data.duration)
stats.moderator_count.should.be.equal(1)
stats.internal_meeting_id.should.be.equal(apiResponse.internal_meeting_id)
stats.tag.should.be.equal('dinum')
stats.weekday.should.be.equal(0)
stats.duration.should.be.equal(apiResponse.data.duration);
stats.moderator_count.should.be.equal(1);
stats.internal_meeting_id.should.be.equal(
apiResponse.internal_meeting_id
);
stats.tag.should.be.equal('dinum');
stats.weekday.should.be.equal(0);
});
});
});
});

0 comments on commit 46e7b7a

Please sign in to comment.