Skip to content
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

Open
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

gdelpu
Copy link

@gdelpu gdelpu commented May 20, 2021

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.

@devinivy
Copy link
Member

devinivy commented Jun 3, 2021

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 👍

@gdelpu
Copy link
Author

gdelpu commented Jun 4, 2021

@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?

Copy link
Member

@nlf nlf left a 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;
Copy link
Member

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)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: extra parens

Suggested change
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);
Copy link
Member

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)
Copy link
Member

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()) {
Copy link
Member

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';
Copy link
Member

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) => {
Copy link
Member

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) => {
Copy link
Member

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);
Copy link
Member

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).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
* `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).

@nlf
Copy link
Member

nlf commented Jun 4, 2021

@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?

if this is something you're interested in implementing we would be happy to review and merge!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants