Why need more than one secret key on koa?
Asked Answered
B

1

11

Sorry, I don't quite understand how secret key works in koa. In koa, there is a keys field on app object which will be used like this:

const app = new Koa();
app.keys = ['some secret', 'another secret', 'or more ...']; // it's an
                                                             // array right?

And then, when using koa-csrf middleware, by default the built-in csrf.middleware doesn't use the keys provided by app.keys. If I use the default middleware, I need to create another middleware which set the secret key on the session.

app.use(session()); // koa-generic-session
app.use(async (ctx, next) => {               // without this custom middleware
  ctx.session.secret = 'yet another secret'; // POST /protected-route
  await next();                              // will give 403
});                                          // missing secret
csrf(app);
app.use(csrf.middleware);

When I use Flask, I need to supply only one secret key which is set by string not an array. Why need more than one secret key? Is using only one for the whole application not enough?

Boarish answered 12/2, 2016 at 12:19 Comment(0)
G
13

Seems like you're asking multiple things here.

  1. You can supply Koa app.keys = [...] multiple keys for the purpose of key rotation.

    For example, if you want to generate a new key every month, you can sign new cookies with it without immediately invalidating all old cookies. Instead, you'd prefer to let old cookies naturally expire.

    If key rotation isn't something you care about, then you'd just just use app.keys = ['mysecret'] that you never change.

  2. koa-csrf's middleware actually does use the keys you set with app.keys=.

    Koa passes app.keys into its Cookies instance (https://github.com/pillarjs/cookies) so that the built-in this.cookies.get() and this.cookies.set() will use the keys (if provided).

    koa-session uses Koa's built-in this.cookies.{get,set}.

    koa-csrf uses koa-session.

But all of that is irrelevant.

The 403 response isn't complaining that you haven't set app.keys= secrets. It's complaining that you haven't provided a CSRF token (aka secret) much less a valid one.

Your "fix" of manually setting this.session.secret is just manually setting the value where koa-csrf looks to find the CSRF token. You're bypassing the entire security measure of the CSRF system.

The whole point of a CSRF token system is to ensure that someone hitting a protected endpoint actually originated from, say, a <form> that posts to that endpoint from a page that you control.

It does this by generating a token, saving it in a cookie, attaching the token to the form, and then, on submission, it ensures that the form token matches the session token.

What you seem to be missing is that you have to:

  1. Generate the CSRF token
  2. Set it to the secret cookie for the client
  3. Expose the CSRF token to the client so that they submit it to your protected endpoint.
  4. On the protected endpoint, ensure that the client sends a CSRF token that matches the CSRF token in their 'secret' cookie. Here are the places that koa-csrf checks to find that token.

koa-csrf's this.csrf call does #1 and #2. You have to implement #3. koa-csrf's this.assertCSRF does #4.

So, all together, this is how it looks (untested):

var koa = require('koa')
var csrf = require('koa-csrf')
var session = require('koa-session')
var Router = require('koa-router');
var bodyParser = require('koa-bodyparser');

var app = koa()
app.keys = ['session secret']
app.use(session())
app.use(bodyParser())
csrf(app)
app.use(csrf.middleware)

var router = new Router();

router.get('/messages', function*() {
  this.render('new_message_form.html', {
    token: this.csrf   // this call also sets `this.session.secret` for you
  });
});

router.post('/messages', function*() {
  this.assertCSRF(this.request.body);

  // If we get this far, then the CSRF check passed
  yield database.insertMessage(this.body.message);
});

app.use(router.routes());
app.listen(3000, () => console.log('server listening on 3000'));

And here's what 'new_message_form.html' would look like. Notice that I'm setting a hidden field _csrf so that when the user submits it, the token generated by this.csrf is sent to my protected endpoint, and the _csrf field is one of the places that koa-csrf checks to find the submitted token.

<form action="/messages" method="POST">
  <input type="hidden" name="_csrf" value="{{ token }}">
  <input type="message" name="message" placeholder="Write your message here...">
  <button type="submit">Save Message<button>
</form>
Gaslit answered 13/2, 2016 at 1:26 Comment(1)
Thanks for the answer. I understand what you're explaining there. But, (sorry if I'm not so clear), actually I used ajax post to rest API on the server and expose the csrf token on meta tag which is taken with javascript. I've tested the 403 message is not about token is missing but about secret key is not properly setup, the message is different. You can reproduce my problem with koa-generic-session not koa-session which is different. I suspect there's a miss communication between koa-generic-session and koa-csrf. By the way, thanks, I now understand the purpose of that keys array.Boarish

© 2022 - 2024 — McMap. All rights reserved.