has_editable_password 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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: