authem 1.5.0 → 2.0.0

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.
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