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 expiring, databaseless password reset tokens #682

Closed
wants to merge 13 commits into from

Conversation

derekprior
Copy link
Contributor

@derekprior derekprior commented May 15, 2016

It is a best security practice for password reset token to expire after
some amount of time. In Clearance 1.x, this was not the case. A password
reset email could be used months after it was originally sent so long as
no other password reset was ever completed.

In this change, password resets expire after 15 minutes (configurable)
or after the user successfully changes their password in any manner
(whichever comes first).

The token, confusingly called confirmation_token, is no longer stored
in the database. Instead, we use ActiveSupport::MessageVerifier to
sign the token and validate it when it is used. The message verifier is
configurable in case developers want to use something else.

In a future refactoring, I'd like to introduce a layer between Clearance
and ActiveSupport::MessageVerifier to make the API a bit more pleasant
to use, but this is an exercise for a future PR. For instance, I'd
prefer that the Clearance abstraction generate and validate tokens only
by taking a user object (and using the Clearance configuration).

Closes #465

- Remove support for Ruby 1.9, 2.0, and 2.1
- Remove support for Rails 3.1, 3.2, 4.0, and 4.1
These messages are used to tell users they can't access a page without
signing in, that their password is incorrect, etc. These are much closer
to error or alert states than notice.
`PasswordsController#url_after_create` was never called by our code.
These `respond_to` checks were added to support changing Rails API as
new versions of Rails were released. Now that we support 4.2 or newer,
we don't need them.
* Remove deprecated parameter handling
* Order actions more logically
* Update to double quotes
expect(email.html_part.body).to include(link)
expect(email.html_part.body).to have_css(
"a",
text: I18n.t("clearance_mailer.change_password.link_text")

Choose a reason for hiding this comment

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

Put a comma after the last parameter of a multiline method call.

user_model = Clearance.configuration.user_model

begin
user_id, encrypted_password, expiration = verifier.verify(token)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe encrypted_password shouldn't be part of what we sign here? It allows us to invalidate any tokens as soon as the password is changed but it also means the encrypted password is part of the payload (signed). I dont know that the tradeoff is worth it. Curious for additional thoughts here...

Copy link
Contributor

Choose a reason for hiding this comment

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

What about storing a hash of the encrypted password? The token would still change with the password without leaking anything.

I can't think of reason you wouldn't provide a user with their encrypted password, but it makes me nervous anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it makes me nervous because I'd be providing anyone that can access that email access to their encrypted password. I'll play with it some to see what I can come up with.

Hashing the encrypted password is a decent idea. Complicates the lookup a tiny bit, but it may be worth it.

@jferris
Copy link
Contributor

jferris commented May 16, 2016

This is a great idea.

@@ -56,6 +56,8 @@ Clearance.configure do |config|
config.routes = true
config.httponly = false
config.mailer_sender = "[email protected]"
config.message_verifier = ActiveSupport::MessageVerifier.new(secret_key_base)
config.password_reset_time_limit = 15.minutes
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice.

@geoffharcourt
Copy link
Contributor

I had to implement expiring reset tokens in Clearance on a project last week. Very excited to see this become something upstream (and to remove dependency on the database).

@tute
Copy link
Contributor

tute commented May 16, 2016

This also fixes #612. Thank you!

It is a best security practice for password reset token to expire after
some amount of time. In Clearance 1.x, this was not the case. A password
reset email could be used months after it was originally sent so long as
no other password reset was ever completed.

In this change, password resets expire after 15 minutes (configurable)
or after the user successfully changes their password in any manner
(whichever comes first).

The token, confusingly called `confirmation_token`, is no longer stored
in the database. Instead, we use `ActiveSupport::MessageVerifier` to
sign the token and validate it when it is used. The message verifier is
configurable in case developers want to use something else.

In a future refactoring, I'd like to introduce a layer between Clearance
and `ActiveSupport::MessageVerifier` to make the API a bit more pleasant
to use, but this is an exercise for a future PR. For instance, I'd
prefer that the Clearance abstraction generate and validate tokens only
by taking a user object (and using the Clearance configuration).
This cleans up some of the duplication of knowledge for how password
reset tokens are generated and allows us to move tests for the various
ways a reset token can be invalid into unit tests.
We were previously using the encrypted password as part of the signed
password reset token. Theoretically, emailing this token out could
expose the encrypted password to some adversary who would then be able
to do offline attacks against it.

This would likely not be very successful, but in an abundance of
caution, this change exposes an MD5'd version of the encrypted password
instead.
token = Clearance.configuration.message_verifier.generate([
user.id,
Digest::MD5.hexdigest(user.encrypted_password),
Clearance.configuration.password_reset_time_limit.from_now

Choose a reason for hiding this comment

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

Put a comma after the last item of a multiline array.

@robinvdvleuten
Copy link

Is there still any effort to get this into Clearance?

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.