-
Notifications
You must be signed in to change notification settings - Fork 21.4k
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
Add signed ids to Active Record #39313
Conversation
Add support for finding records based on signed ids, which are tamper-proof, verified ids that can be set to expire and scoped with a purpose. This is particularly useful for things like password reset or email verification, where you want the bearer of the signed id to be able to interact with the underlying record, but usually only within a certain time period.
This is cool. Would it be useful to have an option to self-destruct after n uses? Maybe an email link for verification that is one and done. |
Just make the verification idempotent and you're good. These signed ids carry no state. That's their advantage over explicit tokens. So they can't have any self-destruct setup. |
Co-authored-by: Santiago Bartesaghi <sbartesaghi@hotmail.com>
Co-authored-by: प्रथमेश Sonpatki <csonpatki@gmail.com>
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.
Would it make sense to add a couple of tests for the bang version of the method?
First of all great addition 👍 currently I am doing all the same things using JWTs, where the payload has class, id and scope (same as purpose). Would love to have it provided by rails out-of-box. The current implementation of SignedId doesn't handle one time usage cases, like password reset token should automatically expire if used one time. JWTs have an IssuedAt (iat) attribute that I use for this specific purpose. May be we can add an issued_at timestamp in SignedID as well and verify it against a provided timestamp field (updated_at or password_changed_at) and flag the token as expired if isssued_at < password_changed_at. This way we can handle one time usage cases as well. Just a suggestion :) |
Co-authored-by: Marc Best <marcbest123@gmail.com>
Co-authored-by: Marc Best <marcbest123@gmail.com>
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.
Sign me up 😄🙌
Is this different than: https://github.com/rails/globalid#signed-global-ids ? This API seems nicer to me, but I'm confused about how this might work with |
Co-authored-by: David McCoy <davidmccoy@users.noreply.github.com>
@swanson GlobalID is for polymorphism. When the passed ID might respond to any number of classes. This is for when it's not that. We can use a much simpler API. So: Use signed_id when you're only referring to a single concrete class, use gid when referring to any number of classes. |
(Which helps justify the lack of a default expiration date, cc @kaspth)
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.
Some final nits. Dig the purpose versioning explanation, hadn't spotted that usage idea, so cool that we clarified — and the verifier exposure ✌️
Co-authored-by: Kasper Timm Hansen <kaspth@gmail.com>
Thanks for the review @kaspth! Looking good now ✌️ |
The signed id feature introduced in #39313 can cause loading issues since it may try to generate a key before the secret key base has been set. To prevent this wrap the secret initialization in a lambda.
|
||
# :nodoc: | ||
def combine_signed_id_purposes(purpose) | ||
[ name.underscore, purpose.to_s ].compact_blank.join("/") |
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.
Using combined_signed_id_purpose
can generate identical 'purposes'. For example:
[User.name.underscore, "profile"].compact.join('/') # user/profile
[User::Profile.name.underscore, nil].compact.join('/') # user/profile
Does the possible overlap present enough of a concern to be nervous? I believe this'd mean:
user = User.find_by(name: '')
signed_id = user.signed_id(purpose: 'profile')
User::Profile.find_signed!(signed_id) # returns the profile with the same ID as the found user
If so - does it make sense to move the name into the key used via the signed_id_verifier
(i.e. each model has a separate key). Happy to open a PR if so.
Hello, |
Want to make sure that those methods work on relation and return expected result when retation has or doesn't have any records. Those methods are delegated by https://github.com/rails/rails/blob/7cb451346618811796efce1f8a2bf576b8e4999c/activerecord/lib/active_record/relation/delegation.rb#L21, https://github.com/rails/rails/blob/7cb451346618811796efce1f8a2bf576b8e4999c/activerecord/lib/active_record/relation/delegation.rb#L95-L114, https://github.com/rails/rails/blob/7cb451346618811796efce1f8a2bf576b8e4999c/activerecord/lib/active_record/relation/delegation.rb#L56-L78 as I understand. Related to rails#39313
This scenario should not happen and now it raises a `ActiveSupport::MessageVerifier::InvalidSignature` error. Could be related to this new feature in 6.1 https://blog.saeloun.com/2020/05/20/rails-6-1-adds-support-for-signed-ids-to-active-record.html rails/rails#39313 And various issues that were supposedly fixed rails/rails#41233
This scenario should not happen and now it raises a `ActiveSupport::MessageVerifier::InvalidSignature` error. Could be related to this new feature in 6.1 https://blog.saeloun.com/2020/05/20/rails-6-1-adds-support-for-signed-ids-to-active-record.html rails/rails#39313 And various issues that were supposedly fixed rails/rails#41233
@uxxman For that use case you could simply put the current password in the purpose User.first.signed_id expires_in: 15.minutes, purpose: User.first.password_digest |
@dommmel this won't work when you have to find the user by signed_id find_signed since we cannot know what purpose to use when finding |
@uxxman You're right. That won't work as such. I guess you could use a token that combines the above with another (static) signed id and then split them up again later, using the first part for finding and the second for additional verification. Then again, there might be problems with that as well. I'll stop commenting with half baked ideas now ;-) |
Add support for finding records based on signed ids, which are tamper-proof, verified ids that can be set to expire and scoped with a purpose. This is particularly useful for things like password reset or email verification, where you want the bearer of the signed id to be able to interact with the underlying record, but usually only within a certain time period.