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.
- checksums.yaml +4 -4
- data/Rakefile +10 -0
- data/app/controllers/pillowfort/concerns/controller_activation.rb +27 -0
- data/app/controllers/pillowfort/concerns/controller_authentication.rb +2 -9
- data/app/models/pillowfort/concerns/model_activation.rb +84 -0
- data/app/models/pillowfort/concerns/model_authentication.rb +10 -25
- data/app/models/pillowfort/concerns/model_password_reset.rb +62 -0
- data/lib/pillowfort/controller_methods.rb +12 -0
- data/lib/pillowfort/model_finder.rb +7 -0
- data/lib/pillowfort/token_generator.rb +19 -0
- data/lib/pillowfort/version.rb +1 -1
- data/spec/{dummy/spec/controllers → controllers}/accounts_controller_spec.rb +19 -1
- data/spec/dummy/app/controllers/accounts_controller.rb +2 -0
- data/spec/dummy/app/models/account.rb +2 -0
- data/spec/dummy/config/database.yml +5 -1
- data/spec/dummy/config/environments/development.rb +42 -0
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/migrate/20150210215727_add_password_reset_tokens.rb +8 -0
- data/spec/dummy/db/migrate/20150211185152_add_activation_token_to_account.rb +9 -0
- data/spec/dummy/db/migrate/20150413161345_add_auth_token_ttl_to_account.rb +7 -0
- data/spec/dummy/db/schema.rb +9 -3
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/test.log +15233 -1641
- data/spec/dummy/spec/spec_helper.rb +1 -10
- data/spec/factories/accounts.rb +19 -0
- data/spec/models/account_spec.rb +531 -0
- data/spec/{dummy/spec/rails_helper.rb → rails_helper.rb} +1 -1
- data/spec/spec_helper.rb +25 -0
- data/spec/{dummy/spec/support → support}/helpers/authentication_helper.rb +0 -0
- metadata +62 -17
- data/spec/dummy/log/development.log +0 -0
- data/spec/dummy/spec/factories/accounts.rb +0 -10
- 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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 090ed10762f83742768099773d63068509626187
|
4
|
+
data.tar.gz: ff8ca919af0d107937dad72486dfcfb90b825892
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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,
|
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.
|
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,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
|
data/lib/pillowfort/version.rb
CHANGED
@@ -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 {
|
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
|
@@ -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
|