-
-
Notifications
You must be signed in to change notification settings - Fork 468
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
Set an expiration for password reset tokens #532
Changes from all commits
d073537
19035fd
11a6917
041f77a
18b94f5
83c2c4c
54c0880
597b156
0103fcc
7b6fa2a
ee54dda
7583ba9
1d39226
a6e9f2e
e9eb533
98f009f
63a7043
e0b035a
ad444c3
393be4c
e25e13d
7c9a250
f8ec321
ea1630f
4e12f79
3ef2427
8cb3007
9867298
15d0be8
0348bcf
c39d235
f36db0e
5f233fc
efb85c5
8bc26b9
78ca920
363954f
58ad25d
8cb433c
1986551
5b9d26a
9024ac2
fa3af5f
d7ee951
fdcede4
d27f72a
c360398
dfb10c3
1ac223a
1e4870b
2c2a317
42153c0
91b0bab
dbf0acb
caccebf
c41a02a
498488e
84ddd55
d2cfb56
cc0858b
cf0c985
8492fcc
2962f39
70bce4a
0ec2fa5
d49f107
d5ad597
a3c73c7
e8358da
3bd868e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,6 +32,9 @@ The generator: | |
* Creates an initializer to allow further configuration. | ||
* Creates a migration that either creates a users table or adds any necessary | ||
columns to the existing table. | ||
* Creates a `PasswordReset` model. | ||
* Creates a migration to remove the `confirmation_token` column from the users | ||
table if it exists. | ||
|
||
## Configure | ||
|
||
|
@@ -47,6 +50,7 @@ Clearance.configure do |config| | |
config.routes = true | ||
config.httponly = false | ||
config.mailer_sender = '[email protected]' | ||
config.password_reset_time_limit = 20.minutes | ||
config.password_strategy = Clearance::PasswordStrategies::BCrypt | ||
config.redirect_url = '/' | ||
config.secure_cookie = false | ||
|
@@ -106,15 +110,34 @@ helpers. For example: | |
|
||
### Password Resets | ||
|
||
When a user resets their password, Clearance delivers them an email. You | ||
should change the `mailer_sender` default, used in the email's "from" header: | ||
When a user resets their password, Clearance sends them an email. | ||
|
||
By default, the password reset token expires in 15 minutes. You can change the | ||
time limit by passing in an `ActiveSupport::Duration` to | ||
`config.password_reset_time_limit`. | ||
|
||
You should also change the `mailer_sender` default, which used in the email's | ||
"from" header: | ||
|
||
```ruby | ||
Clearance.configure do |config| | ||
config.mailer_sender = '[email protected]' | ||
config.password_reset_time_limit = 1.hour | ||
end | ||
``` | ||
|
||
*Existing users*: If your app is already using Clearance but it does not have | ||
the token expiration feature, you can generate and run the migrations: | ||
|
||
```shell | ||
rails generate clearance:password_reset_migration | ||
``` | ||
|
||
This will create a migration for the new password_resets table. If there are any | ||
existing password resets (i.e. any user with a confirmation token), those will | ||
be migrated over to the new table with its expiration set to the time limit | ||
configured. | ||
|
||
### Integrating with Rack Applications | ||
|
||
Clearance adds its session to the Rack environment hash so middleware and other | ||
|
@@ -241,8 +264,8 @@ If you are using an earlier version of Rails, you can override the | |
|
||
```ruby | ||
class PasswordsController < Clearance::PasswordsController | ||
def deliver_email(user) | ||
ClearanceMailer.delay.change_password(user) | ||
def deliver_email(password_reset) | ||
ClearanceMailer.delay.change_password(password_reset) | ||
end | ||
end | ||
``` | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
class PasswordReset < ActiveRecord::Base | ||
before_create :generate_token, :generate_expiration_timestamp | ||
|
||
belongs_to :user | ||
|
||
validates :user_id, presence: true | ||
|
||
delegate :email, to: :user, prefix: true | ||
|
||
def self.active_for(user_id) | ||
where( | ||
"#{Clearance.configuration.user_id_parameter} = ? AND expires_at > ?", | ||
user_id, | ||
Time.zone.now | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Put a comma after the last parameter of a multiline method call. |
||
) | ||
end | ||
|
||
def self.time_limit | ||
Clearance.configuration.password_reset_time_limit | ||
end | ||
|
||
def complete(new_password) | ||
reset_successful = false | ||
|
||
transaction do | ||
unless user.update_password(new_password) | ||
raise ActiveRecord::Rollback | ||
end | ||
|
||
deactivate_user_resets | ||
reset_successful = true | ||
end | ||
|
||
reset_successful | ||
end | ||
|
||
def deactivate | ||
update_attributes(expires_at: Time.zone.now) | ||
end | ||
|
||
def expired? | ||
expires_at <= Time.zone.now | ||
end | ||
|
||
private | ||
|
||
def active? | ||
!expired? | ||
end | ||
|
||
def deactivate_user_resets | ||
Clearance::PasswordResetsDeactivator.new(user).run | ||
end | ||
|
||
def generate_token | ||
self.token = Clearance::Token.new | ||
end | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Extra empty line detected at body end. |
||
def generate_expiration_timestamp | ||
self.expires_at = self.class.time_limit.from_now | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,8 @@ | ||
<p><%= t(".opening") %></p> | ||
<p><%= t(".opening", time_limit: distance_of_time_in_words(@time_limit)) %></p> | ||
|
||
<p> | ||
<%= link_to t(".link_text", default: "Change my password"), | ||
edit_user_password_url(@user, token: @user.confirmation_token.html_safe) %> | ||
edit_user_password_url(@password_reset.user, token: @password_reset.token) %> | ||
</p> | ||
|
||
<p><%= raw t(".closing") %></p> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
<%= t(".opening") %></p> | ||
<%= t(".opening", time_limit: distance_of_time_in_words(@time_limit)) %> | ||
|
||
<%= edit_user_password_url(@user, token: @user.confirmation_token.html_safe) %> | ||
<%= edit_user_password_url(@password_reset.user, token: @password_reset.token) %> | ||
|
||
<%= raw t(".closing") %> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
class CreatePasswordResets < ActiveRecord::Migration | ||
def change | ||
create_table :password_resets do |t| | ||
t.integer :user_id, null: false | ||
t.string :token, null: false | ||
t.datetime :expires_at, null: false | ||
t.timestamps null: false | ||
end | ||
|
||
add_index :password_resets, [:user_id, :expires_at] | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
class RemoveConfirmationTokenFromUsers < ActiveRecord::Migration | ||
def up | ||
execute <<-SQL | ||
INSERT INTO password_resets | ||
(user_id, token, expires_at, created_at, updated_at) | ||
SELECT | ||
users.id, | ||
users.confirmation_token, | ||
'#{expiration_timestamp}', | ||
CURRENT_TIMESTAMP, | ||
CURRENT_TIMESTAMP | ||
FROM users | ||
WHERE users.confirmation_token IS NOT NULL | ||
SQL | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good thinking! |
||
|
||
remove_column :users, :confirmation_token | ||
end | ||
|
||
def expiration_timestamp | ||
Clearance. | ||
configuration. | ||
password_reset_time_limit. | ||
from_now | ||
end | ||
|
||
def down | ||
add_column :users, :confirmation_token, :string, limit: 128 | ||
|
||
execute <<-SQL | ||
UPDATE users | ||
SET confirmation_token = | ||
(SELECT token FROM password_resets | ||
WHERE users.id = password_resets.user_id) | ||
SQL | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -47,6 +47,11 @@ class Configuration | |
# @return [String] | ||
attr_accessor :mailer_sender | ||
|
||
# The time limit given to the user before their password reset token expires | ||
# Defaults to 20.minutes (900 seconds) | ||
# @return [ActiveSupport::Duration] | ||
attr_accessor :password_reset_time_limit | ||
|
||
# The password strategy to use when authenticating and setting passwords. | ||
# Defaults to `Clearance::PasswordStrategies::BCrypt`. | ||
# @return [Class #authenticated? #password=] | ||
|
@@ -93,6 +98,7 @@ def initialize | |
@cookie_name = "remember_token" | ||
@httponly = false | ||
@mailer_sender = '[email protected]' | ||
@password_reset_time_limit = 20.minutes | ||
@redirect_url = '/' | ||
@routes = true | ||
@secure_cookie = false | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
module Clearance | ||
class PasswordResetsDeactivator | ||
def initialize(user) | ||
@user = user | ||
end | ||
|
||
def run | ||
active_password_resets.each(&:deactivate) | ||
end | ||
|
||
private | ||
|
||
attr_reader :user | ||
|
||
def active_password_resets | ||
PasswordReset.active_for(user) | ||
end | ||
end | ||
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.
Should this be a template instead of in
app/models
, like https://github.com/thoughtbot/clearance/blob/master/lib/generators/clearance/install/templates/user.rb?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. Clearance models are internal to clearance and not generate. User is a template because we expect you already have a user class of some sort.