pillowfort 0.1.2 → 0.2.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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +10 -0
  3. data/app/controllers/pillowfort/concerns/controller_activation.rb +27 -0
  4. data/app/controllers/pillowfort/concerns/controller_authentication.rb +2 -9
  5. data/app/models/pillowfort/concerns/model_activation.rb +84 -0
  6. data/app/models/pillowfort/concerns/model_authentication.rb +10 -25
  7. data/app/models/pillowfort/concerns/model_password_reset.rb +62 -0
  8. data/lib/pillowfort/controller_methods.rb +12 -0
  9. data/lib/pillowfort/model_finder.rb +7 -0
  10. data/lib/pillowfort/token_generator.rb +19 -0
  11. data/lib/pillowfort/version.rb +1 -1
  12. data/spec/{dummy/spec/controllers → controllers}/accounts_controller_spec.rb +19 -1
  13. data/spec/dummy/app/controllers/accounts_controller.rb +2 -0
  14. data/spec/dummy/app/models/account.rb +2 -0
  15. data/spec/dummy/config/database.yml +5 -1
  16. data/spec/dummy/config/environments/development.rb +42 -0
  17. data/spec/dummy/db/development.sqlite3 +0 -0
  18. data/spec/dummy/db/migrate/20150210215727_add_password_reset_tokens.rb +8 -0
  19. data/spec/dummy/db/migrate/20150211185152_add_activation_token_to_account.rb +9 -0
  20. data/spec/dummy/db/migrate/20150413161345_add_auth_token_ttl_to_account.rb +7 -0
  21. data/spec/dummy/db/schema.rb +9 -3
  22. data/spec/dummy/db/test.sqlite3 +0 -0
  23. data/spec/dummy/log/test.log +15233 -1641
  24. data/spec/dummy/spec/spec_helper.rb +1 -10
  25. data/spec/factories/accounts.rb +19 -0
  26. data/spec/models/account_spec.rb +531 -0
  27. data/spec/{dummy/spec/rails_helper.rb → rails_helper.rb} +1 -1
  28. data/spec/spec_helper.rb +25 -0
  29. data/spec/{dummy/spec/support → support}/helpers/authentication_helper.rb +0 -0
  30. metadata +62 -17
  31. data/spec/dummy/log/development.log +0 -0
  32. data/spec/dummy/spec/factories/accounts.rb +0 -10
  33. data/spec/dummy/spec/models/account_spec.rb +0 -276
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 79ad110faff2311bfa921692616527606ab26a6b
4
- data.tar.gz: 664c0965f9784ec4b2bb234b0069693a7e300b6d
3
+ metadata.gz: 090ed10762f83742768099773d63068509626187
4
+ data.tar.gz: ff8ca919af0d107937dad72486dfcfb90b825892
5
5
  SHA512:
6
- metadata.gz: 986537184b9046344eaa78e13fd4dd4e1918ef720eac61eee0ee8fa9873d322d2e0402ee0fb38474c5f20ffa29c112948cf5f4621a1b1fbe806b068d9045ddcf
7
- data.tar.gz: 22077770faa03688705f150e055fd1e3f112a2a9a9bd29a4349eeb519fe88cfc867c0cce62a01fc26305706546e609bf7f1d7431c466ef0a36ce0ed8905899c9
6
+ metadata.gz: 5fe28a307620e700011e9a3c99768a9f20380e116ac5dd5ed2c77c5ece56824f9d3ca849273c2ea5701b1587fc0350f2707d49c01c846963cd6e4845151bd1bd
7
+ data.tar.gz: c203cfc9f830a6c63dc00a05a40d15074d20f61f3002bada09d4346ef6b315086a35d6f14086526c284a7cc6f24f4f354eec94aa925524f11e2197786d113021
data/Rakefile CHANGED
@@ -19,5 +19,15 @@ end
19
19
  load 'rails/tasks/statistics.rake'
20
20
 
21
21
 
22
+ APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
23
+ load 'rails/tasks/engine.rake'
22
24
 
23
25
  Bundler::GemHelper.install_tasks
26
+
27
+ require 'rspec/core'
28
+ require 'rspec/core/rake_task'
29
+
30
+ desc "Run all the specs in the dummy app"
31
+ RSpec::Core::RakeTask.new(:spec => 'app:db:test:prepare')
32
+
33
+ task :default => :spec
@@ -0,0 +1,27 @@
1
+ require 'pillowfort/model_context'
2
+ require 'pillowfort/controller_methods'
3
+
4
+ module Pillowfort
5
+ module Concerns::ControllerActivation
6
+ extend ActiveSupport::Concern
7
+ include Pillowfort::ControllerMethods
8
+
9
+ included do
10
+ before_filter :enforce_activation!
11
+ end
12
+
13
+ private
14
+
15
+ def enforce_activation!
16
+ context = Pillowfort::ModelContext
17
+ if resource = self.send(context.resource_reader_name)
18
+ unless resource.activated?
19
+ head :forbidden
20
+ end
21
+ else
22
+ return false
23
+ end
24
+ end
25
+
26
+ end
27
+ end
@@ -1,9 +1,11 @@
1
1
  require 'pillowfort/model_context'
2
+ require 'pillowfort/controller_methods'
2
3
 
3
4
  module Pillowfort
4
5
  module Concerns::ControllerAuthentication
5
6
  extend ActiveSupport::Concern
6
7
  include ActionController::HttpAuthentication::Basic::ControllerMethods
8
+ include Pillowfort::ControllerMethods
7
9
 
8
10
  included do
9
11
  before_filter :authenticate_from_account_token!
@@ -26,15 +28,6 @@ module Pillowfort
26
28
  allow_client_to_handle_unauthorized_status
27
29
  end
28
30
 
29
- def ensure_resource_reader(context)
30
- reader_name = context.resource_reader_name
31
- return if respond_to? reader_name
32
-
33
- self.class.send :define_method, reader_name do
34
- @authentication_resource
35
- end
36
- end
37
-
38
31
  # This is necessary, as it allows Cordova to properly delegate 401 response
39
32
  # handling to our application. If we keep this header, Cordova will defer
40
33
  # handling to iOS, and we'll never see the 401 status in the app... it'll just
@@ -0,0 +1,84 @@
1
+ require 'pillowfort/model_context'
2
+ require 'pillowfort/token_generator'
3
+ require 'pillowfort/model_finder'
4
+
5
+ module Pillowfort
6
+ module Concerns::ModelActivation
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ Pillowfort::ModelContext.model_class = self
11
+
12
+ # non-activated resource
13
+ validates :activation_token, presence: true, uniqueness: true, unless: :activated_at
14
+ validates :activation_token_expires_at, presence: true, unless: :activated_at
15
+ validates :activated_at, absence: true, if: :activation_token_expires_at
16
+
17
+ # Activated resource
18
+ validates :activation_token, presence: true, uniqueness: true, allow_nil: true, if: :activated_at
19
+ validates :activated_at, presence: true, unless: :activation_token_expires_at
20
+ validates :activation_token_expires_at, absence: true, if: :activated?
21
+
22
+ def create_activation_token(expiry: nil)
23
+ expiry ||= 1.hour.from_now
24
+ self.activation_token = generate_activation_token
25
+ self.activation_token_expires_at = expiry
26
+ end
27
+
28
+ def create_activation_token!(expiry: nil)
29
+ create_activation_token(expiry)
30
+ save validate: false
31
+ end
32
+
33
+ def activation_token_expired?
34
+ if activation_token_expires_at
35
+ activation_token_expires_at <= Time.now
36
+ else
37
+ true
38
+ end
39
+ end
40
+
41
+ def activated?
42
+ !!activated_at
43
+ end
44
+
45
+ def activate!
46
+ update_columns \
47
+ activated_at: Time.now,
48
+ activation_token_expires_at: nil
49
+ end
50
+
51
+ private
52
+
53
+ def generate_activation_token
54
+ resource_class = self.class
55
+ loop do
56
+ token = resource_class.friendly_token
57
+ break token unless resource_class.where(activation_token: token).first
58
+ end
59
+ end
60
+ end
61
+
62
+ module ClassMethods
63
+ include Pillowfort::TokenGenerator
64
+ include Pillowfort::ModelFinder
65
+
66
+ def find_and_activate(email, token)
67
+ return false if email.blank? || token.blank?
68
+
69
+ transaction do
70
+ if resource = find_by_email_case_insensitive(email)
71
+ if resource.activation_token_expired?
72
+ return false
73
+ else
74
+ if secure_compare(resource.activation_token, token)
75
+ resource.activate!
76
+ yield resource if block_given?
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -1,5 +1,7 @@
1
1
  require 'bcrypt'
2
2
  require 'pillowfort/model_context'
3
+ require 'pillowfort/token_generator'
4
+ require 'pillowfort/model_finder'
3
5
 
4
6
  module Pillowfort
5
7
  module Concerns::ModelAuthentication
@@ -15,7 +17,7 @@ module Pillowfort
15
17
  has_secure_password
16
18
 
17
19
  validates :email, presence: true, uniqueness: true
18
- validates :password, length: { minimum: MIN_PASSWORD_LENGTH }
20
+ validates :password, length: { minimum: MIN_PASSWORD_LENGTH }, allow_nil: true
19
21
 
20
22
  before_save :ensure_auth_token
21
23
 
@@ -33,14 +35,14 @@ module Pillowfort
33
35
  save validate: false
34
36
  end
35
37
 
36
- def token_expired?
38
+ def auth_token_expired?
37
39
  auth_token_expires_at <= Time.now
38
40
  end
39
41
 
40
42
  private
41
43
 
42
44
  def touch_token_expiry!
43
- update_column :auth_token_expires_at, 1.day.from_now
45
+ update_column :auth_token_expires_at, Time.now + auth_token_ttl
44
46
  end
45
47
 
46
48
  def generate_auth_token
@@ -53,6 +55,9 @@ module Pillowfort
53
55
  end
54
56
 
55
57
  module ClassMethods
58
+ include Pillowfort::TokenGenerator
59
+ include Pillowfort::ModelFinder
60
+
56
61
  def authenticate_securely(email, token)
57
62
  return false if email.blank? || token.blank?
58
63
 
@@ -63,7 +68,7 @@ module Pillowfort
63
68
 
64
69
  # if the resource token is expired, reset it and
65
70
  # return false, triggering a 401
66
- if resource.token_expired?
71
+ if resource.auth_token_expired?
67
72
  resource.reset_auth_token!
68
73
  return false
69
74
  else
@@ -81,7 +86,7 @@ module Pillowfort
81
86
 
82
87
  def find_and_authenticate(email, password)
83
88
  resource = find_by_email_case_insensitive(email)
84
-
89
+
85
90
  if resource && resource.authenticate(password)
86
91
  resource.tap do |u|
87
92
  u.reset_auth_token!
@@ -90,26 +95,6 @@ module Pillowfort
90
95
  return false
91
96
  end
92
97
  end
93
-
94
- def find_by_email_case_insensitive(email)
95
- find_by("lower(email) = ?", email.downcase)
96
- end
97
-
98
- # constant-time comparison algorithm to prevent timing attacks. Lifted
99
- # from Devise.
100
- def secure_compare(a, b)
101
- return false if a.blank? || b.blank? || a.bytesize != b.bytesize
102
- l = a.unpack "C#{a.bytesize}"
103
-
104
- res = 0
105
- b.each_byte { |byte| res |= byte ^ l.shift }
106
- res == 0
107
- end
108
-
109
- # Generates a value for our auth token. Lifted from Devise.
110
- def friendly_token
111
- SecureRandom.base64(32).tr('+/=lIO0', 'pqrsxyz')
112
- end
113
98
  end
114
99
  end
115
100
  end
@@ -0,0 +1,62 @@
1
+ require 'pillowfort/model_context'
2
+ require 'pillowfort/token_generator'
3
+ require 'pillowfort/model_finder'
4
+
5
+ module Pillowfort
6
+ module Concerns::ModelPasswordReset
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ def create_password_reset_token(expiry: nil)
11
+ expiry ||= 1.hour.from_now
12
+ self.password_reset_token = generate_password_reset_token
13
+ self.password_reset_token_expires_at = expiry
14
+ end
15
+
16
+ def password_token_expired?
17
+ if password_reset_token_expires_at
18
+ password_reset_token_expires_at <= Time.now
19
+ else
20
+ true
21
+ end
22
+ end
23
+
24
+ def clear_password_reset_token
25
+ update_columns \
26
+ password_reset_token: nil,
27
+ password_reset_token_expires_at: nil
28
+ end
29
+
30
+ private
31
+
32
+ def generate_password_reset_token
33
+ resource_class = self.class
34
+ loop do
35
+ token = resource_class.friendly_token
36
+ break token unless resource_class.where(password_reset_token: token).first
37
+ end
38
+ end
39
+ end
40
+
41
+ module ClassMethods
42
+ include Pillowfort::TokenGenerator
43
+ include Pillowfort::ModelFinder
44
+
45
+ def find_and_validate_password_reset_token(email, token)
46
+ return false if email.blank? || token.blank?
47
+
48
+ transaction do
49
+ if resource = find_by_email_case_insensitive(email)
50
+ if resource.password_token_expired?
51
+ return false
52
+ else
53
+ if secure_compare(resource.password_reset_token, token)
54
+ yield resource if block_given?
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,12 @@
1
+ module Pillowfort
2
+ module ControllerMethods
3
+ def ensure_resource_reader(context)
4
+ reader_name = context.resource_reader_name
5
+ return if respond_to? reader_name
6
+
7
+ self.class.send :define_method, reader_name do
8
+ @authentication_resource
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,7 @@
1
+ module Pillowfort
2
+ module ModelFinder
3
+ def find_by_email_case_insensitive(email)
4
+ find_by("lower(email) = ?", email.downcase)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,19 @@
1
+ module Pillowfort
2
+ module TokenGenerator
3
+ # constant-time comparison algorithm to prevent timing attacks. Lifted
4
+ # from Devise.
5
+ def secure_compare(a, b)
6
+ return false if a.blank? || b.blank? || a.bytesize != b.bytesize
7
+ l = a.unpack "C#{a.bytesize}"
8
+
9
+ res = 0
10
+ b.each_byte { |byte| res |= byte ^ l.shift }
11
+ res == 0
12
+ end
13
+
14
+ # Generates a value for our auth token. Lifted from Devise.
15
+ def friendly_token
16
+ SecureRandom.base64(32).tr('+/=lIO0', 'pqrsxyz')
17
+ end
18
+ end
19
+ end
@@ -1,3 +1,3 @@
1
1
  module Pillowfort
2
- VERSION = "0.1.2"
2
+ VERSION = "0.2.1"
3
3
  end
@@ -6,7 +6,10 @@ describe AccountsController, :type => :controller do
6
6
 
7
7
  context 'when authenticated' do
8
8
  let(:account) { FactoryGirl.create :account }
9
- before { authenticate_with account }
9
+ before {
10
+ account.activate!
11
+ authenticate_with account
12
+ }
10
13
 
11
14
  describe 'unprotected #index' do
12
15
  before { get :index }
@@ -30,6 +33,21 @@ describe AccountsController, :type => :controller do
30
33
  it { should have_http_status :unauthorized }
31
34
  end
32
35
  end
36
+
37
+ context 'when not activated' do
38
+ let(:account) { FactoryGirl.create :account }
39
+ before { authenticate_with account }
40
+
41
+ describe 'unprotected #index' do
42
+ before { get :index }
43
+ it { should have_http_status :success }
44
+ end
45
+
46
+ describe 'protected #show' do
47
+ before { get :show, id: 1 }
48
+ it { should have_http_status :forbidden }
49
+ end
50
+ end
33
51
  end
34
52
 
35
53
  describe 'its methods' do
@@ -1,7 +1,9 @@
1
1
  class AccountsController < ApplicationController
2
2
  include Pillowfort::Concerns::ControllerAuthentication
3
+ include Pillowfort::Concerns::ControllerActivation
3
4
 
4
5
  skip_filter :authenticate_from_account_token!, only: [:index]
6
+ skip_filter :enforce_activation!, only: [:index]
5
7
 
6
8
  def index
7
9
  head :ok
@@ -1,3 +1,5 @@
1
1
  class Account < ActiveRecord::Base
2
2
  include Pillowfort::Concerns::ModelAuthentication
3
+ include Pillowfort::Concerns::ModelPasswordReset
4
+ include Pillowfort::Concerns::ModelActivation
3
5
  end
@@ -8,10 +8,14 @@ default: &default
8
8
  adapter: sqlite3
9
9
  pool: 5
10
10
  timeout: 5000
11
-
11
+
12
12
  # Warning: The database defined as "test" will be erased and
13
13
  # re-generated from your development database when you run "rake".
14
14
  # Do not set this db to the same as development or production.
15
15
  test:
16
16
  <<: *default
17
17
  database: db/test.sqlite3
18
+
19
+ development:
20
+ <<: *default
21
+ database: db/development.sqlite3
@@ -0,0 +1,42 @@
1
+ Rails.application.configure do
2
+ # Settings specified here will take precedence over those in config/application.rb.
3
+
4
+ # The test environment is used exclusively to run your application's
5
+ # test suite. You never need to work with it otherwise. Remember that
6
+ # your test database is "scratch space" for the test suite and is wiped
7
+ # and recreated between test runs. Don't rely on the data there!
8
+ config.cache_classes = true
9
+
10
+ # Do not eager load code on boot. This avoids loading your whole application
11
+ # just for the purpose of running a single test. If you are using a tool that
12
+ # preloads Rails for running tests, you may have to set it to true.
13
+ config.eager_load = true
14
+
15
+ # Configure static file server for tests with Cache-Control for performance.
16
+ config.serve_static_files = true
17
+ config.static_cache_control = 'public, max-age=3600'
18
+
19
+ # Show full error reports and disable caching.
20
+ config.consider_all_requests_local = true
21
+ config.action_controller.perform_caching = false
22
+
23
+ # Raise exceptions instead of rendering exception templates.
24
+ config.action_dispatch.show_exceptions = false
25
+
26
+ # Disable request forgery protection in test environment.
27
+ config.action_controller.allow_forgery_protection = false
28
+
29
+ # Tell Action Mailer not to deliver emails to the real world.
30
+ # The :test delivery method accumulates sent emails in the
31
+ # ActionMailer::Base.deliveries array.
32
+ config.action_mailer.delivery_method = :test
33
+
34
+ # Randomize the order test cases are executed.
35
+ config.active_support.test_order = :random
36
+
37
+ # Print deprecation notices to the stderr.
38
+ config.active_support.deprecation = :stderr
39
+
40
+ # Raises error for missing translations
41
+ # config.action_view.raise_on_missing_translations = true
42
+ end