-
-
Notifications
You must be signed in to change notification settings - Fork 86
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
Store hashed tokens #81
Conversation
app/models/passwordless/session.rb
Outdated
break token unless Session.find_by(token: token) | ||
} | ||
|
||
if not self.token or not self.token_digest |
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.
I couldn't get conditional assignment to work with destructuring 🤷♂️
@@ -13,7 +13,7 @@ def change | |||
t.datetime :claimed_at | |||
t.text :user_agent, null: false | |||
t.string :remote_addr, null: false | |||
t.string :token, null: false | |||
t.string :token_digest, null: false |
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.
Does it make sense to modify the existing migration, or should we add a second migration?
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.
I think we should modify the original but provide upgrade instructions for existing installations.
@@ -13,11 +13,13 @@ class Session < ApplicationRecord | |||
:expires_at, | |||
:user_agent, | |||
:remote_addr, | |||
:token, | |||
:token_digest, |
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.
I thought calling the database column token_digest
made it more clear that it's not the actual token. The tradeoff is this is a breaking change and assumedly would require a major version bump and running a migration. We can keep the column name the same to make upgrading easier, but you could still call it a breaking change if there are any valid non-hashed tokens in the database when you upgrade.
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.
I agree we should hint in the column name that it's not the actual token 👍
As we're still pre 1.0, we can do breaking changes like this in a minor release. But I still like to provide all the necessary upgrade info. I think we should:
- Rename the field so all new installations have it right
- Provide upgrade instructions in the changelog including an example migration
lib/passwordless.rb
Outdated
mattr_accessor(:token_generator) { UrlSafeBase64Generator.new } | ||
mattr_accessor(:token_generator) { | ||
Passwordless::TokenGenerator.new( | ||
ActiveSupport::CachingKeyGenerator.new(ActiveSupport::KeyGenerator.new('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.
I copied this from devise. I'm not sure whether it's necessary for OpenSSL::HMAC
to get a secret "key". If so, should we use rails' SECRET_KEY_BASE
or whatever?
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.
I think I'd like to keep CachingKeyGenerator out of this block – maybe move it to a Passwordless.digest(token)
method? Then use it in a setter on Session
like:
def token=(value)
self.token_digest = Passwordless.digest(value)
end
I think using Rails' secret key is a good idea 👍
lib/passwordless/token_generator.rb
Outdated
module Passwordless | ||
class TokenGenerator | ||
def initialize(key_generator, friendly_token_generator, digest) | ||
@key = key_generator.generate_key('passwordless') |
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.
Devise uses the column name but I've just called it 'passwordless'
Wonderful work so far ❤️ |
Thanks! I’d love your thoughts on the field rename and migration bit when you have a sec (see my inline comments). And the secret key thing if any ideas come to mind, otherwise I’ll do some more research on it. |
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.
Let me know what you think 😊
@@ -13,11 +13,13 @@ class Session < ApplicationRecord | |||
:expires_at, | |||
:user_agent, | |||
:remote_addr, | |||
:token, | |||
:token_digest, |
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.
I agree we should hint in the column name that it's not the actual token 👍
As we're still pre 1.0, we can do breaking changes like this in a minor release. But I still like to provide all the necessary upgrade info. I think we should:
- Rename the field so all new installations have it right
- Provide upgrade instructions in the changelog including an example migration
lib/passwordless.rb
Outdated
mattr_accessor(:token_generator) { UrlSafeBase64Generator.new } | ||
mattr_accessor(:token_generator) { | ||
Passwordless::TokenGenerator.new( | ||
ActiveSupport::CachingKeyGenerator.new(ActiveSupport::KeyGenerator.new('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.
I think I'd like to keep CachingKeyGenerator out of this block – maybe move it to a Passwordless.digest(token)
method? Then use it in a setter on Session
like:
def token=(value)
self.token_digest = Passwordless.digest(value)
end
I think using Rails' secret key is a good idea 👍
lib/passwordless/token_generator.rb
Outdated
require 'openssl' | ||
|
||
module Passwordless | ||
class TokenGenerator |
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.
I think we should split out the hashing from this. The token generators are supposed to be easy to provide yourself and if you do, I think we should still provide the hashing.
Let's let the token generator still just return a simple, unhashed string.
Thanks for the feedback! All great notes. I'll work on that over the next week or so as I get time. Didn't realise there could be breaking changes in a minor version release for this library though! I'll have to update my project's Gemfile to prevent breaking changes coming in 😬 |
@@ -9,6 +10,8 @@ | |||
module Passwordless | |||
mattr_accessor(:default_from_address) { "[email protected]" } | |||
mattr_accessor(:token_generator) { UrlSafeBase64Generator.new } | |||
mattr_accessor(:digest_algorithm) { "SHA256" } | |||
mattr_accessor(:digest_secret) { lambda { Rails.application.secret_key_base } } |
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.
According to the docs this should get it in all 3 environments. Note that deivse uses a helper class for this to support multiple versions of rails out of the box. We could copy that, or just point them here if they're using an older rails version.
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.
No need to support anything below 5.1.4 as per https://github.com/mikker/passwordless/blob/master/passwordless.gemspec#L19
Okay, I think I've incorporated all of your feedback. I dropped the separate TokenGenerator module in order to keep token generation and hashing functionality separate. I moved hashing to a static Beyond this, I think it's just a matter of updating the readme to reflect (1) how to upgrade/migrate, and (2) how to customise hashing. Looking forward to your feedback. |
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.
Sorry this took a bit. Left a few comments again 😊
@@ -13,7 +13,7 @@ def change | |||
t.datetime :claimed_at | |||
t.text :user_agent, null: false | |||
t.string :remote_addr, null: false | |||
t.string :token, null: false | |||
t.string :token_digest, null: false |
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.
I think we should modify the original but provide upgrade instructions for existing installations.
if !token || !token_digest | ||
self.token, self.token_digest = loop { | ||
token = Passwordless.token_generator.call(self) | ||
token_digest = Passwordless.digest(token) | ||
break [token, token_digest] unless Session.find_by(token_digest: token_digest) | ||
} | ||
end |
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 looks a bit suspicious to me. I don't think anything's wrong but it's hard to make sense of. Do we need/want to check for both !token || !token_digest
. What if we have one but not the other? Will that ever happen? Could we prevent it? Just dumping my brain, sorry 😊
@@ -9,6 +10,8 @@ | |||
module Passwordless | |||
mattr_accessor(:default_from_address) { "[email protected]" } | |||
mattr_accessor(:token_generator) { UrlSafeBase64Generator.new } | |||
mattr_accessor(:digest_algorithm) { "SHA256" } | |||
mattr_accessor(:digest_secret) { lambda { Rails.application.secret_key_base } } |
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.
No need to support anything below 5.1.4 as per https://github.com/mikker/passwordless/blob/master/passwordless.gemspec#L19
def self.digest(token) | ||
key_generator = ActiveSupport::CachingKeyGenerator.new( | ||
ActiveSupport::KeyGenerator.new(digest_secret.call) | ||
) | ||
key = key_generator.generate_key("passwordless") | ||
OpenSSL::HMAC.hexdigest(digest_algorithm, key, token) | ||
end |
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.
Perhaps it would be nice to move this to its own class. Maybe only create the generator once and save it as an ivar.
I'm quite busy as well. Any help from anyone on bringing this home is very appreciated 😊 |
AFAIR the work is already done in the big 1.0 PR. It was short sighted of me to have everything be in one PR. If anyone's willing to split it up into separate PRs it would be greatly appreciated. |
Stores hashed versions of the tokens in the database per #80. This PR is not quite done yet, but I wanted to share the draft to get feedback in the meantime. I'm not normally a ruby developer so I'm sure there's things we could improve in it.
I used
OpenSSL::HMAC
like devise does (and basically copied their token generator class) since BCrypt is not idempotent (there's no way to dosession = Session.find_by(token_hash: BCrypt::Password.create(token))
ascreate
generates a new salt every time).I'll leave more comments inline in the code.
I know I still need to:
navigation_test
to passLooking forward to your feedback (feel free to edit directly). Hope this is a good start.