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