authenticate 0.1.0

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 (102) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +21 -0
  6. data/Gemfile.lock +154 -0
  7. data/LICENSE +20 -0
  8. data/README.md +240 -0
  9. data/Rakefile +6 -0
  10. data/app/assets/config/authenticate_manifest.js +0 -0
  11. data/app/assets/images/authenticate/.keep +0 -0
  12. data/app/assets/javascripts/authenticate/.keep +0 -0
  13. data/app/assets/stylesheets/authenticate/.keep +0 -0
  14. data/app/controllers/.keep +0 -0
  15. data/app/helpers/.keep +0 -0
  16. data/app/mailers/.keep +0 -0
  17. data/app/models/.keep +0 -0
  18. data/app/views/.keep +0 -0
  19. data/authenticate.gemspec +38 -0
  20. data/bin/rails +12 -0
  21. data/config/routes.rb +2 -0
  22. data/lib/authenticate.rb +12 -0
  23. data/lib/authenticate/callbacks/authenticatable.rb +4 -0
  24. data/lib/authenticate/callbacks/brute_force.rb +31 -0
  25. data/lib/authenticate/callbacks/lifetimed.rb +5 -0
  26. data/lib/authenticate/callbacks/timeoutable.rb +15 -0
  27. data/lib/authenticate/callbacks/trackable.rb +8 -0
  28. data/lib/authenticate/configuration.rb +144 -0
  29. data/lib/authenticate/controller.rb +110 -0
  30. data/lib/authenticate/crypto/bcrypt.rb +30 -0
  31. data/lib/authenticate/debug.rb +10 -0
  32. data/lib/authenticate/engine.rb +21 -0
  33. data/lib/authenticate/lifecycle.rb +120 -0
  34. data/lib/authenticate/login_status.rb +27 -0
  35. data/lib/authenticate/model/brute_force.rb +51 -0
  36. data/lib/authenticate/model/db_password.rb +71 -0
  37. data/lib/authenticate/model/email.rb +76 -0
  38. data/lib/authenticate/model/lifetimed.rb +48 -0
  39. data/lib/authenticate/model/timeoutable.rb +47 -0
  40. data/lib/authenticate/model/trackable.rb +43 -0
  41. data/lib/authenticate/model/username.rb +45 -0
  42. data/lib/authenticate/modules.rb +61 -0
  43. data/lib/authenticate/session.rb +123 -0
  44. data/lib/authenticate/token.rb +7 -0
  45. data/lib/authenticate/user.rb +50 -0
  46. data/lib/authenticate/version.rb +3 -0
  47. data/lib/tasks/authenticate_tasks.rake +4 -0
  48. data/spec/configuration_spec.rb +60 -0
  49. data/spec/dummy/README.rdoc +28 -0
  50. data/spec/dummy/Rakefile +6 -0
  51. data/spec/dummy/app/assets/images/.keep +0 -0
  52. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  53. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  54. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  55. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  56. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  57. data/spec/dummy/app/mailers/.keep +0 -0
  58. data/spec/dummy/app/models/.keep +0 -0
  59. data/spec/dummy/app/models/concerns/.keep +0 -0
  60. data/spec/dummy/app/models/user.rb +3 -0
  61. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  62. data/spec/dummy/bin/bundle +3 -0
  63. data/spec/dummy/bin/rails +4 -0
  64. data/spec/dummy/bin/rake +4 -0
  65. data/spec/dummy/bin/setup +29 -0
  66. data/spec/dummy/config.ru +4 -0
  67. data/spec/dummy/config/application.rb +26 -0
  68. data/spec/dummy/config/boot.rb +5 -0
  69. data/spec/dummy/config/database.yml +25 -0
  70. data/spec/dummy/config/environment.rb +5 -0
  71. data/spec/dummy/config/environments/development.rb +41 -0
  72. data/spec/dummy/config/environments/production.rb +79 -0
  73. data/spec/dummy/config/environments/test.rb +42 -0
  74. data/spec/dummy/config/initializers/assets.rb +11 -0
  75. data/spec/dummy/config/initializers/authenticate.rb +7 -0
  76. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  77. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  78. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  79. data/spec/dummy/config/initializers/inflections.rb +16 -0
  80. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  81. data/spec/dummy/config/initializers/session_store.rb +3 -0
  82. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  83. data/spec/dummy/config/locales/en.yml +23 -0
  84. data/spec/dummy/config/routes.rb +56 -0
  85. data/spec/dummy/config/secrets.yml +22 -0
  86. data/spec/dummy/db/development.sqlite3 +0 -0
  87. data/spec/dummy/db/migrate/20160120003910_create_users.rb +18 -0
  88. data/spec/dummy/db/schema.rb +31 -0
  89. data/spec/dummy/db/test.sqlite3 +0 -0
  90. data/spec/dummy/lib/assets/.keep +0 -0
  91. data/spec/dummy/log/.keep +0 -0
  92. data/spec/dummy/public/404.html +67 -0
  93. data/spec/dummy/public/422.html +67 -0
  94. data/spec/dummy/public/500.html +66 -0
  95. data/spec/dummy/public/favicon.ico +0 -0
  96. data/spec/factories/users.rb +23 -0
  97. data/spec/model/session_spec.rb +86 -0
  98. data/spec/model/token_spec.rb +11 -0
  99. data/spec/model/user_spec.rb +12 -0
  100. data/spec/orm/active_record.rb +17 -0
  101. data/spec/spec_helper.rb +148 -0
  102. metadata +255 -0
@@ -0,0 +1,10 @@
1
+ module Authenticate
2
+ module Debug
3
+ extend ActiveSupport::Concern
4
+
5
+ def d(msg)
6
+ Rails.logger.info msg.to_s if Authenticate.configuration.debug
7
+ end
8
+
9
+ end
10
+ end
@@ -0,0 +1,21 @@
1
+ module Authenticate
2
+ class Engine < ::Rails::Engine
3
+ # config.generators do |g|
4
+ # g.test_framework :rspec
5
+ # g.fixture_replacement :factory_girl, dir: 'spec/factories'
6
+ # end
7
+
8
+ # viget
9
+ config.generators do |g|
10
+ g.test_framework :rspec
11
+ g.fixture_replacement :factory_girl, dir: 'spec/factories'
12
+
13
+
14
+ # g.test_framework :rspec, fixture: false
15
+ # g.fixture_replacement :factory_girl, dir: 'spec/factories'
16
+ # g.assets false
17
+ # g.helper false
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,120 @@
1
+ module Authenticate
2
+
3
+ # Lifecycle stores and runs callbacks for authorization events.
4
+ #
5
+ # Heavily borrowed from warden (https://github.com/hassox/warden).
6
+ #
7
+ # = Events:
8
+ # :set_user - called after the user object is loaded, either through id/password or via session token.
9
+ # :authentication - called after the user authenticates with id & password
10
+ #
11
+ # Callbacks are added via after_set_user or after_authentication.
12
+ #
13
+ # Callbacks can throw(:failure,message) to signal an authentication/authorization failure, or perform
14
+ # actions on the user or session.
15
+ #
16
+ # = Options
17
+ #
18
+ # The callback options may optionally specify when to run the callback:
19
+ # only - executes the callback only if it matches the event(s) given
20
+ # except - executes the callback except if it matches the event(s) given
21
+ #
22
+ # The callback may also specify a 'name' key in options. This is for debugging purposes only.
23
+ #
24
+ # = Callback block parameters
25
+ #
26
+ # Callbacks are invoked with the following block parameters: |user, session, opts|
27
+ # user - the user object just loaded
28
+ # session - the Authenticate::Session
29
+ # opts - any options you want passed into the callback
30
+ #
31
+ # = Example
32
+ #
33
+ # # A callback to track the users successful logins:
34
+ # Authenticate.lifecycle.after_set_user do |user, session, opts|
35
+ # user.sign_in_count += 1
36
+ # end
37
+ #
38
+ class Lifecycle
39
+ include Debug
40
+ @@conditions = [:only, :except, :event]
41
+
42
+ # This callback is triggered after the first time a user is set during per-hit authorization, or during login.
43
+ def after_set_user(options = {}, method = :push, &block)
44
+ raise BlockNotGiven unless block_given?
45
+ options = process_opts(options)
46
+ # puts "register after_set_user #{options.inspect}"
47
+ after_set_user_callbacks.send(method, [block, options])
48
+ end
49
+
50
+
51
+
52
+ # A callback to run after the user successfully authenticates, during the login process.
53
+ # Mechanically identical to [#after_set_user].
54
+ def after_authentication(options = {}, method = :push, &block)
55
+ raise BlockNotGiven unless block_given?
56
+ options = process_opts(options)
57
+ # puts "register after_authentication #{options}"
58
+ after_authentication_callbacks.send(method, [block, options])
59
+ end
60
+
61
+
62
+ def run_callbacks(kind, *args) # args - |user, session, opts|
63
+ # Last callback arg MUST be a Hash
64
+ options = args.last
65
+ d "@@@@@@@@@@@@ run_callbacks kind:#{kind} options:#{options.inspect}"
66
+
67
+ # each callback has 'conditions' stored with it
68
+ send("#{kind}_callbacks").each do |callback, conditions|
69
+ conditions = conditions.dup # make a copy, we mutate it
70
+ d "running callback -- #{conditions.inspect}"
71
+ conditions.delete_if {|key, val| !@@conditions.include? key}
72
+ # d "conditions after filter:#{conditions.inspect}"
73
+ invalid = conditions.find do |key, value|
74
+ # d "!!!!!!! conditions key:#{key} value:#{value} options[key]:#{options[key].inspect}"
75
+ # d("!value.include?(options[key]):#{!value.include?(options[key])}") if value.is_a?(Array)
76
+ value.is_a?(Array) ? !value.include?(options[key]) : (value != options[key])
77
+ end
78
+ d "callback invalid? #{invalid.inspect}"
79
+ callback.call(*args) unless invalid
80
+ end
81
+ d "FINISHED run_callbacks #{kind}"
82
+ nil
83
+ end
84
+
85
+
86
+ def prepend_after_authentication(options = {}, &block)
87
+ after_authentication(options, :unshift, &block)
88
+ end
89
+
90
+ private
91
+
92
+ # set event: to run callback on based on options
93
+ def process_opts(options)
94
+ if options.key?(:only)
95
+ options[:event] = options.delete(:only)
96
+ elsif options.key?(:except)
97
+ options[:event] = [:set_user, :authentication] - Array(options.delete(:except))
98
+ end
99
+ options
100
+ end
101
+
102
+
103
+ def after_set_user_callbacks
104
+ @after_set_user_callbacks ||= []
105
+ end
106
+
107
+ def after_authentication_callbacks
108
+ @after_authentication_callbacks ||= []
109
+ end
110
+ end
111
+
112
+
113
+ def self.lifecycle
114
+ @lifecycle ||= Lifecycle.new
115
+ end
116
+
117
+ def self.lifecycle=(lifecycle)
118
+ @lifecycle = lifecycle
119
+ end
120
+ end
@@ -0,0 +1,27 @@
1
+ module Authenticate
2
+
3
+ # Indicate login attempt was successful. Allows caller to supply a block to login() predicated on success?
4
+ class Success
5
+ def success?
6
+ true
7
+ end
8
+ end
9
+
10
+ # Indicate login attempt was a failure, with a message.
11
+ # Allows caller to supply a block to login() predicated on success?
12
+ class Failure
13
+ # The reason the sign in failed.
14
+ attr_reader :message
15
+
16
+ # @param [String] message The reason the login failed.
17
+ def initialize(message)
18
+ @message = message
19
+ end
20
+
21
+ def success?
22
+ false
23
+ end
24
+ end
25
+
26
+ end
27
+
@@ -0,0 +1,51 @@
1
+ require 'authenticate/callbacks/brute_force'
2
+
3
+ module Authenticate
4
+ module Model
5
+
6
+
7
+ # Protect from brute force attacks.
8
+ # Lock accounts that have too many failed consecutive logins.
9
+ # Todo: email user to allow faster unlocking via token.
10
+ module BruteForce
11
+ extend ActiveSupport::Concern
12
+
13
+ def self.required_fields(klass)
14
+ [:failed_logins_count, :lock_expires_at]
15
+ end
16
+
17
+
18
+ def register_failed_login!
19
+ self.failed_logins_count ||= 0
20
+ self.failed_logins_count += 1
21
+ lock! if self.failed_logins_count >= max_bad_logins
22
+ end
23
+
24
+ def lock!
25
+ self.update_attribute(:lock_expires_at, Time.now.utc + lockout_period)
26
+ end
27
+
28
+ def unlock!
29
+ self.update_attributes({failed_logins_count: 0, lock_expires_at: nil})
30
+ end
31
+
32
+ def locked?
33
+ !unlocked?
34
+ end
35
+
36
+ def unlocked?
37
+ self.lock_expires_at.nil?
38
+ end
39
+
40
+ private
41
+
42
+ def max_bad_logins
43
+ Authenticate.configuration.max_consecutive_bad_logins_allowed
44
+ end
45
+
46
+ def lockout_period
47
+ Authenticate.configuration.bad_login_lockout_period
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,71 @@
1
+ require 'authenticate/crypto/bcrypt'
2
+
3
+
4
+ module Authenticate
5
+ module Model
6
+
7
+ # Encrypts and stores a password in the database to validate the authenticity of a user while signing in.
8
+ #
9
+ # Authenticate can plug in any crypto provider, but currently only features BCrypt.
10
+ #
11
+ # = Methods
12
+ #
13
+ # The following methods are added to your user model:
14
+ # - password_match?(password) - checks to see if the user's password matches the given password
15
+ # - password=(new_password) - encrypt and set the user password
16
+ #
17
+ # = Validations
18
+ #
19
+ # - :password validation, requiring the password is set
20
+ #
21
+ module DbPassword
22
+ extend ActiveSupport::Concern
23
+
24
+ def self.required_fields(klass)
25
+ [:encrypted_password]
26
+ end
27
+
28
+ included do
29
+ include crypto_provider
30
+ attr_reader :password
31
+ attr_accessor :password_changing
32
+ validates :password, presence: true, unless: :skip_password_validation?
33
+ end
34
+
35
+
36
+
37
+ module ClassMethods
38
+
39
+ # We only have one crypto provider at the moment, but this is a pluggable point
40
+ # to install different crypto.
41
+ def crypto_provider
42
+ Authenticate.configuration.crypto_provider || Authenticate::Crypto::BCrypt
43
+ end
44
+
45
+ end
46
+
47
+
48
+
49
+ def password_match?(password)
50
+ match?(password, self.encrypted_password)
51
+ end
52
+
53
+ def password=(new_password)
54
+ @password = new_password
55
+
56
+ if new_password.present?
57
+ self.encrypted_password = encrypt(new_password)
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ # If we already have an encrypted password and it's not changing, skip the validation.
64
+ def skip_password_validation?
65
+ encrypted_password.present? && !password_changing
66
+ end
67
+
68
+ end
69
+ end
70
+ end
71
+
@@ -0,0 +1,76 @@
1
+ require 'email_validator'
2
+
3
+ module Authenticate
4
+ module Model
5
+
6
+ # Use :email as the identifier for the user. Must be unique to the system.
7
+ #
8
+ # = Columns
9
+ # - :email containing the email address of the user
10
+ #
11
+ # = Validations
12
+ # - :email requires email is set, validations the format, and ensure it is unique
13
+ #
14
+ # = Callbacks
15
+ # - :normalize_email - normalize the email, removing spaces etc, before saving
16
+ #
17
+ # = Methods
18
+ # - :email - require the email address is set and is a valid format
19
+ #
20
+ # = class methods
21
+ # - authenticate(email, password) - find user with given email, validate their password, return the user.
22
+ # - normalize_email(email) - clean up the given email and return it.
23
+ # - find_by_normalized_email(email) - normalize the given email, then look for the user with that email.
24
+ #
25
+ module Email
26
+ extend ActiveSupport::Concern
27
+
28
+ def self.required_fields(klass)
29
+ [:email]
30
+ end
31
+
32
+ included do
33
+ before_validation :normalize_email
34
+ validates :email,
35
+ email: { strict_mode: true },
36
+ presence: true,
37
+ uniqueness: { allow_blank: true }
38
+ end
39
+
40
+
41
+ module ClassMethods
42
+
43
+ def credentials(params)
44
+ # todo closure from configuration
45
+ [params[:session][:email], params[:session][:password]]
46
+ end
47
+
48
+ def authenticate(credentials)
49
+ user = find_by_credentials(credentials)
50
+ user && user.password_match?(credentials[1]) ? user : nil
51
+ end
52
+
53
+ def find_by_credentials(credentials)
54
+ email = credentials[0]
55
+ puts "find_by_credentials email: #{email}"
56
+ find_by_email normalize_email(email)
57
+ end
58
+
59
+ def normalize_email(email)
60
+ email.to_s.downcase.gsub(/\s+/, '')
61
+ end
62
+
63
+ end
64
+
65
+ # Sets the email on this instance to the value returned by
66
+ # {.normalize_email}
67
+ #
68
+ # @return [String]
69
+ def normalize_email
70
+ self.email = self.class.normalize_email(email)
71
+ end
72
+ end
73
+
74
+ end
75
+ end
76
+
@@ -0,0 +1,48 @@
1
+ require 'authenticate/callbacks/lifetimed'
2
+
3
+ module Authenticate
4
+ module Model
5
+
6
+ # The user session has a maximum allowed lifespan, after which the session is expired and requires
7
+ # re-authentication.
8
+ #
9
+ # = configuration
10
+ #
11
+ # Set the maximum session lifetime in the initializer, giving a timestamp.
12
+ #
13
+ # Authenticate.configure do |config|
14
+ # config.max_session_lifetime = 8.hours
15
+ # end
16
+ #
17
+ # If the max_session_lifetime configuration parameter is nil, the :lifetimed module is not loaded.
18
+ #
19
+ # = columns
20
+ # Requires the `current_sign_in_at` column. This column is managed by the :trackable plugin.
21
+ #
22
+ # = methods
23
+ # - max_session_timedout? - true if the user's session is too old and must be reaped
24
+ #
25
+ #
26
+ module Lifetimed
27
+ extend ActiveSupport::Concern
28
+
29
+ def self.required_fields(klass)
30
+ [:current_sign_in_at]
31
+ end
32
+
33
+ # Has the session reached its maximum allowed lifespan?
34
+ def max_session_timedout?
35
+ return false if max_session_lifetime.nil?
36
+ return false if current_sign_in_at.nil?
37
+ current_sign_in_at <= max_session_lifetime.ago
38
+ end
39
+
40
+ private
41
+
42
+ def max_session_lifetime
43
+ Authenticate.configuration.max_session_lifetime
44
+ end
45
+
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,47 @@
1
+ require 'authenticate/callbacks/timeoutable'
2
+
3
+ module Authenticate
4
+ module Model
5
+
6
+ # Expire user sessions that have not been accessed within a certain period of time.
7
+ # Expired users will be asked for credentials again.
8
+ #
9
+ # == Columns
10
+ #
11
+ # This module expects and tracks this column on your user model:
12
+ # - last_access_at - timestamp of the last access by the user
13
+ #
14
+ # == Configuration
15
+ #
16
+ # Timeoutable is enabled and configured with the `timeout_in` configuration parameter.
17
+ # `timeout_in` expects a timestamp. Example:
18
+ #
19
+ # Authenticate.configure do |config|
20
+ # config.timeout_in = 15.minutes
21
+ # end
22
+ #
23
+ # You must specify a non-nil timeout_in in your initializer to enable Timeoutable.
24
+ #
25
+ module Timeoutable
26
+ extend ActiveSupport::Concern
27
+
28
+ def self.required_fields(klass)
29
+ [:last_access_at]
30
+ end
31
+
32
+ # Checks whether the user session has expired based on configured time.
33
+ def timedout?
34
+ Rails.logger.info "User.timedout? timeout_in:#{timeout_in} last_access_at:#{last_access_at}"
35
+ return false if timeout_in.nil?
36
+ return false if last_access_at.nil?
37
+ # result = Time.now.utc > (last_access_at + timeout_in)
38
+ Rails.logger.info "User.timedout? #{last_access_at >= timeout_in.ago} timeout_in.ago:#{timeout_in.ago} last_access_at:#{last_access_at}"
39
+ last_access_at <= timeout_in.ago
40
+ end
41
+
42
+ def timeout_in
43
+ Authenticate.configuration.timeout_in
44
+ end
45
+ end
46
+ end
47
+ end