authem 1.5.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.rspec +2 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +6 -0
  7. data/Appraisals +12 -0
  8. data/CHANGELOG.md +42 -0
  9. data/Gemfile +10 -0
  10. data/README.markdown +15 -1
  11. data/Rakefile +11 -5
  12. data/authem.gemspec +25 -0
  13. data/gemfiles/rails_4.0.gemfile +16 -0
  14. data/gemfiles/rails_4.1.gemfile +15 -0
  15. data/lib/authem.rb +4 -10
  16. data/lib/authem/controller.rb +50 -0
  17. data/lib/authem/errors/ambigous_role.rb +8 -0
  18. data/lib/authem/errors/unknown_role.rb +7 -0
  19. data/lib/authem/railtie.rb +12 -0
  20. data/lib/authem/role.rb +62 -0
  21. data/lib/authem/session.rb +41 -0
  22. data/lib/authem/support.rb +129 -0
  23. data/lib/authem/token.rb +5 -5
  24. data/lib/authem/user.rb +27 -13
  25. data/lib/authem/version.rb +1 -1
  26. data/lib/generators/authem/session/session_generator.rb +12 -0
  27. data/lib/generators/authem/session/templates/create_sessions.rb +15 -0
  28. data/lib/generators/authem/user/templates/create_table_migration.rb +22 -0
  29. data/lib/generators/authem/user/templates/model.rb +11 -0
  30. data/lib/generators/authem/user/user_generator.rb +13 -0
  31. data/spec/controller_spec.rb +413 -0
  32. data/spec/session_spec.rb +52 -0
  33. data/spec/spec_helper.rb +4 -0
  34. data/spec/support/active_record.rb +45 -0
  35. data/spec/support/i18n.rb +1 -0
  36. data/spec/support/time.rb +1 -0
  37. data/spec/token_spec.rb +10 -0
  38. data/spec/user_spec.rb +115 -0
  39. metadata +42 -112
  40. data/lib/authem/base_user.rb +0 -54
  41. data/lib/authem/config.rb +0 -21
  42. data/lib/authem/controller_support.rb +0 -51
  43. data/lib/authem/sorcery_user.rb +0 -24
  44. data/lib/generators/authem/model/model_generator.rb +0 -23
@@ -1,7 +1,7 @@
1
- require 'securerandom'
2
-
3
- class Authem::Token
4
- def self.generate
5
- SecureRandom.hex(20)
1
+ module Authem
2
+ class Token
3
+ def self.generate
4
+ SecureRandom.urlsafe_base64(45)
5
+ end
6
6
  end
7
7
  end
@@ -1,21 +1,35 @@
1
- module Authem::User
2
- extend ::ActiveSupport::Concern
3
- include Authem::BaseUser
1
+ require "authem/token"
4
2
 
5
- included do
6
- Authem::Config.user_class = self
3
+ module Authem
4
+ module User
5
+ extend ActiveSupport::Concern
7
6
 
8
- has_secure_password
7
+ included do
8
+ has_many :authem_sessions, as: :subject, class_name: "Authem::Session"
9
+ has_secure_password
9
10
 
10
- alias_method :original_authenticate, :authenticate
11
+ validates :email,
12
+ uniqueness: true,
13
+ presence: true,
14
+ format: { with: /\A\S+@\S+\z/, allow_blank: true }
11
15
 
12
- def authenticate(password)
13
- if password.present?
14
- original_authenticate(password)
15
- else
16
- false
16
+ before_create{ self.password_reset_token = Authem::Token.generate }
17
+ end
18
+
19
+ def email=(value)
20
+ super value.try(:downcase)
21
+ end
22
+
23
+ def reset_password(password, confirmation)
24
+ if password.blank?
25
+ errors.add :password, :blank
26
+ return false
17
27
  end
28
+
29
+ self.password = password
30
+ self.password_confirmation = confirmation
31
+
32
+ update_column :password_reset_token, Authem::Token.generate if save
18
33
  end
19
34
  end
20
-
21
35
  end
@@ -1,3 +1,3 @@
1
1
  module Authem
2
- VERSION = '1.5.0'.freeze
2
+ VERSION = "2.0.0".freeze
3
3
  end
@@ -0,0 +1,12 @@
1
+ require "rails/generators/active_record"
2
+
3
+ module Authem
4
+ class SessionGenerator < ActiveRecord::Generators::Base
5
+ argument :name, type: :string, default: 'unused but required by activerecord'
6
+ source_root File.expand_path("../templates", __FILE__)
7
+
8
+ def copy_session_migration
9
+ migration_template "create_sessions.rb", "db/migrate/create_authem_sessions.rb"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ class CreateAuthemSessions < ActiveRecord::Migration
2
+ def change
3
+ create_table :authem_sessions do |t|
4
+ t.string :role, null: false
5
+ t.references :subject, null: false, polymorphic: true
6
+ t.string :token, null: false, limit: 60
7
+ t.datetime :expires_at, null: false
8
+ t.integer :ttl, null: false
9
+ t.timestamps
10
+ end
11
+
12
+ add_index :authem_sessions, %i[expires_at token], unique: true
13
+ add_index :authem_sessions, %i[expires_at subject_type subject_id], name: 'index_authem_sessions_subject'
14
+ end
15
+ end
@@ -0,0 +1,22 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration
2
+ def change
3
+ create_table :<%= table_name %> do |t|
4
+ t.string :email, null: false
5
+ t.string :password_digest, null: false
6
+ t.string :password_reset_token, null: false, limit: 60
7
+ <% attributes.each do |attribute| -%>
8
+ <% if attribute.password_digest? -%>
9
+ t.string :password_digest<%= attribute.inject_options %>
10
+ <% else -%>
11
+ t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %>
12
+ <% end -%>
13
+ <% end -%>
14
+ <% if options[:timestamps] %>
15
+ t.timestamps
16
+ <% end -%>
17
+ end
18
+ <% attributes_with_index.each do |attribute| -%>
19
+ add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
20
+ <% end -%>
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ <% module_namespacing do -%>
2
+ class <%= class_name %> < <%= parent_class_name.classify %>
3
+ include Authem::User
4
+ <% attributes.select(&:reference?).each do |attribute| -%>
5
+ belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %>
6
+ <% end -%>
7
+ <% if attributes.any?(&:password_digest?) -%>
8
+ has_secure_password
9
+ <% end -%>
10
+ end
11
+ <% end -%>
@@ -0,0 +1,13 @@
1
+ require "rails/generators/active_record/model/model_generator"
2
+
3
+ module Authem
4
+ class UserGenerator < ActiveRecord::Generators::ModelGenerator
5
+ source_root File.expand_path("../templates", __FILE__)
6
+
7
+ private
8
+
9
+ def migration_template(_, migration_file_name)
10
+ super "create_table_migration.rb", migration_file_name
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,413 @@
1
+ require "spec_helper"
2
+
3
+ describe Authem::Controller do
4
+ class User < ActiveRecord::Base
5
+ self.table_name = :users
6
+ end
7
+
8
+ module MyNamespace
9
+ class SuperUser < ActiveRecord::Base
10
+ self.table_name = :users
11
+ end
12
+ end
13
+
14
+ class BaseController
15
+ include Authem::Controller
16
+
17
+ class << self
18
+ def helper_methods_list
19
+ @helper_methods_list ||= []
20
+ end
21
+
22
+ def helper_method(*methods)
23
+ helper_methods_list.concat methods
24
+ end
25
+ end
26
+
27
+ def clear_session!
28
+ session.clear
29
+ end
30
+
31
+ def reloaded
32
+ original_session, original_cookies = session, cookies
33
+
34
+ self.class.new.tap do |controller|
35
+ controller.class_eval do
36
+ define_method(:session){ original_session }
37
+ define_method(:cookies){ original_cookies }
38
+ end
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def session
45
+ @_session ||= HashWithIndifferentAccess.new
46
+ end
47
+
48
+ def cookies
49
+ @_cookies ||= Cookies.new
50
+ end
51
+ end
52
+
53
+ class Cookies < HashWithIndifferentAccess
54
+ attr_reader :expires_at
55
+
56
+ def permanent
57
+ self
58
+ end
59
+
60
+ alias_method :signed, :permanent
61
+
62
+ def []=(key, value)
63
+ if value.kind_of?(Hash) && value.key?(:expires)
64
+ @expires_at = value[:expires]
65
+ super key, value.fetch(:value)
66
+ else
67
+ super
68
+ end
69
+ end
70
+
71
+ def delete(key, *)
72
+ super key
73
+ end
74
+ end
75
+
76
+ def build_controller
77
+ controller_klass.new
78
+ end
79
+
80
+ let(:controller){ build_controller.tap{ |c| allow(c).to receive(:request){ request }}}
81
+ let(:view_helpers){ controller_klass.helper_methods_list }
82
+ let(:cookies){ controller.send(:cookies) }
83
+ let(:session){ controller.send(:session) }
84
+ let(:reloaded_controller){ controller.reloaded }
85
+ let(:request_url){ "http://example.com/foo" }
86
+ let(:request){ double("Request", url: request_url, xhr?: false) }
87
+
88
+ context "with one role" do
89
+ let(:user){ User.create(email: "joe@example.com") }
90
+ let(:controller_klass){ Class.new(BaseController){ authem_for :user }}
91
+
92
+ it "has current_user method" do
93
+ expect(controller).to respond_to(:current_user)
94
+ end
95
+
96
+ it "has sign_in_user method" do
97
+ expect(controller).to respond_to(:sign_in_user)
98
+ end
99
+
100
+ it "has clear_all_user_sessions_for method" do
101
+ expect(controller).to respond_to(:clear_all_user_sessions_for)
102
+ end
103
+
104
+ it "has require_user method" do
105
+ expect(controller).to respond_to(:require_user)
106
+ end
107
+
108
+ it "has user_signed_in? method" do
109
+ expect(controller).to respond_to(:user_signed_in?)
110
+ end
111
+
112
+ it "has redirect_back_or_to method" do
113
+ expect(controller).to respond_to(:redirect_back_or_to)
114
+ end
115
+
116
+ it "can clear all sessions using clear_all_sessions method" do
117
+ expect(controller).to receive(:clear_all_user_sessions_for).with(user)
118
+ controller.clear_all_sessions_for user
119
+ end
120
+
121
+ it "defines view helpers" do
122
+ expect(view_helpers).to include(:current_user)
123
+ expect(view_helpers).to include(:user_signed_in?)
124
+ end
125
+
126
+ it "raises error when calling clear_all_sessions_for with nil" do
127
+ expect{ controller.clear_all_sessions_for nil }.to raise_error(ArgumentError)
128
+ expect{ controller.clear_all_user_sessions_for nil }.to raise_error(ArgumentError)
129
+ end
130
+
131
+ it "can sign in user using sign_in_user method" do
132
+ controller.sign_in_user user
133
+ expect(controller.current_user).to eq(user)
134
+ expect(reloaded_controller.current_user).to eq(user)
135
+ end
136
+
137
+ it "can show status of current session with user_signed_in? method" do
138
+ expect{ controller.sign_in user }.to change(controller, :user_signed_in?).from(false).to(true)
139
+ expect{ controller.sign_out user }.to change(controller, :user_signed_in?).from(true).to(false)
140
+ end
141
+
142
+ it "can store session token in a cookie when :remember option is used" do
143
+ expect{ controller.sign_in user, remember: true }.to change(cookies, :size).by(1)
144
+ end
145
+
146
+ it "throws NotImplementedError when require strategy is not defined" do
147
+ message = "No strategy for require_user defined. Please define `deny_user_access` method in your controller"
148
+ expect{ controller.require_user }.to raise_error(NotImplementedError, message)
149
+ end
150
+
151
+ it "can require authenticated user with require_user method" do
152
+ def controller.deny_user_access; redirect_to :custom_path; end
153
+ expect(controller).to receive(:redirect_to).with(:custom_path)
154
+ expect{ controller.require_user }.to change{ session[:return_to_url] }.from(nil).to(request_url)
155
+ end
156
+
157
+ it "sets cookie expiration date when :remember options is used" do
158
+ controller.sign_in user, remember: true, ttl: 1.week
159
+ expect(cookies.expires_at).to be_within(1.second).of(1.week.from_now)
160
+ end
161
+
162
+ it "can restore user from cookie when session is lost" do
163
+ controller.sign_in user, remember: true
164
+ controller.clear_session!
165
+ expect(controller.reloaded.current_user).to eq(user)
166
+ end
167
+
168
+ it "does not use cookies by default" do
169
+ expect{ controller.sign_in user }.not_to change(cookies, :size)
170
+ end
171
+
172
+ it "returns session object on sign in" do
173
+ result = controller.sign_in_user(user)
174
+ expect(result).to be_kind_of(::Authem::Session)
175
+ end
176
+
177
+ it "allows to specify ttl using sign_in_user with ttl option" do
178
+ session = controller.sign_in_user(user, ttl: 40.minutes)
179
+ expect(session.ttl).to eq(40.minutes)
180
+ end
181
+
182
+ it "forgets user after session has expired" do
183
+ session = controller.sign_in(user)
184
+ session.update_column :expires_at, 1.minute.ago
185
+ expect(reloaded_controller.current_user).to be_nil
186
+ end
187
+
188
+ it "renews session ttl each time it is used" do
189
+ session = controller.sign_in(user, ttl: 1.day)
190
+ session.update_column :expires_at, 1.minute.from_now
191
+ reloaded_controller.current_user
192
+ expect(session.reload.expires_at).to be_within(1.second).of(1.day.from_now)
193
+ end
194
+
195
+ it "renews cookie expiration date each time it is used" do
196
+ session = controller.sign_in(user, ttl: 1.day, remember: true)
197
+ session.update_column :ttl, 30.days
198
+ reloaded_controller.current_user
199
+ expect(cookies.expires_at).to be_within(1.second).of(30.days.from_now)
200
+ end
201
+
202
+ it "can sign in using sign_in method" do
203
+ expect(controller).to receive(:sign_in_user).with(user, {})
204
+ controller.sign_in user
205
+ end
206
+
207
+ it "allows to specify ttl using sign_in method with ttl option" do
208
+ session = controller.sign_in(user, ttl: 40.minutes)
209
+ expect(session.ttl).to eq(40.minutes)
210
+ end
211
+
212
+ it "raises an error when trying to sign in unknown model" do
213
+ model = MyNamespace::SuperUser.create(email: "admin@example.com")
214
+ message = "Unknown authem role: #{model.inspect}"
215
+ expect{ controller.sign_in model }.to raise_error(Authem::UnknownRoleError, message)
216
+ end
217
+
218
+ it "raises an error when trying to sign in nil" do
219
+ expect{ controller.sign_in nil }.to raise_error(ArgumentError)
220
+ expect{ controller.sign_in_user nil }.to raise_error(ArgumentError)
221
+ end
222
+
223
+ it "has sign_out_user method" do
224
+ expect(controller).to respond_to(:sign_out_user)
225
+ end
226
+
227
+ context "when user is signed in" do
228
+ let(:sign_in_options){ Hash.new }
229
+
230
+ before do
231
+ controller.sign_in user, sign_in_options
232
+ expect(controller.current_user).to eq(user)
233
+ end
234
+
235
+ after do
236
+ expect(controller.current_user).to be_nil
237
+ expect(reloaded_controller.current_user).to be_nil
238
+ end
239
+
240
+ it "can sign out using sign_out_user method" do
241
+ controller.sign_out_user
242
+ end
243
+
244
+ it "can sign out using sign_out method" do
245
+ controller.sign_out user
246
+ end
247
+
248
+ context "with cookies" do
249
+ let(:sign_in_options){{ remember: true }}
250
+
251
+ after{ expect(cookies).to be_empty }
252
+
253
+ it "removes session token from cookies on sign out" do
254
+ controller.sign_out_user
255
+ end
256
+ end
257
+ end
258
+
259
+ context "with multiple sessions across devices" do
260
+ let(:first_device){ controller }
261
+ let(:second_device){ build_controller }
262
+
263
+ before do
264
+ first_device.sign_in user
265
+ second_device.sign_in user
266
+ end
267
+
268
+ it "signs out all currently active sessions on all devices" do
269
+ expect{ first_device.clear_all_user_sessions_for user }.to change(Authem::Session, :count).by(-2)
270
+ expect(second_device.reloaded.current_user).to be_nil
271
+ end
272
+ end
273
+
274
+ it "raises an error when calling sign_out with nil" do
275
+ expect{ controller.sign_out nil }.to raise_error(ArgumentError)
276
+ end
277
+
278
+ it "persists session in database" do
279
+ expect{ controller.sign_in user }.to change(Authem::Session, :count).by(1)
280
+ end
281
+
282
+ it "removes database session on sign out" do
283
+ controller.sign_in user
284
+ expect{ controller.sign_out user }.to change(Authem::Session, :count).by(-1)
285
+ end
286
+ end
287
+
288
+ context "with multiple roles" do
289
+ let(:admin){ MyNamespace::SuperUser.create(email: "admin@example.com") }
290
+ let(:controller_klass) do
291
+ Class.new(BaseController) do
292
+ authem_for :user
293
+ authem_for :admin, model: MyNamespace::SuperUser
294
+ end
295
+ end
296
+
297
+ it "has current_admin method" do
298
+ expect(controller).to respond_to(:current_admin)
299
+ end
300
+
301
+ it "has sign_in_admin method" do
302
+ expect(controller).to respond_to(:sign_in_admin)
303
+ end
304
+
305
+ it "can sign in admin using sign_in_admin method" do
306
+ controller.sign_in_admin admin
307
+ expect(controller.current_admin).to eq(admin)
308
+ expect(reloaded_controller.current_admin).to eq(admin)
309
+ end
310
+
311
+ it "can sign in using sign_in method" do
312
+ expect(controller).to receive(:sign_in_admin).with(admin, {})
313
+ controller.sign_in admin
314
+ end
315
+
316
+ context "with signed in user and admin" do
317
+ let(:user){ User.create(email: "joe@example.com") }
318
+
319
+ before do
320
+ controller.sign_in_user user
321
+ controller.sign_in_admin admin
322
+ end
323
+
324
+ after do
325
+ expect(controller.current_admin).to eq(admin)
326
+ expect(reloaded_controller.current_admin).to eq(admin)
327
+ end
328
+
329
+ it "can sign out user separately from admin using sign_out_user" do
330
+ controller.sign_out_user
331
+ end
332
+
333
+ it "can sign out user separately from admin using sign_out" do
334
+ controller.sign_out user
335
+ end
336
+ end
337
+ end
338
+
339
+ context "multiple roles with same model class" do
340
+ let(:user){ User.create(email: "joe@example.com") }
341
+ let(:customer){ User.create(email: "shmoe@example.com") }
342
+ let(:controller_klass) do
343
+ Class.new(BaseController) do
344
+ authem_for :user
345
+ authem_for :customer, model: User
346
+ end
347
+ end
348
+
349
+ it "can sign in user separately from customer" do
350
+ controller.sign_in_user user
351
+ expect(controller.current_user).to eq(user)
352
+ expect(controller.current_customer).to be_nil
353
+ expect(reloaded_controller.current_user).to eq(user)
354
+ expect(reloaded_controller.current_customer).to be_nil
355
+ end
356
+
357
+ it "can sign in customer and user separately" do
358
+ controller.sign_in_user user
359
+ controller.sign_in_customer customer
360
+ expect(controller.current_user).to eq(user)
361
+ expect(controller.current_customer).to eq(customer)
362
+ expect(reloaded_controller.current_user).to eq(user)
363
+ expect(reloaded_controller.current_customer).to eq(customer)
364
+ end
365
+
366
+ it "raises the error when sign in can't guess the model properly" do
367
+ message = "Ambigous match for #{user.inspect}: user, customer"
368
+ expect{ controller.sign_in user }.to raise_error(Authem::AmbigousRoleError, message)
369
+ end
370
+
371
+ it "allows to specify role with special :as option" do
372
+ expect(controller).to receive(:sign_in_customer).with(user, as: :customer)
373
+ controller.sign_in user, as: :customer
374
+ end
375
+
376
+ it "raises the error when sign out can't guess the model properly" do
377
+ message = "Ambigous match for #{user.inspect}: user, customer"
378
+ expect{ controller.sign_out user }.to raise_error(Authem::AmbigousRoleError, message)
379
+ end
380
+ end
381
+
382
+ context "redirect after authentication" do
383
+ let(:controller_klass){ Class.new(BaseController){ authem_for :user }}
384
+
385
+ context "with saved url" do
386
+ before{ session[:return_to_url] = :my_url }
387
+
388
+ it "redirects back to saved url if it's available" do
389
+ expect(controller).to receive(:redirect_to).with(:my_url, notice: "foo")
390
+ controller.redirect_back_or_to :root, notice: "foo"
391
+ end
392
+
393
+ it "removes values from session after successful redirect" do
394
+ expect(controller).to receive(:redirect_to).with(:my_url, {})
395
+ expect{ controller.redirect_back_or_to :root }.to change{ session[:return_to_url] }.from(:my_url).to(nil)
396
+ end
397
+ end
398
+
399
+ it "redirects to specified url if there is no saved value" do
400
+ expect(controller).to receive(:redirect_to).with(:root, notice: "foo")
401
+ controller.redirect_back_or_to :root, notice: "foo"
402
+ end
403
+ end
404
+
405
+ context "when defining authem" do
406
+ it "settings do not propagate to parent controller" do
407
+ parent_klass = Class.new(BaseController){ authem_for :user }
408
+ child_klass = Class.new(parent_klass){ authem_for :member }
409
+ expect(child_klass.authem_roles.size).to eq(2)
410
+ expect(parent_klass.authem_roles.size).to eq(1)
411
+ end
412
+ end
413
+ end