sorcery 0.11.0 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sorcery might be problematic. Click here for more details.

Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -0
  3. data/.rubocop_todo.yml +435 -0
  4. data/.travis.yml +15 -24
  5. data/CHANGELOG.md +19 -0
  6. data/Gemfile +1 -1
  7. data/README.md +25 -5
  8. data/lib/generators/sorcery/templates/initializer.rb +66 -3
  9. data/lib/generators/sorcery/templates/migration/magic_login.rb +9 -0
  10. data/lib/generators/sorcery/templates/migration/reset_password.rb +1 -0
  11. data/lib/sorcery.rb +2 -0
  12. data/lib/sorcery/adapters/active_record_adapter.rb +2 -1
  13. data/lib/sorcery/controller.rb +2 -0
  14. data/lib/sorcery/controller/submodules/external.rb +9 -0
  15. data/lib/sorcery/controller/submodules/session_timeout.rb +1 -1
  16. data/lib/sorcery/model/config.rb +4 -1
  17. data/lib/sorcery/model/submodules/external.rb +15 -0
  18. data/lib/sorcery/model/submodules/magic_login.rb +134 -0
  19. data/lib/sorcery/model/submodules/reset_password.rb +18 -0
  20. data/lib/sorcery/model/temporary_token.rb +3 -1
  21. data/lib/sorcery/providers/vk.rb +3 -2
  22. data/lib/sorcery/test_helpers/rails/request.rb +20 -0
  23. data/lib/sorcery/version.rb +1 -1
  24. data/sorcery.gemspec +6 -5
  25. data/spec/active_record/user_magic_login_spec.rb +15 -0
  26. data/spec/providers/vk_spec.rb +41 -0
  27. data/spec/rails_app/app/mailers/sorcery_mailer.rb +7 -0
  28. data/spec/rails_app/app/views/sorcery_mailer/magic_login_email.html.erb +13 -0
  29. data/spec/rails_app/app/views/sorcery_mailer/magic_login_email.text.erb +6 -0
  30. data/spec/rails_app/db/migrate/magic_login/20170924151831_add_magic_login_to_users.rb +17 -0
  31. data/spec/rails_app/db/migrate/reset_password/20101224223622_add_reset_password_to_users.rb +2 -0
  32. data/spec/shared_examples/user_magic_login_shared_examples.rb +150 -0
  33. data/spec/shared_examples/user_reset_password_shared_examples.rb +16 -0
  34. data/spec/sorcery_temporary_token_spec.rb +27 -0
  35. metadata +44 -13
@@ -16,6 +16,8 @@ module Sorcery
16
16
  attr_accessor :reset_password_token_attribute_name
17
17
  # Expires at attribute name.
18
18
  attr_accessor :reset_password_token_expires_at_attribute_name
19
+ # Counter access to reset password page
20
+ attr_accessor :reset_password_page_access_count_attribute_name
19
21
  # When was email sent, used for hammering protection.
20
22
  attr_accessor :reset_password_email_sent_at_attribute_name
21
23
  # Mailer class (needed)
@@ -34,6 +36,8 @@ module Sorcery
34
36
  base.sorcery_config.instance_eval do
35
37
  @defaults.merge!(:@reset_password_token_attribute_name => :reset_password_token,
36
38
  :@reset_password_token_expires_at_attribute_name => :reset_password_token_expires_at,
39
+ :@reset_password_page_access_count_attribute_name =>
40
+ :access_count_to_reset_password_page,
37
41
  :@reset_password_email_sent_at_attribute_name => :reset_password_email_sent_at,
38
42
  :@reset_password_mailer => nil,
39
43
  :@reset_password_mailer_disabled => false,
@@ -103,6 +107,20 @@ module Sorcery
103
107
  end
104
108
  mail
105
109
  end
110
+
111
+ # Increment access_count_to_reset_password_page attribute.
112
+ # For example, access_count_to_reset_password_page attribute is over 1, which
113
+ # means the user doesn't have a right to access.
114
+ def increment_password_reset_page_access_counter
115
+ sorcery_adapter.increment(self.sorcery_config.reset_password_page_access_count_attribute_name)
116
+ end
117
+
118
+ # Reset access_count_to_reset_password_page attribute into 0.
119
+ # This is expected to be used after sending an instruction email.
120
+ def reset_password_reset_page_access_counter
121
+ send(:"#{sorcery_config.reset_password_page_access_count_attribute_name}=", 0)
122
+ sorcery_adapter.save
123
+ end
106
124
 
107
125
  # Clears token and tries to update the new password for the user.
108
126
  def change_password!(new_password)
@@ -7,12 +7,14 @@ module Sorcery
7
7
  # such as reseting password and activating the user by email.
8
8
  module TemporaryToken
9
9
  def self.included(base)
10
+ # FIXME: This may not be the ideal way of passing sorcery_config to generate_random_token.
11
+ @sorcery_config = base.sorcery_config
10
12
  base.extend(ClassMethods)
11
13
  end
12
14
 
13
15
  # Random code, used for salt and temporary tokens.
14
16
  def self.generate_random_token
15
- SecureRandom.urlsafe_base64(15).tr('lIO0', 'sxyz')
17
+ SecureRandom.urlsafe_base64(@sorcery_config.token_randomness).tr('lIO0', 'sxyz')
16
18
  end
17
19
 
18
20
  module ClassMethods
@@ -9,7 +9,7 @@ module Sorcery
9
9
  class Vk < Base
10
10
  include Protocols::Oauth2
11
11
 
12
- attr_accessor :auth_path, :token_path, :user_info_url, :scope
12
+ attr_accessor :auth_path, :token_path, :user_info_url, :scope, :api_version
13
13
 
14
14
  def initialize
15
15
  super
@@ -28,7 +28,8 @@ module Sorcery
28
28
  access_token: access_token.token,
29
29
  uids: access_token.params['user_id'],
30
30
  fields: user_info_mapping.values.join(','),
31
- scope: scope
31
+ scope: scope,
32
+ v: api_version.to_s
32
33
  }
33
34
 
34
35
  response = access_token.get(user_info_url, params: params)
@@ -0,0 +1,20 @@
1
+ module Sorcery
2
+ module TestHelpers
3
+ module Rails
4
+ module Request
5
+ # Accepts arguments for user to login, the password, route to use and HTTP method
6
+ # Defaults - @user, 'secret', 'user_sessions_url' and http_method: POST
7
+ def login_user(user = nil, password = 'secret', route = nil, http_method = :post)
8
+ user ||= @user
9
+ route ||= user_sessions_url
10
+
11
+ username_attr = user.sorcery_config.username_attribute_names.first
12
+ username = user.send(username_attr)
13
+ password_attr = user.sorcery_config.password_attribute_name
14
+
15
+ send(http_method, route, params: { "#{username_attr}": username, "#{password_attr}": password })
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,3 +1,3 @@
1
1
  module Sorcery
2
- VERSION = '0.11.0'
2
+ VERSION = '0.12.0'
3
3
  end
@@ -18,16 +18,17 @@ Gem::Specification.new do |s|
18
18
 
19
19
  s.licenses = ['MIT']
20
20
 
21
- s.required_ruby_version = '>= 2.0.0'
21
+ s.required_ruby_version = '>= 2.2.2'
22
22
 
23
23
  s.add_dependency 'oauth', '~> 0.4', '>= 0.4.4'
24
24
  s.add_dependency 'oauth2', '~> 1.0', '>= 0.8.0'
25
25
  s.add_dependency 'bcrypt', '~> 3.1'
26
26
 
27
- s.add_development_dependency 'yard', '~> 0.6.0'
27
+ s.add_development_dependency 'yard', '~> 0.9.0', '>= 0.9.12'
28
28
  s.add_development_dependency 'timecop'
29
29
  s.add_development_dependency 'simplecov', '>= 0.3.8'
30
- s.add_development_dependency 'rspec-rails', '~> 3.5.0'
31
- s.add_development_dependency 'test-unit', '~> 3.1.0'
32
- s.add_development_dependency 'byebug', '~> 9.0.0'
30
+ s.add_development_dependency 'rspec-rails', '~> 3.7.0'
31
+ s.add_development_dependency 'test-unit', '~> 3.2.0'
32
+ s.add_development_dependency 'byebug', '~> 10.0.0'
33
+ s.add_development_dependency 'webmock', '~> 3.3.0'
33
34
  end
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+ require 'shared_examples/user_magic_login_shared_examples'
3
+
4
+ describe User, 'with magic_login submodule', active_record: true do
5
+ before(:all) do
6
+ ActiveRecord::Migrator.migrate("#{Rails.root}/db/migrate/magic_login")
7
+ User.reset_column_information
8
+ end
9
+
10
+ after(:all) do
11
+ ActiveRecord::Migrator.rollback("#{Rails.root}/db/migrate/magic_login")
12
+ end
13
+
14
+ it_behaves_like 'magic_login_model'
15
+ end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+ require 'sorcery/providers/base'
3
+ require 'sorcery/providers/vk'
4
+ require 'webmock/rspec'
5
+
6
+ describe Sorcery::Providers::Vk do
7
+ include WebMock::API
8
+
9
+ let(:provider) { Sorcery::Controller::Config.vk }
10
+
11
+ before(:all) do
12
+ sorcery_reload!([:external])
13
+ sorcery_controller_property_set(:external_providers, [:vk])
14
+ sorcery_controller_external_property_set(:vk, :key, "KEY")
15
+ sorcery_controller_external_property_set(:vk, :secret, "SECRET")
16
+ end
17
+
18
+ def stub_vk_authorize
19
+ stub_request(:post, /https\:\/\/oauth\.vk\.com\/access_token/)
20
+ .to_return(
21
+ status: 200,
22
+ body: '{"access_token":"TOKEN","expires_in":86329,"user_id":1}',
23
+ headers: {'content-type' => 'application/json'})
24
+ end
25
+
26
+ context "getting user info hash" do
27
+ it "should provide VK API version" do
28
+ stub_vk_authorize
29
+ sorcery_controller_external_property_set(:vk, :api_version, '5.71')
30
+
31
+ get_user = stub_request(:get, "https://api.vk.com/method/getProfiles?access_token=TOKEN&fields=&scope=email&uids=1&v=5.71")
32
+ .to_return(body: '{"response":[{"id":1}]}')
33
+
34
+ token = provider.process_callback({ code: 'CODE' }, nil)
35
+ provider.get_user_hash(token)
36
+
37
+ expect(get_user).to have_been_requested
38
+ end
39
+ end
40
+
41
+ end
@@ -28,4 +28,11 @@ class SorceryMailer < ActionMailer::Base
28
28
  mail(to: user.email,
29
29
  subject: 'Your account has been locked due to many wrong logins')
30
30
  end
31
+
32
+ def magic_login_email(user)
33
+ @user = user
34
+ @url = 'http://example.com/login'
35
+ mail(to: user.email,
36
+ subject: 'Magic Login')
37
+ end
31
38
  end
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
5
+ </head>
6
+ <body>
7
+ <h1>Hello, <%= @user.username %></h1>
8
+ <p>
9
+ To login without a password, just follow this link: <%= @url %>.
10
+ </p>
11
+ <p>Have a great day!</p>
12
+ </body>
13
+ </html>
@@ -0,0 +1,6 @@
1
+ Hello, <%= @user.username %>
2
+ ===============================================
3
+
4
+ To login without a password, just follow this link: <%= @url %>.
5
+
6
+ Have a great day!
@@ -0,0 +1,17 @@
1
+ class AddMagicLoginToUsers < ActiveRecord::CompatibleLegacyMigration.migration_class
2
+ def self.up
3
+ add_column :users, :magic_login_token, :string, default: nil
4
+ add_column :users, :magic_login_token_expires_at, :datetime, default: nil
5
+ add_column :users, :magic_login_email_sent_at, :datetime, default: nil
6
+
7
+ add_index :users, :magic_login_token
8
+ end
9
+
10
+ def self.down
11
+ remove_index :users, :magic_login_token
12
+
13
+ remove_column :users, :magic_login_token
14
+ remove_column :users, :magic_login_token_expires_at
15
+ remove_column :users, :magic_login_email_sent_at
16
+ end
17
+ end
@@ -3,11 +3,13 @@ class AddResetPasswordToUsers < ActiveRecord::CompatibleLegacyMigration.migratio
3
3
  add_column :users, :reset_password_token, :string, default: nil
4
4
  add_column :users, :reset_password_token_expires_at, :datetime, default: nil
5
5
  add_column :users, :reset_password_email_sent_at, :datetime, default: nil
6
+ add_column :users, :access_count_to_reset_password_page, :integer, default: 0
6
7
  end
7
8
 
8
9
  def self.down
9
10
  remove_column :users, :reset_password_email_sent_at
10
11
  remove_column :users, :reset_password_token_expires_at
11
12
  remove_column :users, :reset_password_token
13
+ remove_column :users, :access_count_to_reset_password_page
12
14
  end
13
15
  end
@@ -0,0 +1,150 @@
1
+ shared_examples_for "magic_login_model" do
2
+ let(:user) {create_new_user}
3
+ before(:each) do
4
+ User.sorcery_adapter.delete_all
5
+ end
6
+
7
+ context 'loaded plugin configuration' do
8
+ let(:config) {User.sorcery_config}
9
+
10
+ before(:all) do
11
+ sorcery_reload!([:magic_login])
12
+ end
13
+
14
+ after(:each) do
15
+ User.sorcery_config.reset!
16
+ end
17
+
18
+ describe "enables configuration options" do
19
+ it do
20
+ sorcery_model_property_set(:magic_login_token_attribute_name, :test_magic_login_token)
21
+ expect(config.magic_login_token_attribute_name).to eq :test_magic_login_token
22
+ end
23
+
24
+ it do
25
+ sorcery_model_property_set(:magic_login_token_expires_at_attribute_name, :test_magic_login_token_expires_at)
26
+ expect(config.magic_login_token_expires_at_attribute_name).to eq :test_magic_login_token_expires_at
27
+ end
28
+
29
+ it do
30
+ sorcery_model_property_set(:magic_login_email_sent_at_attribute_name, :test_magic_login_email_sent_at)
31
+ expect(config.magic_login_email_sent_at_attribute_name).to eq :test_magic_login_email_sent_at
32
+ end
33
+
34
+ it do
35
+ TestMailerClass = Class.new # need a mailer class to test
36
+ sorcery_model_property_set(:magic_login_mailer_class, TestMailerClass)
37
+ expect(config.magic_login_mailer_class).to eq TestMailerClass
38
+ end
39
+
40
+ it do
41
+ sorcery_model_property_set(:magic_login_mailer_disabled, false)
42
+ expect(config.magic_login_mailer_disabled).to eq false
43
+ end
44
+
45
+ it do
46
+ sorcery_model_property_set(:magic_login_email_method_name, :test_magic_login_email)
47
+ expect(config.magic_login_email_method_name).to eq :test_magic_login_email
48
+ end
49
+
50
+ it do
51
+ sorcery_model_property_set(:magic_login_expiration_period, 100000000)
52
+ expect(config.magic_login_expiration_period).to eq 100000000
53
+ end
54
+
55
+ it do
56
+ sorcery_model_property_set(:magic_login_time_between_emails, 100000000)
57
+ expect(config.magic_login_time_between_emails).to eq 100000000
58
+ end
59
+ end
60
+
61
+ describe "#generate_magic_login_token!" do
62
+ context "magic_login_token is nil" do
63
+ it "magic_login_token_expires_at and magic_login_email_sent_at aren't nil " do
64
+ user.generate_magic_login_token!
65
+ expect(user.magic_login_token_expires_at).not_to be_nil
66
+ expect(user.magic_login_email_sent_at).not_to be_nil
67
+ end
68
+
69
+ it "magic_login_token is different from the one before" do
70
+ token_before = user.magic_login_token
71
+ user.generate_magic_login_token!
72
+ expect(user.magic_login_token).not_to eq token_before
73
+ end
74
+ end
75
+
76
+ context "magic_login_token is not nil" do
77
+ it "changes `user.magic_login_token`" do
78
+ token_before = user.magic_login_token
79
+ user.generate_magic_login_token!
80
+ expect(user.magic_login_token).not_to eq token_before
81
+ end
82
+ end
83
+ end
84
+
85
+ describe "#deliver_magic_login_instructions!" do
86
+ context "success" do
87
+ before do
88
+ sorcery_model_property_set(:magic_login_time_between_emails, 30*60)
89
+ sorcery_model_property_set(:magic_login_mailer_disabled, false)
90
+ Timecop.travel(10.days.ago) do
91
+ user.send(:"#{config.magic_login_email_sent_at_attribute_name}=", DateTime.now)
92
+ end
93
+ sorcery_model_property_set(:magic_login_mailer_class, ::SorceryMailer)
94
+ end
95
+
96
+ it do
97
+ user.deliver_magic_login_instructions!
98
+ expect(ActionMailer::Base.deliveries.size).to eq 1
99
+ end
100
+
101
+ it do
102
+ expect(user.deliver_magic_login_instructions!).to eq true
103
+ end
104
+ end
105
+
106
+ context "failure" do
107
+ context "magic_login_time_between_emails is nil" do
108
+ it "returns false" do
109
+ sorcery_model_property_set(:magic_login_time_between_emails, nil)
110
+ expect(user.deliver_magic_login_instructions!).to eq false
111
+ end
112
+ end
113
+
114
+ context "magic_login_email_sent_at is nil" do
115
+ it "returns false" do
116
+ user.send(:"#{config.magic_login_email_sent_at_attribute_name}=", nil)
117
+ expect(user.deliver_magic_login_instructions!).to eq false
118
+ end
119
+ end
120
+
121
+ context "now is before magic_login_email_sent_at plus the interval" do
122
+ it "returns false" do
123
+ user.send(:"#{config.magic_login_email_sent_at_attribute_name}=", DateTime.now)
124
+ sorcery_model_property_set(:magic_login_time_between_emails, 30*60)
125
+ expect(user.deliver_magic_login_instructions!).to eq false
126
+ end
127
+ end
128
+
129
+ context "magic_login_mailer_disabled is true" do
130
+ it "returns false" do
131
+ sorcery_model_property_set(:magic_login_mailer_disabled, true)
132
+ expect(user.deliver_magic_login_instructions!).to eq false
133
+ end
134
+ end
135
+ end
136
+ end
137
+
138
+ describe "#clear_magic_login_token!" do
139
+ it "makes magic_login_token_attribute_name and magic_login_token_expires_at_attribute_name nil" do
140
+ user.magic_login_token = "test_token"
141
+ user.magic_login_token_expires_at = Time.now
142
+
143
+ user.clear_magic_login_token!
144
+
145
+ expect(user.magic_login_token).to eq nil
146
+ expect(user.magic_login_token_expires_at).to eq nil
147
+ end
148
+ end
149
+ end
150
+ end
@@ -214,6 +214,22 @@ shared_examples_for 'rails_3_reset_password_model' do
214
214
  expect(user.reset_password_token).not_to eq old_password_code
215
215
  end
216
216
 
217
+ describe '#increment_password_reset_page_access_counter' do
218
+ it 'increments reset_password_page_access_count_attribute_name' do
219
+ expected_count = user.access_count_to_reset_password_page + 1
220
+ user.increment_password_reset_page_access_counter
221
+ expect(user.access_count_to_reset_password_page).to eq expected_count
222
+ end
223
+ end
224
+
225
+ describe '#reset_password_reset_page_access_counter' do
226
+ it 'reset reset_password_page_access_count_attribute_name into 0' do
227
+ user.update(access_count_to_reset_password_page: 10)
228
+ user.reset_password_reset_page_access_counter
229
+ expect(user.access_count_to_reset_password_page).to eq 0
230
+ end
231
+ end
232
+
217
233
  context 'mailer is enabled' do
218
234
  it 'sends an email on reset' do
219
235
  old_size = ActionMailer::Base.deliveries.size
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sorcery::Model::TemporaryToken do
4
+ describe '.generate_random_token' do
5
+ before do
6
+ sorcery_reload!
7
+ end
8
+
9
+ subject { Sorcery::Model::TemporaryToken.generate_random_token.length }
10
+
11
+ context 'token_randomness is 3' do
12
+ before do
13
+ sorcery_model_property_set(:token_randomness, 3)
14
+ end
15
+
16
+ it { is_expected.to eq 4 }
17
+ end
18
+
19
+ context 'token_randomness is 15' do
20
+ before do
21
+ sorcery_model_property_set(:token_randomness, 15)
22
+ end
23
+
24
+ it { is_expected.to eq 20 }
25
+ end
26
+ end
27
+ end