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 (with encrypted tokens) #834

Closed
wants to merge 4 commits into from
Closed

Add expiring, databaseless password reset tokens (with encrypted tokens) #834

wants to merge 4 commits into from

Conversation

ignatiusreza
Copy link

Reopened against master from #823

Hi guys! I saw that #682 got stale and with #817 on the way, I thought that it's a good time to try to revive this effort..

This PR is a rebased of #682, with extra changes on top to address the concern related to the risk of exposing the existing encrypted password..

In summary, the differences between this PR and #682 is, in this PR reset token are generated using ActiveSupport::MessageEncryptor instead of ActiveSupport::MessageVerifier..

As can be seen from the module documentation, ActiveSupport::MessageEncryptor is designed as a simple way to encrypt values which get stored somewhere you don't trust., which perfectly fit our needs..

As part of the changes, I introduces a new class Clearance::Tokenizer to wrap the logic around ActiveSupport::MessageEncryptor and changes the configuration option from configuration.message_verifier to configuration.tokenizer.. I think, in the long term, we could somehow merge Clearance::Token and Clearance::Tokenizer (or delegate token generation logic inside Clearance::Token to Clearance::Tokenizer)..

Derek Prior & Melissa Xie and others added 3 commits April 26, 2019 08:59
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.

put :update, params: update_parameters(
user,
new_password: "",
new_password: ""

Choose a reason for hiding this comment

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

Style/TrailingCommaInArguments: Put a comma after the last parameter of a multiline method call.

user_id: user.id,
token: user.reload.confirmation_token,
user_id: 1,
token: token

Choose a reason for hiding this comment

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

Style/TrailingCommaInHashLiteral: Put a comma after the last item of a multiline hash.

Timecop.freeze(1.day.from_now) do
get :edit, params: {
user_id: 1,
token: token

Choose a reason for hiding this comment

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

Style/TrailingCommaInHashLiteral: Put a comma after the last item of a multiline hash.

get :edit, params: {
user_id: 1,
token: user.confirmation_token + "a",
token: "a"

Choose a reason for hiding this comment

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

Style/TrailingCommaInHashLiteral: Put a comma after the last item of a multiline hash.

get :edit, params: {
user_id: user,
token: token_for(user)

Choose a reason for hiding this comment

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

Style/TrailingCommaInHashLiteral: Put a comma after the last item of a multiline hash.

token = Clearance::PasswordResetToken.generate_for(user)

Timecop.freeze(1.day.from_now) do
expect(Clearance::PasswordResetToken.find_user(user.id, token)).to be nil

Choose a reason for hiding this comment

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

Metrics/LineLength: Line is too long. [81/80]

user = create(:user)
token = Clearance::PasswordResetToken.generate_for(user)

expect(Clearance::PasswordResetToken.find_user(user.id + 1, token)).to be nil

Choose a reason for hiding this comment

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

Metrics/LineLength: Line is too long. [83/80]

spec/clearance/password_reset_token_spec.rb Outdated Show resolved Hide resolved
spec/clearance/password_reset_token_spec.rb Outdated Show resolved Hide resolved
module Generators
class UpgradeGenerator < Rails::Generators::Base
include Rails::Generators::Migration
source_root File.expand_path("../templates", __FILE__)

Choose a reason for hiding this comment

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

Style/ExpandPathArguments: Use expand_path('templates', dir) instead of expand_path('../templates', FILE).

attr_reader :encryptor

def decrypt(token)
begin

Choose a reason for hiding this comment

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

Style/RedundantBegin: Redundant begin block detected.

lib/clearance/configuration.rb Outdated Show resolved Hide resolved
def find_user_for_update
find_user_by_id_and_confirmation_token
def find_user_by_password_reset_token(user_id, token)
@user ||= Clearance::PasswordResetToken.find_user(user_id, token)

Choose a reason for hiding this comment

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

Naming/MemoizedInstanceVariableName: Memoized variable @user does not match method name find_user_by_password_reset_token. Use @find_user_by_password_reset_token instead.

replace default tokenizer from ActiveSupport::MessageVerifier
with ActiveSupport::MessageEncryptor, since the former can
potentially leaks user private information

ActiveSupport::MessageEncryptor are configured to work using key
generated from user encrypted_password and the application
secret_key_base.. this ensure that the token automatically
get invalidated when user changed their password..
@ignatiusreza ignatiusreza deleted the dp-password-resets-encrypted branch August 17, 2021 15:19
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