authenticate 0.1.0

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