devision 0.0.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 43c30705818f3d105f21c7270c92e7f1824225b0
4
+ data.tar.gz: aa8bb3b3b720f5336b42f13e7ff1ad18450c76cd
5
+ SHA512:
6
+ metadata.gz: c9da084babbbe7878ca794570626f27b8d4b15d02b91c1b209be41de3edcca642809bc0ad96fc908abfd1752a9628e75cb7f1517c06a916850853beabf974bfc
7
+ data.tar.gz: 73f23e9e1aeb2f8c8bcabd1444a9c97dabe0745938e641fdc099e2b3c37ca5b340a5c7e729218a3031c260cbad66b6615892de251bbfcc783ae01cf3f2e4930f
data/.gitignore ADDED
@@ -0,0 +1,23 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
23
+ *.sw[op]
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in devision.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 John Faucett
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # Devision
2
+
3
+ This is a gem that is heavily based off of Devise, think of it as Devise's little cousin.
4
+ Basically, it provides a lot of the functionality but doesn't make many assumptions about
5
+ how you want to want to structure your app, especially on the session handling end.
6
+
7
+ Here's some of the things it does not provide.
8
+ 1. No routing
9
+ 2. No automatic mailing or setup for mailers
10
+ 3. No controller helpers and session/cookie stuff.
11
+
12
+ So what does it provide, is probably your next question. It basically covers just the domain
13
+ model end, things like reset_password, etc.
14
+
15
+ ## Installation
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ gem 'devision'
20
+
21
+ And then execute:
22
+
23
+ $ bundle
24
+
25
+ Or install it yourself as:
26
+
27
+ $ gem install devision
28
+
29
+ ## Usage
30
+
31
+ TODO: Write usage instructions here
32
+
33
+ ## Contributing
34
+
35
+ 1. Fork it ( https://github.com/[my-github-username]/devision/fork )
36
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
37
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
38
+ 4. Push to the branch (`git push origin my-new-feature`)
39
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
data/devision.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # encoding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $:.unshift(lib) unless $:.include?(lib)
4
+ require 'devision/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'devision'
8
+ spec.version = Devision::VERSION::STRING
9
+ spec.authors = ['John Faucett']
10
+ spec.email = ['jwaterfaucett@gmail.com']
11
+ spec.summary = 'Devise on a Diet'
12
+ spec.license = 'MIT'
13
+
14
+ spec.files = `git ls-files -z`.split("\x0")
15
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
16
+ spec.test_files = spec.files.grep(%r{^(spec|features)/})
17
+ spec.require_paths = ["lib"]
18
+
19
+ spec.add_development_dependency 'bundler', '~> 1.6'
20
+ spec.add_development_dependency 'rake'
21
+
22
+ spec.add_development_dependency 'rspec', '~> 3.0.0'
23
+
24
+ spec.add_dependency 'railties', '>= 3.2.6', '< 5'
25
+ spec.add_dependency 'orm_adapter', '~> 0.5'
26
+ spec.add_dependency 'bcrypt', '~> 3.1'
27
+ spec.add_dependency 'thread_safe', '~> 0.3'
28
+
29
+ end
data/lib/devision.rb ADDED
@@ -0,0 +1,47 @@
1
+ require 'active_support'
2
+ require 'orm_adapter'
3
+ require 'active_support/core_ext/numeric/time'
4
+ require 'securerandom'
5
+
6
+ module Devision
7
+ autoload :TokenGenerator, 'devision/token_generator'
8
+
9
+ module Models
10
+ autoload :Config, 'devision/models/config'
11
+ autoload :Confirmable, 'devision/models/confirmable'
12
+ autoload :DatabaseAuthenticatable, 'devision/models/database_authenticatable'
13
+ autoload :Recoverable, 'devision/models/recoverable'
14
+ autoload :Rememberable, 'devision/models/rememberable'
15
+ end
16
+
17
+ # Devision configuration settings (for initializers)
18
+ require 'devision/configuration_options'
19
+
20
+
21
+ # Generate a user friendly 'nice' string randomly to be used as token.
22
+ def self.nice_token
23
+ SecureRandom.urlsafe_base64(15).tr('lIO0', 'sxyz')
24
+ end
25
+
26
+ # constant-time comparison algorithm to prevent timing attacks
27
+ def self.secure_compare(a, b)
28
+ return false if a.blank? || b.blank? || a.bytesize != b.bytesize
29
+ l = a.unpack "C#{a.bytesize}"
30
+
31
+ res = 0
32
+ b.each_byte { |byte| res |= byte ^ l.shift }
33
+ res == 0
34
+ end
35
+
36
+ # Digests a password using bcrypt.
37
+ def self.bcrypt(klass, password)
38
+ ::BCrypt::Password.create("#{password}#{klass.pepper}", cost: klass.stretches).to_s
39
+ end
40
+
41
+ # Default way to setup Devision. Just call this in an initializer
42
+ def self.setup
43
+ yield(self)
44
+ end
45
+
46
+
47
+ end
@@ -0,0 +1,118 @@
1
+ module Devision
2
+
3
+ # =========================
4
+ # AUTHENTICATABLE OPTIONS
5
+ # =========================
6
+
7
+ # Keys used when authenticating a user.
8
+ mattr_accessor :authentication_keys
9
+ @@authentication_keys = [ :email ]
10
+
11
+ # =========================
12
+ # RECOVERABLE OPTIONS
13
+ # =========================
14
+
15
+ # Defines which key will be used when recovering the password for an account
16
+ mattr_accessor :reset_password_keys
17
+ @@reset_password_keys = [ :email ]
18
+
19
+ # Time interval you can reset your password with a reset password key
20
+ mattr_accessor :reset_password_within
21
+ @@reset_password_within = 6.hours
22
+
23
+
24
+ # Email regex used to validate email formats. It simply asserts that
25
+ # an one (and only one) @ exists in the given string. This is mainly
26
+ # to give user feedback and not to assert the e-mail validity.
27
+ mattr_accessor :email_regexp
28
+ @@email_regexp = /\A[^@\s]+@([^@\s]+\.)+[^@\s]+\z/
29
+
30
+ # Range validation for password length
31
+ mattr_accessor :password_length
32
+ @@password_length = 6..128
33
+
34
+ # ======================
35
+ # REMEMBERABLE OPTIONS
36
+ # ======================
37
+
38
+ # Custom domain or key for cookies. Not set by default
39
+ # mattr_accessor :rememberable_options
40
+ # @@rememberable_options = {}
41
+
42
+ # The time the user will be remembered without asking for credentials again.
43
+ mattr_accessor :remember_for
44
+ @@remember_for = 2.weeks
45
+
46
+ # If true, extends the user's remember period when remembered via cookie.
47
+ mattr_accessor :extend_remember_period
48
+ @@extend_remember_period = false
49
+
50
+ # ========================
51
+ # CONFIRMABLE OPTIONS
52
+ # ========================
53
+
54
+ # Time interval you can access your account before confirming your account.
55
+ # nil - allows unconfirmed access for unlimited time
56
+ mattr_accessor :allow_unconfirmed_access_for
57
+ @@allow_unconfirmed_access_for = 0.days
58
+
59
+ # Time interval the confirmation token is valid. nil = unlimited
60
+ mattr_accessor :confirm_within
61
+ @@confirm_within = nil
62
+
63
+ # Defines which key will be used when confirming an account.
64
+ mattr_accessor :confirmation_keys
65
+ @@confirmation_keys = [ :email ]
66
+
67
+ # Defines if email should be reconfirmable.
68
+ # False by default for backwards compatibility.
69
+ mattr_accessor :reconfirmable
70
+ @@reconfirmable = false
71
+
72
+ # ====================
73
+ # LOCKABLE OPTIONS
74
+ # ====================
75
+
76
+ # Defines which strategy can be used to lock an account.
77
+ # Values: :failed_attempts, :none
78
+ mattr_accessor :lock_strategy
79
+ @@lock_strategy = :failed_attempts
80
+
81
+ # Defines which key will be used when locking and unlocking an account
82
+ mattr_accessor :unlock_keys
83
+ @@unlock_keys = [ :email ]
84
+
85
+ # Defines which strategy can be used to unlock an account.
86
+ # Values: :email, :time, :both
87
+ mattr_accessor :unlock_strategy
88
+ @@unlock_strategy = :both
89
+
90
+ # Number of authentication tries before locking an account
91
+ mattr_accessor :maximum_attempts
92
+ @@maximum_attempts = 20
93
+
94
+ # Time interval to unlock the account if :time is defined as unlock_strategy.
95
+ mattr_accessor :unlock_in
96
+ @@unlock_in = 1.hour
97
+
98
+ # ======================
99
+ # ENCRYPTION OPTIONS
100
+ # ======================
101
+
102
+ # Stores the token generator
103
+ mattr_accessor :token_generator
104
+ @@token_generator = nil
105
+
106
+ # Secret key used by the key generator
107
+ mattr_accessor :secret_key
108
+ @@secret_key = nil
109
+
110
+ # The number of times to encrypt password.
111
+ mattr_accessor :stretches
112
+ @@stretches = 10
113
+
114
+ # Used to encrypt password. Please generate one with rake secret.
115
+ mattr_accessor :pepper
116
+ @@pepper = nil
117
+
118
+ end
@@ -0,0 +1,44 @@
1
+ module Devision
2
+ module Authenticatable
3
+
4
+ extend ActiveSupport::Concern
5
+
6
+ def self.required_fields(klass)
7
+ []
8
+ end
9
+
10
+
11
+ module ClassMethods
12
+
13
+ def find_first_by_auth_conditions(tainted_conditions, options = {})
14
+ to_adapter.find_first(tainted_conditions.merge(options))
15
+ end
16
+
17
+ def find_or_initialize_with_error_by(attribute, value, error=:invalid)
18
+ find_or_initialize_with_errors([attribute], { attribute => value }, error)
19
+ end
20
+
21
+ def find_or_initialize_with_errors(required_attributes, attributes, error=:invalid)
22
+ attributes = attributes.slice(*required_attributes)
23
+ attributes.delete_if { |key, value| value.blank? }
24
+
25
+ if attributes.size == required_attributes.size
26
+ record = find_first_by_auth_conditions(attributes)
27
+ end
28
+
29
+ unless record
30
+ record = new
31
+ required_attributes.each do |key|
32
+ value = attributes[key]
33
+ record.public_send("#{key}=", value)
34
+ record.errors.add(key, value.present? ? error : :blank)
35
+ end
36
+ end
37
+
38
+ record
39
+ end
40
+
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,32 @@
1
+ module Devision
2
+ module Models
3
+
4
+ module Config
5
+
6
+ def self.add_options(mod, *accessors)
7
+ class << mod; attr_accessor :available_configs; end
8
+ mod.available_configs = accessors
9
+
10
+ accessors.each do |accessor|
11
+ mod.class_eval <<-METHOD, __FILE__, __LINE__ + 1
12
+ def #{accessor}
13
+ if defined?(@#{accessor})
14
+ @#{accessor}
15
+ elsif superclass.respond_to?(:#{accessor})
16
+ superclass.#{accessor}
17
+ else
18
+ Devise.#{accessor}
19
+ end
20
+ end
21
+
22
+ def #{accessor}=(value)
23
+ @#{accessor} = value
24
+ end
25
+ METHOD
26
+ end
27
+ end
28
+
29
+ end # Config
30
+
31
+ end # Models
32
+ end # Devision
@@ -0,0 +1,86 @@
1
+ module Devision
2
+ module Models
3
+ module Confirmable
4
+ extend ActiveSupport::Concern
5
+
6
+ # Fields required on the target Model
7
+ def self.required_fields(klass)
8
+ [:confirmation_token, :confirmed_at, :confirmation_sent_at]
9
+ end
10
+
11
+ included do
12
+
13
+ attr_reader :raw_confirmation_token
14
+
15
+ def confirm!
16
+ pending_any_confirmation do
17
+ if confirmation_period_expired?
18
+ self.errors.add(:email, :confirmation_period_expired)
19
+ return false
20
+ end
21
+ self.confirmation_token = nil
22
+ self.confirmed_at = Time.now.utc
23
+ save(validate: false)
24
+ end
25
+ end
26
+
27
+ def confirmed?
28
+ !!confirmed_at
29
+ end
30
+
31
+ def generate_confirmation_tokens
32
+ raw, enc = Devision.token_generator.generate(self.class, :confirmation_token)
33
+ @raw_confirmation_token = raw
34
+ self.confirmation_token = enc
35
+ self.confirmation_sent_at = Time.now.utc
36
+ end
37
+
38
+ def generate_confirmation_tokens!
39
+ generate_confirmation_tokens && save(validate: false)
40
+ end
41
+
42
+ def clear_confirmation_token
43
+ @raw_confirmation_token = nil
44
+ self.confirmation_token = nil
45
+ self.confirmation_sent_at = nil
46
+ end
47
+
48
+ # Checks whether the record requires any confirmation.
49
+ def pending_any_confirmation
50
+ if !confirmed?
51
+ yield
52
+ else
53
+ self.errors.add(:email, :already_confirmed)
54
+ false
55
+ end
56
+ end
57
+
58
+ def confirmation_period_expired?
59
+ self.class.confirm_within && (Time.now > self.confirmation_sent_at + self.class.confirm_within )
60
+ end
61
+
62
+ def confirmation_period_valid?
63
+ self.class.allow_unconfirmed_access_for.nil? || (confirmation_sent_at && confirmation_sent_at.utc >= self.class.allow_unconfirmed_access_for.ago)
64
+ end
65
+
66
+ end
67
+
68
+ module ClassMethods
69
+
70
+ def confirm_by_token(raw_confirmation_token)
71
+ original_token = raw_confirmation_token
72
+ saved_token = Devision.token_generator.digest(self, :confirmation_token, original_token)
73
+
74
+ confirmable = find_or_initialize_by(confirmation_token: saved_token)
75
+ confirmable.confirm! if confirmable.persisted?
76
+ confirmable.confirmation_token = original_token
77
+ confirmable
78
+ end
79
+
80
+ Devision::Models::Config.add_options(self, :allow_unconfirmed_access_for, :confirmation_keys, :reconfirmable, :confirm_within)
81
+ end
82
+
83
+
84
+ end # Confirmable
85
+ end # Models
86
+ end # Devision
@@ -0,0 +1,81 @@
1
+ require 'bcrypt'
2
+
3
+ module Devision
4
+
5
+ module Models
6
+ # Authenticatable Module, responsible for encrypting password and validating
7
+ # authenticity of a user while signing in.
8
+ #
9
+ # == Options
10
+ #
11
+ # == Examples
12
+ #
13
+ # User.find(1).valid_password?('password123') # returns true/false
14
+ #
15
+ module DatabaseAuthenticatable
16
+ extend ActiveSupport::Concern
17
+
18
+ # Fields required on the target Model
19
+ def self.required_fields(klass)
20
+ [:encrypted_password] + klass.authentication_keys
21
+ end
22
+
23
+ included do
24
+ attr_reader :password, :current_password
25
+ attr_accessor :password_confirmation
26
+ end
27
+
28
+ # Generates password encryption based on the given value.
29
+ def password=(new_password)
30
+ @password = new_password
31
+ self.encrypted_password = password_digest(@password) if @password.present?
32
+ end
33
+
34
+ # Verifies whether an password (ie from sign in) is the user password.
35
+ def valid_password?(password)
36
+ return false if encrypted_password.blank?
37
+ bcrypt = ::BCrypt::Password.new(encrypted_password)
38
+ password = ::BCrypt::Engine.hash_secret("#{password}#{self.class.pepper}", bcrypt.salt)
39
+ Devision.secure_compare(password, encrypted_password)
40
+ end
41
+
42
+ # Set password and password confirmation to nil
43
+ def clean_up_passwords
44
+ self.password = self.password_confirmation = nil
45
+ end
46
+
47
+ # A callback initiated after successfully authenticating. This can be
48
+ # used to insert your own logic that is only run after the user successfully
49
+ # authenticates.
50
+ #
51
+ # Example:
52
+ #
53
+ # def after_database_authentication
54
+ # self.update_attribute(:invite_code, nil)
55
+ # end
56
+ #
57
+ def after_database_authentication
58
+ end
59
+
60
+ # A reliable way to expose the salt regardless of the implementation.
61
+ def authenticatable_salt
62
+ encrypted_password[0,29] if encrypted_password
63
+ end
64
+
65
+ protected
66
+
67
+ # Digests the password using bcrypt. Custom encryption should override
68
+ # this method to apply their own algorithm.
69
+ #
70
+ # See https://github.com/plataformatec/devise-encryptable for examples
71
+ # of other encryption engines.
72
+ def password_digest(password)
73
+ Devision.bcrypt(self.class, password)
74
+ end
75
+
76
+ module ClassMethods
77
+ Devision::Models::Config.add_options(self, :pepper, :stretches)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,67 @@
1
+ module Devision
2
+ module Lockable
3
+ extend ActiveSupport::Concern
4
+
5
+ delegate :lock_strategy_enabled?, :unlock_strategy_enabled?, to: 'self.class'
6
+
7
+ def self.required_fields(klass)
8
+ attributes = []
9
+ attributes << :failed_attempts if klass.lock_strategy_enabled?(:failed_attempts)
10
+ attributes << :locked_at if klass.unlock_strategy_enabled?(:time)
11
+ attributes << :unlock_token if klass.unlock_strategy_enabled?(:email)
12
+ attributes
13
+ end
14
+
15
+ def lock_access!
16
+ self.locked_at = Time.now.utc
17
+ save(validate: false)
18
+ end
19
+
20
+ def unlock_access!
21
+ self.locked_at = nil
22
+ self.failed_attempts = 0 if respond_to?(:failed_attempts=)
23
+ self.unlock_token = nil if respond_to?(:unlock_token=)
24
+ save(validate: false)
25
+ end
26
+
27
+ def access_locked?
28
+ !!locked_at && !lock_expired?
29
+ end
30
+
31
+ protected
32
+ def attempts_exceeded?
33
+ self.failed_attempts >= self.class.maximum_attempts
34
+ end
35
+
36
+ def last_attempt?
37
+ self.failed_attempts == (self.class.maximum_attempts - 1)
38
+ end
39
+
40
+ def lock_expired?
41
+ if unlock_strategy_enabled?(:time)
42
+ locked_at && locked_at < self.class.unlock_in.ago
43
+ else
44
+ false
45
+ end
46
+ end
47
+
48
+ module ClassMethods
49
+ def unlock_access_by_token(unlock_token)
50
+ original_token = unlock_token
51
+ unlock_token = Devision.token_generator.digest(self, :unlock_token, unlock_token)
52
+
53
+ lockable = find_or_initialize_with_error_by(:unlock_token, unlock_token)
54
+ lockable.unlock_access! if lockable.persisted?
55
+ lockable.unlock_token = original_token
56
+ lockable
57
+ end
58
+
59
+ def unlock_strategy_enabled?(strategy)
60
+ self.lock_strategy == strategy
61
+ end
62
+
63
+ Devision::Models::Config.add_options(self, :maximum_attempts, :lock_strategy, :unlock_strategy, :unlock_in, :unlock_keys)
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,137 @@
1
+ module Devision
2
+ module Models
3
+
4
+ # Recoverable takes care of resetting the user password and send reset instructions.
5
+ #
6
+ # ==Options
7
+ #
8
+ # Recoverable adds the following options to a model
9
+ #
10
+ # * +reset_password_keys+: the keys you want to use when recovering the password for an account
11
+ #
12
+ # == Examples
13
+ #
14
+ # # resets the user password and save the record, true if valid passwords are given, otherwise false
15
+ # User.find(1).reset_password!('password123', 'password123')
16
+ #
17
+ # # only resets the user password, without saving the record
18
+ # user = User.find(1)
19
+ # user.reset_password('password123', 'password123')
20
+ #
21
+ # # creates a new token and send it with instructions about how to reset the password
22
+ # User.find(1).send_reset_password_instructions
23
+ #
24
+ module Recoverable
25
+ extend ActiveSupport::Concern
26
+
27
+ def self.required_fields(klass)
28
+ [:reset_password_sent_at, :reset_password_token]
29
+ end
30
+
31
+ included do
32
+ attr_accessor :raw_reset_password_token
33
+ end
34
+
35
+ # Update password saving the record and clearing token. Returns true if
36
+ # the passwords are valid and the record was saved, false otherwise.
37
+ def reset_password!(new_password, new_password_confirmation)
38
+ self.password = new_password
39
+ self.password_confirmation = new_password_confirmation
40
+
41
+ if valid?
42
+ clear_reset_password_token
43
+ after_password_reset
44
+ end
45
+
46
+ save
47
+ end
48
+
49
+ # Resets reset password token and send reset password instructions by email.
50
+ # Returns the token which can then be sent via email
51
+ def generate_password_tokens
52
+ @raw_password_reset_token, enc = Devision.token_generator.generate(self.class, :reset_password_token)
53
+
54
+ self.reset_password_token = enc
55
+ self.reset_password_sent_at = Time.now.utc
56
+ @raw_password_reset_token
57
+ end
58
+
59
+ def generate_password_tokens!
60
+ generate_password_tokens && save(validate: false)
61
+ end
62
+
63
+ # Checks if the reset password token sent is within the limit time.
64
+ # We do this by calculating if the difference between today and the
65
+ # sending date does not exceed the confirm in time configured.
66
+ # Returns true if the resource is not responding to reset_password_sent_at at all.
67
+ # reset_password_within is a model configuration, must always be an integer value.
68
+ #
69
+ # Example:
70
+ #
71
+ # # reset_password_within = 1.day and reset_password_sent_at = today
72
+ # reset_password_period_valid? # returns true
73
+ #
74
+ # # reset_password_within = 5.days and reset_password_sent_at = 4.days.ago
75
+ # reset_password_period_valid? # returns true
76
+ #
77
+ # # reset_password_within = 5.days and reset_password_sent_at = 5.days.ago
78
+ # reset_password_period_valid? # returns false
79
+ #
80
+ # # reset_password_within = 0.days
81
+ # reset_password_period_valid? # will always return false
82
+ #
83
+ def reset_password_period_valid?
84
+ reset_password_sent_at && reset_password_sent_at.utc >= self.class.reset_password_within.ago
85
+ end
86
+
87
+ protected
88
+
89
+ # Removes reset_password token
90
+ def clear_reset_password_token
91
+ self.raw_reset_password_token = nil
92
+ self.reset_password_token = nil
93
+ self.reset_password_sent_at = nil
94
+ end
95
+
96
+ def after_password_reset
97
+ end
98
+
99
+ module ClassMethods
100
+ # Attempt to find a user by its email. If a record is found, send new
101
+ # password instructions to it. If user is not found, returns a new user
102
+ # with an email not found error.
103
+ # Attributes must contain the user's email
104
+ def send_reset_password_instructions(attributes={})
105
+ recoverable = find_or_initialize_with_errors(reset_password_keys, attributes, :not_found)
106
+ recoverable.send_reset_password_instructions if recoverable.persisted?
107
+ recoverable
108
+ end
109
+
110
+ # Attempt to find a user by its reset_password_token to reset its
111
+ # password. If a user is found and token is still valid, reset its password and automatically
112
+ # try saving the record. If not user is found, returns a new user
113
+ # containing an error in reset_password_token attribute.
114
+ # Attributes must contain reset_password_token, password and confirmation
115
+ def reset_password_by_token(attributes={})
116
+ original_token = attributes[:reset_password_token]
117
+ reset_password_token = Devision.token_generator.digest(self, :reset_password_token, original_token)
118
+
119
+ recoverable = find_or_initialize_with_errors(:reset_password_token, reset_password_token)
120
+
121
+ if recoverable.persisted?
122
+ if recoverable.reset_password_period_valid?
123
+ recoverable.reset_password!(attributes[:password], attributes[:password_confirmation])
124
+ else
125
+ recoverable.errors.add(:reset_password_token, :expired)
126
+ end
127
+ end
128
+
129
+ recoverable.reset_password_token = original_token
130
+ recoverable
131
+ end
132
+
133
+ Devision::Models::Config.add_options(self, :reset_password_keys, :reset_password_within)
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,114 @@
1
+ module Devision
2
+ module Models
3
+ # Rememberable manages generating and clearing token for remember the user
4
+ # from a saved cookie. Rememberable also has utility methods for dealing
5
+ # with serializing the user into the cookie and back from the cookie, trying
6
+ # to lookup the record based on the saved information.
7
+ # You probably wouldn't use rememberable methods directly, they are used
8
+ # mostly internally for handling the remember token.
9
+ #
10
+ # == Options
11
+ #
12
+ # Rememberable adds the following options in devise_for:
13
+ #
14
+ # * +remember_for+: the time you want the user will be remembered without
15
+ # asking for credentials. After this time the user will be blocked and
16
+ # will have to enter their credentials again. This configuration is also
17
+ # used to calculate the expires time for the cookie created to remember
18
+ # the user. By default remember_for is 2.weeks.
19
+ #
20
+ # * +extend_remember_period+: if true, extends the user's remember period
21
+ # when remembered via cookie. False by default.
22
+ #
23
+ # * +rememberable_options+: configuration options passed to the created cookie.
24
+ #
25
+ # == Examples
26
+ #
27
+ # User.find(1).remember_me! # regenerating the token
28
+ # User.find(1).forget_me! # clearing the token
29
+ #
30
+ # # generating info to put into cookies
31
+ # User.serialize_into_cookie(user)
32
+ #
33
+ # # lookup the user based on the incoming cookie information
34
+ # User.serialize_from_cookie(cookie_string)
35
+ module Rememberable
36
+ extend ActiveSupport::Concern
37
+
38
+ attr_accessor :remember_me, :extend_remember_period
39
+
40
+ def self.required_fields(klass)
41
+ [:remember_created_at]
42
+ end
43
+
44
+ # Generate a new remember token and save the record without validations
45
+ # unless remember_across_browsers is true and the user already has a valid token.
46
+ def remember_me!(extend_period=false)
47
+ self.remember_token = self.class.remember_token if generate_remember_token?
48
+ self.remember_created_at = Time.now.utc if generate_remember_timestamp?(extend_period)
49
+ save(validate: false) if self.changed?
50
+ end
51
+
52
+ # If the record is persisted, remove the remember token (but only if
53
+ # it exists), and save the record without validations.
54
+ def forget_me!
55
+ return unless persisted?
56
+ self.remember_token = nil if respond_to?(:remember_token=)
57
+ self.remember_created_at = nil
58
+ save(validate: false)
59
+ end
60
+
61
+ # Remember token should be expired if expiration time not overpass now.
62
+ def remember_expired?
63
+ remember_created_at.nil? || (remember_expires_at <= Time.now.utc)
64
+ end
65
+
66
+ # Remember token expires at created time + remember_for configuration
67
+ def remember_expires_at
68
+ remember_created_at + self.class.remember_for
69
+ end
70
+
71
+ def rememberable_value
72
+ if respond_to?(:remember_token)
73
+ remember_token
74
+ elsif respond_to?(:authenticatable_salt) && (salt = authenticatable_salt)
75
+ salt
76
+ else
77
+ raise "authenticable_salt returned nil for the #{self.class.name} model. " \
78
+ "In order to use rememberable, you must ensure a password is always set " \
79
+ "or have a remember_token column in your model or implement your own " \
80
+ "rememberable_value in the model with custom logic."
81
+ end
82
+ end
83
+
84
+ def rememberable_options
85
+ self.class.rememberable_options
86
+ end
87
+
88
+ protected
89
+
90
+ def generate_remember_token? #:nodoc:
91
+ respond_to?(:remember_token) && remember_expired?
92
+ end
93
+
94
+ # Generate a timestamp if extend_remember_period is true, if no remember_token
95
+ # exists, or if an existing remember token has expired.
96
+ def generate_remember_timestamp?(extend_period) #:nodoc:
97
+ extend_period || remember_created_at.nil? || remember_expired?
98
+ end
99
+
100
+ module ClassMethods
101
+
102
+ # Generate a token checking if one does not already exist in the database.
103
+ def remember_token #:nodoc:
104
+ loop do
105
+ token = Devision.nice_token
106
+ break token unless to_adapter.find_first({ remember_token: token })
107
+ end
108
+ end
109
+
110
+ Devise::Models::Config.add_options(self, :remember_for, :extend_remember_period, :rememberable_options)
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,72 @@
1
+ # Deprecate: Copied verbatim from Rails source, remove once we move to Rails 4 only.
2
+ require 'thread_safe'
3
+ require 'openssl'
4
+ require 'securerandom'
5
+
6
+ module Devision
7
+
8
+ class TokenGenerator
9
+ def initialize(key_generator, digest="SHA256")
10
+ @key_generator = key_generator
11
+ @digest = digest
12
+ end
13
+
14
+ def digest(klass, column, value)
15
+ value.present? && OpenSSL::HMAC.hexdigest(@digest, key_for(column), value.to_s)
16
+ end
17
+
18
+ def generate(klass, column)
19
+ key = key_for(column)
20
+
21
+ loop do
22
+ raw = Devision.nice_token
23
+ enc = OpenSSL::HMAC.hexdigest(@digest, key, raw)
24
+ break [raw, enc] unless klass.to_adapter.find_first({ column => enc })
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def key_for(column)
31
+ @key_generator.generate_key("Devision #{column}")
32
+ end
33
+ end # TokenGenerator
34
+
35
+ # KeyGenerator is a simple wrapper around OpenSSL's implementation of PBKDF2
36
+ # It can be used to derive a number of keys for various purposes from a given secret.
37
+ # This lets Rails applications have a single secure secret, but avoid reusing that
38
+ # key in multiple incompatible contexts.
39
+ class KeyGenerator
40
+ def initialize(secret, options = {})
41
+ @secret = secret
42
+ # The default iterations are higher than required for our key derivation uses
43
+ # on the off chance someone uses this for password storage
44
+ @iterations = options[:iterations] || 2**16
45
+ end
46
+
47
+ # Returns a derived key suitable for use. The default key_size is chosen
48
+ # to be compatible with the default settings of ActiveSupport::MessageVerifier.
49
+ # i.e. OpenSSL::Digest::SHA1#block_length
50
+ def generate_key(salt, key_size=64)
51
+ OpenSSL::PKCS5.pbkdf2_hmac_sha1(@secret, salt, @iterations, key_size)
52
+ end
53
+ end # KeyGenerator
54
+
55
+ # CachingKeyGenerator is a wrapper around KeyGenerator which allows users to avoid
56
+ # re-executing the key generation process when it's called using the same salt and
57
+ # key_size
58
+ class CachingKeyGenerator
59
+ def initialize(key_generator)
60
+ @key_generator = key_generator
61
+ @cache_keys = ThreadSafe::Cache.new
62
+ end
63
+
64
+ # Returns a derived key suitable for use. The default key_size is chosen
65
+ # to be compatible with the default settings of ActiveSupport::MessageVerifier.
66
+ # i.e. OpenSSL::Digest::SHA1#block_length
67
+ def generate_key(salt, key_size=64)
68
+ @cache_keys["#{salt}#{key_size}"] ||= @key_generator.generate_key(salt, key_size)
69
+ end
70
+ end # CachingKeyGenerator
71
+
72
+ end # Devision
@@ -0,0 +1,13 @@
1
+ module Devision
2
+ module VERSION
3
+
4
+ def self.gem_version
5
+ @gem_version ||= Gem::Version.new(STRING)
6
+ end
7
+
8
+ MAJOR = 0
9
+ MINOR = 0
10
+ PATCH = 0
11
+ STRING = [MAJOR,MINOR,PATCH].join('.').freeze
12
+ end
13
+ end
metadata ADDED
@@ -0,0 +1,165 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: devision
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - John Faucett
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-08-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 3.0.0
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 3.0.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: railties
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 3.2.6
62
+ - - "<"
63
+ - !ruby/object:Gem::Version
64
+ version: '5'
65
+ type: :runtime
66
+ prerelease: false
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: 3.2.6
72
+ - - "<"
73
+ - !ruby/object:Gem::Version
74
+ version: '5'
75
+ - !ruby/object:Gem::Dependency
76
+ name: orm_adapter
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '0.5'
82
+ type: :runtime
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '0.5'
89
+ - !ruby/object:Gem::Dependency
90
+ name: bcrypt
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '3.1'
96
+ type: :runtime
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '3.1'
103
+ - !ruby/object:Gem::Dependency
104
+ name: thread_safe
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '0.3'
110
+ type: :runtime
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '0.3'
117
+ description:
118
+ email:
119
+ - jwaterfaucett@gmail.com
120
+ executables: []
121
+ extensions: []
122
+ extra_rdoc_files: []
123
+ files:
124
+ - ".gitignore"
125
+ - Gemfile
126
+ - LICENSE.txt
127
+ - README.md
128
+ - Rakefile
129
+ - devision.gemspec
130
+ - lib/devision.rb
131
+ - lib/devision/configuration_options.rb
132
+ - lib/devision/models/authenticatable.rb
133
+ - lib/devision/models/config.rb
134
+ - lib/devision/models/confirmable.rb
135
+ - lib/devision/models/database_authenticatable.rb
136
+ - lib/devision/models/lockable.rb
137
+ - lib/devision/models/recoverable.rb
138
+ - lib/devision/models/rememberable.rb
139
+ - lib/devision/token_generator.rb
140
+ - lib/devision/version.rb
141
+ homepage:
142
+ licenses:
143
+ - MIT
144
+ metadata: {}
145
+ post_install_message:
146
+ rdoc_options: []
147
+ require_paths:
148
+ - lib
149
+ required_ruby_version: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ required_rubygems_version: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
159
+ requirements: []
160
+ rubyforge_project:
161
+ rubygems_version: 2.4.1
162
+ signing_key:
163
+ specification_version: 4
164
+ summary: Devise on a Diet
165
+ test_files: []