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

Add signed ids to Active Record #39313

Merged
merged 14 commits into from
May 17, 2020
Merged

Add signed ids to Active Record #39313

merged 14 commits into from
May 17, 2020

Conversation

dhh
Copy link
Member

@dhh dhh commented May 17, 2020

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.

signed_id = User.first.signed_id expires_in: 15.minutes, purpose: :password_reset

User.find_signed signed_id # => nil, since the purpose does not match

travel 16.minutes
User.find_signed signed_id # => nil, since the signed id has expired

travel_back
User.find_signed signed_id, purpose: :password_reset # => User.first

User.find_signed! "bad data" # => ActiveSupport::MessageVerifier::InvalidSignature

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.
@HarlemSquirrel
Copy link

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.

activerecord/CHANGELOG.md Outdated Show resolved Hide resolved
@dhh
Copy link
Member Author

dhh commented May 17, 2020

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>
Copy link
Contributor

@sogamoso sogamoso left a 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?

activerecord/CHANGELOG.md Show resolved Hide resolved
@uxxman
Copy link
Contributor

uxxman commented May 17, 2020

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 :)

dhh and others added 2 commits May 17, 2020 07:00
Co-authored-by: Marc Best <marcbest123@gmail.com>
Co-authored-by: Marc Best <marcbest123@gmail.com>
Copy link
Contributor

@kaspth kaspth left a comment

Choose a reason for hiding this comment

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

Sign me up 😄🙌

activerecord/lib/active_record/signed_id.rb Show resolved Hide resolved
activerecord/lib/active_record/signed_id.rb Show resolved Hide resolved
activerecord/lib/active_record/signed_id.rb Show resolved Hide resolved
activerecord/test/cases/signed_id_test.rb Show resolved Hide resolved
activerecord/test/cases/signed_id_test.rb Show resolved Hide resolved
@swanson
Copy link
Contributor

swanson commented May 17, 2020

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 GlobalId or what the main differences are. When would you recommend using one over the other?

@dhh
Copy link
Member Author

dhh commented May 17, 2020

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

dhh added 2 commits May 17, 2020 10:44
(Which helps justify the lack of a default expiration date, cc @kaspth)
Copy link
Contributor

@kaspth kaspth left a 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 ✌️

activerecord/test/cases/signed_id_test.rb Outdated Show resolved Hide resolved
activerecord/test/cases/signed_id_test.rb Outdated Show resolved Hide resolved
dhh and others added 2 commits May 17, 2020 10:57
Co-authored-by: Kasper Timm Hansen <kaspth@gmail.com>
@dhh
Copy link
Member Author

dhh commented May 17, 2020

Thanks for the review @kaspth! Looking good now ✌️

@crawler
Copy link
Contributor

crawler commented May 23, 2020

@dhh This is useful and elegant looking feature, but i noticed that the .find_signed method maybe needs to use a .primary_key instead of the :id Symbol, as the key to find_by parameter. Please take look at the two changed lines of code in my request 😃 #39404
Thank you!

pixeltrix added a commit that referenced this pull request May 28, 2020
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("/")
Copy link
Contributor

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.

@rmkni
Copy link

rmkni commented Jul 28, 2020

Hello,
Is it possible to use signed_ids to secure ids in URL?
Instead of having GET object/12345 exposed, could it be possible to sign ids to prevent request forgery attacks?
Is there any performance issue using find_signed instead of find_by?

jsugarman added a commit to ministryofjustice/Claim-for-Crown-Court-Defence that referenced this pull request Jun 22, 2021
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
jsugarman added a commit to ministryofjustice/Claim-for-Crown-Court-Defence that referenced this pull request Jun 22, 2021
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
@dommmel
Copy link

dommmel commented Mar 22, 2022

The current implementation of SignedId doesn't handle one time usage cases, like password reset token should automatically expire if used one time.

@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

@uxxman
Copy link
Contributor

uxxman commented Mar 23, 2022

@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

@dommmel
Copy link

dommmel commented Mar 23, 2022

@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 ;-)

@uxxman
Copy link
Contributor

uxxman commented Mar 23, 2022

@dommmel if you are also looking for a solution for this you can checkout this pull request #44189

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

Successfully merging this pull request may close these issues.

None yet