-
Notifications
You must be signed in to change notification settings - Fork 51
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implementation of HMAC Based Token pattern #155
base: master
Are you sure you want to change the base?
Conversation
I can tell this was a major effort— the contribution offer is appreciated. There's a lot to dig into and consider here, but I wanted to followup to let you know that this has been seen 👍 |
@devinivy Thanks for your update. I'm looking into implementing the double submit pattern as described in the OWASP cheat sheet. Is it something the CRUMB community could be interested in? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
first off, let me say thank you for implementing this. i love that the hapi community cares about security, and this is a fantastic example of that. your contribution is hugely appreciated!
i requested some changes, largely a lot of nit picking, but the sessionKey
option is definitely worth discussing (see my code comments)
method: (req, h) => { | ||
// This extension method fakes an authentication | ||
req.auth.credentials = { userId: 'john' }; | ||
req.auth.isAuthenticated = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
given the condition that isAuthenticated needs to be true
for us to generate a valid crumb in hmac mode, but it is also possible for isAuthenticated to be false
, i would very much like to see a test here that demonstrates and documents that behavior
crumb = Cryptiles.randomString(settings.size); | ||
} | ||
|
||
if (settings.method === 'hmac' && (request.auth.isAuthenticated)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: extra parens
if (settings.method === 'hmac' && (request.auth.isAuthenticated)) { | |
if (settings.method === 'hmac' && request.auth.isAuthenticated) { |
if (settings.method === 'hmac' && (request.auth.isAuthenticated)) { | ||
crumb = Hmac.encrypt(request.auth.credentials[settings.sessionKey], settings.secret); | ||
} | ||
|
||
h.state(settings.key, crumb, settings.cookieOptions); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as mentioned in another comment, it's possible at this point for crumb
to be null
. we need a test that shows what happens in this scenario
unauthorizedLogger(); | ||
throw Boom.forbidden(); | ||
} | ||
|
||
if (settings.method === 'hmac' && | ||
!Hmac.validate(content[request.route.settings.plugins._crumb.key], request.auth.credentials[settings.sessionKey], settings.secret) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: this line is super long, making it kind of hard to comprehend quickly. it might be best if we assigned the value of request.route.settings.plugins._crumb.key
to a named variable and used that instead of the full reference.
@@ -144,7 +156,13 @@ exports.plugin = { | |||
throw Boom.forbidden(); | |||
} | |||
|
|||
if (header !== getCrumbValue()) { | |||
if (settings.method !== 'hmac' && header !== getCrumbValue()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if we're setting a default of random
to settings.method
, why not make this condition if (settings.method === 'random'
to be more explicit?
|
||
const Crypto = require('crypto'); | ||
|
||
const ALGO = 'sha256'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be configurable. sha256
is a reasonable default, but there's no good reason to not allow users to override it with their own choice
* @param {string}key The secret | ||
* @returns {string}token | ||
*/ | ||
const encrypt = (userId, key) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: all instances of the variable userId
should be sessionId
since that more clearly indicates what the parameter is since it's not necessarily a user's id
* @param {string}key The secret | ||
* @returns {string}token | ||
*/ | ||
const encrypt = (userId, key) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
second nit: key
variables should be renamed to secret
, again to more clearly indicate what it's used for
.update(token_timestamp, 'utf8') // Timestamp used at token generation | ||
.digest('hex'); | ||
|
||
return (digest === token_digest && token_timestamp <= timestamp); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
given the comparison of token_timestamp
and timestamp
, i think it's more appropriate to convert the token_timestamp
to a number after splitting the value out than it is to convert the timestamp
to a string
@@ -63,7 +63,8 @@ The following options are available when registering the plugin. | |||
* `skip` - a function with the signature of `function (request, h) {}`, which when provided, is called for every request. If the provided function returns true, validation and generation of crumb is skipped. Defaults to `false`. | |||
* `enforce` - defaults to true, using enforce with false will set the CSRF header cookie but won't execute the validation | |||
* `logUnauthorized` - whether to add to the request log with tag 'crumb' and data 'validation failed' (defaults to false) | |||
|
|||
* `method` - The token generation method (and therefore how to validate), could take values `random` or `hmac`. Defaults to `random`, if set to `hmac` crumb will use the `Hmac Based Token pattern` [as described by OWASP here](https://github.com/OWASP/CheatSheetSeries/blob/083b4791c44ef28b105f66c82ffd83a86bf9fa16/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.md). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
* `method` - The token generation method (and therefore how to validate), could take values `random` or `hmac`. Defaults to `random`, if set to `hmac` crumb will use the `Hmac Based Token pattern` [as described by OWASP here](https://github.com/OWASP/CheatSheetSeries/blob/083b4791c44ef28b105f66c82ffd83a86bf9fa16/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.md). | |
* `method` - The token generation and validation method, can be `'random'` or `'hmac'`. Defaults to `'random'`, if set to `'hmac'` crumb will use the `Hmac Based Token pattern` [as described by OWASP here](https://github.com/OWASP/CheatSheetSeries/blob/083b4791c44ef28b105f66c82ffd83a86bf9fa16/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.md). |
if this is something you're interested in implementing we would be happy to review and merge! |
Hello,
In the frame of one of my project, I had to modify the Crumb plugin to implement an
HMAC based token pattern
to be used in a stateless server. I think it could valuable for the community to have it officially embedded in Crumb. The implementation follows the pattern described in the OWSAP cheatsheet. I have included a new option to specify the token generation to use: random (the method used by crumb as of now) or hmac (my current contribution)Unit tests have been updated to test 100% of the code base with both generation method. API markdown documentation has also been updated accordingly.