pillowfort 0.1.2 → 0.2.1

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