has_editable_password 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +15 -0
- data/lib/has_editable_password.rb +67 -0
- data/lib/version.rb +1 -0
- data/spec/has_editable_password_spec.rb +225 -0
- data/spec/spec_helper.rb +41 -0
- metadata +93 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
MWNlODUyZGIyZDllODFkYzI3ZGZjMDdmNWVlNjQ1MTFlNzIwYTA3NA==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
ZjgxY2E2YjkyMjQ2N2EwMzc5Yzg3MTlhMGEyMDgzZTZmMDVmYjcyYg==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
NWM4MTQwNTdmYjIyMzdlMzQyZDU4ODg3NDg1NDM1OGFjNTljYzI4NGEzMWJi
|
10
|
+
ZWE4NjU5NzYyZGY0YzQ3ZTk4NTc5Y2FjM2RkMGE3ZjhlYjlkNzRjODhmM2U3
|
11
|
+
OGRlNWEwYmI3OGEzOGNkNDliZjY1NDQwMjI5MTA3ZGFhYzIyNTA=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
YWIxOGNjNDBlN2NkNTI5NDExMmQwZjQzYTIzYjQyMGM4ZDE1YTU0ZjAxMWRi
|
14
|
+
OWYzZTNiYzk3NTJmNDc3NDBkOTBlOWI5OGI3YjU2ZDI5NzE5YTk0MGE4MmU4
|
15
|
+
ZmIyNDAyNGEyNzE2YmYxNGM1MjZlMTFmOTQzNzU4NjFlNjZmOTU=
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'active_model/secure_password'
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
module HasEditablePassword
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
include ActiveModel::SecurePassword
|
8
|
+
|
9
|
+
included do
|
10
|
+
has_secure_password
|
11
|
+
|
12
|
+
attr_writer :current_password
|
13
|
+
attr_writer :recovery_token
|
14
|
+
|
15
|
+
validate :password_change, on: :update, if: :password_digest_changed?
|
16
|
+
|
17
|
+
def password=(value)
|
18
|
+
@old_password_digest = self.password_digest unless @old_password_digest or self.password_digest.blank?
|
19
|
+
super(value)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def generate_recovery_token(options = {})
|
24
|
+
token = SecureRandom.urlsafe_base64(options.delete(:length) || 32)
|
25
|
+
self.password_recovery_token = BCrypt::Password.create(token)
|
26
|
+
self.password_recovery_token_creation = Time.now
|
27
|
+
save unless options.delete(:save) == false
|
28
|
+
token
|
29
|
+
end
|
30
|
+
|
31
|
+
def valid_recovery_token?
|
32
|
+
recovery_token_match? and !recovery_token_expired?
|
33
|
+
end
|
34
|
+
|
35
|
+
def current_password_match?
|
36
|
+
if @current_password
|
37
|
+
if @old_password_digest
|
38
|
+
BCrypt::Password.new(@old_password_digest) == @current_password
|
39
|
+
else
|
40
|
+
# almost same as #authenticate (returns true instead of the object)
|
41
|
+
BCrypt::Password.new(self.password_digest) == @current_password
|
42
|
+
end
|
43
|
+
else
|
44
|
+
false
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
def recovery_token_expired?
|
50
|
+
# 86400 = seconds in a day
|
51
|
+
(Time.now - self.password_recovery_token_creation).round >= 86400
|
52
|
+
end
|
53
|
+
|
54
|
+
def recovery_token_match?
|
55
|
+
BCrypt::Password.new(self.password_recovery_token) == @recovery_token
|
56
|
+
rescue
|
57
|
+
false
|
58
|
+
end
|
59
|
+
|
60
|
+
def allow_password_change?
|
61
|
+
valid_recovery_token? or current_password_match?
|
62
|
+
end
|
63
|
+
|
64
|
+
def password_change
|
65
|
+
errors[:password] << 'Unauthorized to change the password' unless allow_password_change?
|
66
|
+
end
|
67
|
+
end
|
data/lib/version.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
VERSION = '0.0.1'
|
@@ -0,0 +1,225 @@
|
|
1
|
+
require 'spec_helper.rb'
|
2
|
+
|
3
|
+
describe HasEditablePassword do
|
4
|
+
let(:user) { User.new(password: 'secret', password_confirmation: 'secret') }
|
5
|
+
|
6
|
+
# Since this is an extension of has_secure_password ensure that all the
|
7
|
+
# methods defined by has_secure_password are still there
|
8
|
+
|
9
|
+
it 'has an #authenticate method' do
|
10
|
+
expect(user).to respond_to :authenticate
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'has a #password= method' do
|
14
|
+
expect(user).to respond_to :password=
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'has a password_confirmation= method' do
|
18
|
+
expect(user).to respond_to :password=
|
19
|
+
end
|
20
|
+
|
21
|
+
# Additional accessors for current password or password recovery token
|
22
|
+
|
23
|
+
it 'has a #current_password= method' do
|
24
|
+
expect(user).to respond_to :current_password=
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'has a #recovery_token= method' do
|
28
|
+
expect(user).to respond_to :recovery_token=
|
29
|
+
end
|
30
|
+
|
31
|
+
describe '#password=' do
|
32
|
+
it 'sets the password_digest field to the hash of the password' do
|
33
|
+
user.password = 'secret'
|
34
|
+
expect(BCrypt::Password.new(user.password_digest)).to eq 'secret'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe '#generate_recovery_token' do
|
39
|
+
it 'returns a url-safe base64 string' do
|
40
|
+
token = user.generate_recovery_token
|
41
|
+
expect(token).to match(/^[a-z0-9\-_]+=*$/i)
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'returns a 43 bytes token unless specified' do
|
45
|
+
token = user.generate_recovery_token
|
46
|
+
expect(token.size).to eq 43
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'returns token larger than specified size' do
|
50
|
+
token = user.generate_recovery_token length: 100
|
51
|
+
token.size.should be >= 100
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'sets the password_recovery_token attribute with the hash of the token' do
|
55
|
+
token = user.generate_recovery_token
|
56
|
+
expect(BCrypt::Password.new(user.password_recovery_token)).to eq token
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'sets the password_recovery_token_creation to Time.now' do
|
60
|
+
user.generate_recovery_token
|
61
|
+
expect(user.password_recovery_token_creation.round).to eq Time.now.round
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'calls #save unless specified' do
|
65
|
+
expect(user).to receive(:save)
|
66
|
+
user.generate_recovery_token
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'does not call #save if specified' do
|
70
|
+
expect(user).to_not receive(:save)
|
71
|
+
user.generate_recovery_token(save: false)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe '#valid_recovery_token?' do
|
76
|
+
context 'a token was never generated' do
|
77
|
+
it 'returns false' do
|
78
|
+
user.recovery_token = "deadbeef"
|
79
|
+
expect(user.valid_recovery_token?).to be_false
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
context 'the creation is more than 24 hours ago' do
|
84
|
+
it 'returns false' do
|
85
|
+
token = user.generate_recovery_token
|
86
|
+
user.password_recovery_token_creation = Time.now - 86401
|
87
|
+
user.recovery_token = token # Even if the token is correct
|
88
|
+
expect(user.valid_recovery_token?).to be_false
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
context 'the creation is less than 24 hours ago' do
|
93
|
+
it 'returns false if the token is a random string' do
|
94
|
+
user.generate_recovery_token
|
95
|
+
user.recovery_token = "deadbeef"
|
96
|
+
expect(user.valid_recovery_token?).to be_false
|
97
|
+
end
|
98
|
+
|
99
|
+
context 'the token is a base64 string' do
|
100
|
+
it 'returns false if the token does not match' do
|
101
|
+
user.generate_recovery_token
|
102
|
+
user.recovery_token = SecureRandom.urlsafe_base64
|
103
|
+
expect(user.valid_recovery_token?).to be_false
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'returns true if the token matches' do
|
107
|
+
token = user.generate_recovery_token
|
108
|
+
user.recovery_token = token
|
109
|
+
expect(user.valid_recovery_token?).to be_true
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
describe '#current_password_match?' do
|
116
|
+
context 'current_password is not been set' do
|
117
|
+
it 'returns false' do
|
118
|
+
expect(user.current_password_match?).to be_false
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
context 'current_password is set' do
|
123
|
+
context 'current_password does not match' do
|
124
|
+
before { user.current_password = 's3cret' }
|
125
|
+
|
126
|
+
it 'returns false' do
|
127
|
+
expect(user.current_password_match?).to be_false
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
context 'current_password does match' do
|
132
|
+
before { user.current_password = 'secret' }
|
133
|
+
|
134
|
+
it 'returns true' do
|
135
|
+
expect(user.current_password_match?).to be_true
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
context 'password_digest has been modified' do
|
140
|
+
before { user.password = 'new_secret' }
|
141
|
+
|
142
|
+
context 'current_password is set to the previous password' do
|
143
|
+
before { user.current_password = 'secret' }
|
144
|
+
|
145
|
+
it 'returns true' do
|
146
|
+
expect(user.current_password_match?).to be_true
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
context 'current_password is set to the new password' do
|
151
|
+
before { user.current_password = 'new_secret' }
|
152
|
+
|
153
|
+
it 'returns true' do
|
154
|
+
expect(user.current_password_match?).to be_false
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
describe 'password editing validation' do
|
162
|
+
let(:user) { User.new(password: 'banana', password_confirmation: 'banana') }
|
163
|
+
|
164
|
+
context 'on create' do
|
165
|
+
it 'is valid' do
|
166
|
+
expect(user.valid?).to be_true
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
context 'on update' do
|
171
|
+
context 'password_digest was not touched' do
|
172
|
+
before { user.stub(:password_digest_changed?).and_return false }
|
173
|
+
|
174
|
+
it 'is valid' do
|
175
|
+
expect(user.valid?(:update)).to be_true
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
context 'password_digest was modified' do
|
180
|
+
before { user.stub(:password_digest_changed?).and_return true }
|
181
|
+
|
182
|
+
context 'a valid token is set' do
|
183
|
+
let(:token) { user.generate_recovery_token }
|
184
|
+
|
185
|
+
it 'is valid' do
|
186
|
+
user.recovery_token = token
|
187
|
+
expect(user.valid?(:update)).to be_true
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
context 'an invalid valid token is set' do
|
192
|
+
let(:token) do
|
193
|
+
user.generate_recovery_token
|
194
|
+
"deadbeef"
|
195
|
+
end
|
196
|
+
|
197
|
+
it 'is not valid' do
|
198
|
+
user.recovery_token = token
|
199
|
+
expect(user.valid?(:update)).to be_false
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
context 'the current_password is valid' do
|
204
|
+
it 'is valid' do
|
205
|
+
user.current_password = 'banana'
|
206
|
+
expect(user.valid?(:update)).to be_true
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
context 'the current_password is invalid' do
|
211
|
+
it 'is valid' do
|
212
|
+
user.current_password = 'b4n4n4'
|
213
|
+
expect(user.valid?(:update)).to be_false
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
context 'neither is set' do
|
218
|
+
it 'is not valid' do
|
219
|
+
expect(user.valid?(:update)).to be_false
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
|
2
|
+
|
3
|
+
require 'has_editable_password.rb'
|
4
|
+
require 'active_model'
|
5
|
+
require 'bcrypt'
|
6
|
+
|
7
|
+
# Mock an ActiveRecord-like class.
|
8
|
+
# Not using ActiveRecord because we want this to be ORM-agnostic
|
9
|
+
class User
|
10
|
+
extend ActiveModel::Callbacks
|
11
|
+
define_model_callbacks :initialize, :find, :touch, :only => :after
|
12
|
+
define_model_callbacks :save, :create, :update, :destroy
|
13
|
+
|
14
|
+
include ActiveModel::Validations
|
15
|
+
include ActiveModel::Validations::HelperMethods
|
16
|
+
include HasEditablePassword
|
17
|
+
|
18
|
+
# we expect the user to define the password_digest field
|
19
|
+
attr_accessor :password_digest
|
20
|
+
attr_accessor :password_recovery_token
|
21
|
+
attr_accessor :password_recovery_token_creation
|
22
|
+
|
23
|
+
def initialize(hash = {})
|
24
|
+
hash.each do |k, v|
|
25
|
+
send("#{k}=", v)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def attributes
|
30
|
+
keys = [ 'password', 'password_confirmation' ]
|
31
|
+
values = keys.map do |k|
|
32
|
+
send(k)
|
33
|
+
end
|
34
|
+
|
35
|
+
Hash[keys.zip(values)]
|
36
|
+
end
|
37
|
+
|
38
|
+
def save
|
39
|
+
valid?
|
40
|
+
end
|
41
|
+
end
|
metadata
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: has_editable_password
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Francesco Boffa
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-12-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
type: :runtime
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ! '>='
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '0'
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ! '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activemodel
|
29
|
+
type: :runtime
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ! '>='
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bcrypt-ruby
|
43
|
+
type: :runtime
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ~>
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: 3.0.0
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 3.0.0
|
55
|
+
description: HasEditablePassword extends has_secure_password with updating capabilities.
|
56
|
+
On password update, the old password or a recovery token is asked.
|
57
|
+
email: fra.boffa@gmail.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- lib/has_editable_password.rb
|
63
|
+
- lib/version.rb
|
64
|
+
- spec/has_editable_password_spec.rb
|
65
|
+
- spec/spec_helper.rb
|
66
|
+
homepage: http://rubygems.org/gems/has_editable_password
|
67
|
+
licenses:
|
68
|
+
- MIT
|
69
|
+
metadata: {}
|
70
|
+
post_install_message:
|
71
|
+
rdoc_options: []
|
72
|
+
require_paths:
|
73
|
+
- lib
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - ! '>='
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ! '>='
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
requirements: []
|
85
|
+
rubyforge_project:
|
86
|
+
rubygems_version: 2.1.10
|
87
|
+
signing_key:
|
88
|
+
specification_version: 4
|
89
|
+
summary: has_secure_password with safe updating capabilities.
|
90
|
+
test_files:
|
91
|
+
- spec/has_editable_password_spec.rb
|
92
|
+
- spec/spec_helper.rb
|
93
|
+
has_rdoc:
|