sparkly-auth 1.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.
Files changed (71) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +17 -0
  3. data/Rakefile +50 -0
  4. data/VERSION +1 -0
  5. data/app/controllers/sparkly_accounts_controller.rb +59 -0
  6. data/app/controllers/sparkly_controller.rb +47 -0
  7. data/app/controllers/sparkly_sessions_controller.rb +52 -0
  8. data/app/models/password.rb +3 -0
  9. data/app/models/remembrance_token.rb +50 -0
  10. data/app/views/sparkly_accounts/edit.html.erb +24 -0
  11. data/app/views/sparkly_accounts/new.html.erb +24 -0
  12. data/app/views/sparkly_accounts/show.html.erb +0 -0
  13. data/app/views/sparkly_sessions/new.html.erb +22 -0
  14. data/dependencies.rb +1 -0
  15. data/generators/sparkly/USAGE +27 -0
  16. data/generators/sparkly/sparkly_generator.rb +76 -0
  17. data/generators/sparkly/templates/accounts_controller.rb +65 -0
  18. data/generators/sparkly/templates/accounts_helper.rb +2 -0
  19. data/generators/sparkly/templates/help_file.txt +56 -0
  20. data/generators/sparkly/templates/initializer.rb +30 -0
  21. data/generators/sparkly/templates/migrations/add_confirmed_to_sparkly_passwords.rb +9 -0
  22. data/generators/sparkly/templates/migrations/create_sparkly_passwords.rb +19 -0
  23. data/generators/sparkly/templates/migrations/create_sparkly_remembered_tokens.rb +15 -0
  24. data/generators/sparkly/templates/sessions_controller.rb +45 -0
  25. data/generators/sparkly/templates/sessions_helper.rb +2 -0
  26. data/generators/sparkly/templates/tasks/migrations.rb +1 -0
  27. data/generators/sparkly/templates/views/sparkly_accounts/edit.html.erb +24 -0
  28. data/generators/sparkly/templates/views/sparkly_accounts/new.html.erb +24 -0
  29. data/generators/sparkly/templates/views/sparkly_accounts/show.html.erb +0 -0
  30. data/generators/sparkly/templates/views/sparkly_sessions/new.html.erb +22 -0
  31. data/init.rb +44 -0
  32. data/lib/auth.rb +52 -0
  33. data/lib/auth/behavior/base.rb +64 -0
  34. data/lib/auth/behavior/core.rb +87 -0
  35. data/lib/auth/behavior/core/authenticated_model_methods.rb +52 -0
  36. data/lib/auth/behavior/core/controller_extensions.rb +52 -0
  37. data/lib/auth/behavior/core/controller_extensions/class_methods.rb +24 -0
  38. data/lib/auth/behavior/core/controller_extensions/current_user.rb +54 -0
  39. data/lib/auth/behavior/core/password_methods.rb +65 -0
  40. data/lib/auth/behavior/remember_me.rb +17 -0
  41. data/lib/auth/behavior/remember_me/configuration.rb +21 -0
  42. data/lib/auth/behavior/remember_me/controller_extensions.rb +66 -0
  43. data/lib/auth/behavior_lookup.rb +10 -0
  44. data/lib/auth/configuration.rb +328 -0
  45. data/lib/auth/encryptors/sha512.rb +20 -0
  46. data/lib/auth/generators/configuration_generator.rb +20 -0
  47. data/lib/auth/generators/controllers_generator.rb +34 -0
  48. data/lib/auth/generators/migration_generator.rb +32 -0
  49. data/lib/auth/generators/route_generator.rb +19 -0
  50. data/lib/auth/generators/views_generator.rb +26 -0
  51. data/lib/auth/model.rb +94 -0
  52. data/lib/auth/observer.rb +21 -0
  53. data/lib/auth/target_list.rb +5 -0
  54. data/lib/auth/tasks/migrations.rb +71 -0
  55. data/lib/auth/token.rb +10 -0
  56. data/lib/sparkly-auth.rb +1 -0
  57. data/rails/init.rb +17 -0
  58. data/rails/routes.rb +19 -0
  59. data/sparkly-auth.gemspec +143 -0
  60. data/spec/controllers/application_controller_spec.rb +13 -0
  61. data/spec/generators/sparkly_spec.rb +64 -0
  62. data/spec/lib/auth/behavior/core_spec.rb +184 -0
  63. data/spec/lib/auth/behavior/remember_me_spec.rb +127 -0
  64. data/spec/lib/auth/extensions/controller_spec.rb +32 -0
  65. data/spec/lib/auth/model_spec.rb +57 -0
  66. data/spec/lib/auth_spec.rb +32 -0
  67. data/spec/mocks/models/user.rb +3 -0
  68. data/spec/routes_spec.rb +24 -0
  69. data/spec/spec_helper.rb +61 -0
  70. data/spec/views_spec.rb +18 -0
  71. metadata +210 -0
@@ -0,0 +1,87 @@
1
+ module Auth
2
+ module Behavior
3
+ # Adds the most basic authentication behavior to registered models. Passwords have the following
4
+ # validations added:
5
+ # - uniqueness of :secret, :scope => [ :authenticatable_type, :authenticatable_id ]
6
+ # - presence of :secret
7
+ # - format of :secret ("must be at least 7 characters with at least 1 uppercase, 1 lowercase and 1 number")
8
+ # - confirmation of :secret, if secret has changed
9
+ # - presence of :secret_confirmation, if secret has changed
10
+ #
11
+ # Additionally, the following methods are added:
12
+ # #expired?
13
+ #
14
+ # The authenticated model(s) will have the following methods added to them:
15
+ # #password_expired?
16
+ #
17
+ class Core < Auth::Behavior::Base
18
+ migration "create_sparkly_passwords"
19
+
20
+ def apply_to_passwords(password_model)
21
+ password_model.instance_eval do
22
+ belongs_to :authenticatable, :polymorphic => true
23
+
24
+ validates_length_of :unencrypted_secret, :minimum => Auth.minimum_password_length,
25
+ :message => "must be at least #{Auth.minimum_password_length} characters",
26
+ :if => :secret_changed?
27
+ validates_format_of :unencrypted_secret, :with => Auth.password_format, :allow_blank => true,
28
+ :message => Auth.password_format_message,
29
+ :if => :secret_changed?
30
+
31
+ validates_presence_of :secret
32
+ validates_confirmation_of :secret, :if => :secret_changed?
33
+ validates_presence_of :secret_confirmation, :if => :secret_changed?
34
+ validates_presence_of :persistence_token
35
+ validates_uniqueness_of :persistence_token, :if => :persistence_token_changed?
36
+ attr_protected :secret, :secret_confirmation
37
+
38
+ include Auth::Behavior::Core::PasswordMethods
39
+
40
+ validate do |password|
41
+ password.errors.rename_attribute("unencrypted_secret", "secret")
42
+ end
43
+ end
44
+ end
45
+
46
+ def apply_to_accounts(model_config)
47
+ model_config.target.instance_eval do
48
+ has_many :passwords, :dependent => :destroy, :as => :authenticatable, :autosave => true
49
+
50
+ attr_protected :password, :password_confirmation
51
+ validates_presence_of sparkly_config.key
52
+ validates_uniqueness_of sparkly_config.key
53
+ validates_presence_of :password
54
+
55
+ include Auth::Behavior::Core::AuthenticatedModelMethods
56
+
57
+ after_save do |record|
58
+ # clear out old passwords so we're conforming to Auth.password_history_length
59
+ while record.passwords.length > Auth.password_history_length
60
+ record.passwords.shift.destroy
61
+ end
62
+ end
63
+
64
+ validate do |account|
65
+ account.errors.rename_attribute("passwords.secret", "password")
66
+ account.errors.rename_attribute("passwords.secret_confirmation", "password_confirmation")
67
+
68
+ # the various salts make it impossible to do this:
69
+ # validates_uniqueness_of :secret, :scope => [ :authenticatable_type, :authenticatable_id ],
70
+ # :message => Auth.password_uniqueness_message
71
+ # so we have to do it programmatically.
72
+ if account.password_changed?
73
+ secret = account.password_model.unencrypted_secret
74
+ account.passwords.each do |password|
75
+ unless password.new_record? # unless it's the one we're creating
76
+ if password.matches?(secret)
77
+ account.errors.add(:password, Auth.password_uniqueness_message)
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,52 @@
1
+ module Auth::Behavior::Core::AuthenticatedModelMethods
2
+ def self.included(base)
3
+ base.instance_eval do
4
+ delegate :persistence_token, :single_access_token, :perishable_token, :to => :password_model
5
+ end
6
+ end
7
+
8
+ def password_expired?
9
+ passwords.empty? || passwords.last.created_at < sparkly_config.password_update_frequency.ago
10
+ end
11
+
12
+ def password_model
13
+ passwords.empty? || @new_password ? new_password : passwords.last
14
+ end
15
+
16
+ def password_required?
17
+ new_record? || passwords.empty? || passwords.last.secret.blank?
18
+ end
19
+
20
+ def password_matches?(phrase)
21
+ password_model.matches?(phrase)
22
+ end
23
+
24
+ def password
25
+ password_model.secret
26
+ end
27
+
28
+ def password=(value)
29
+ new_password.secret = value
30
+ end
31
+
32
+ def password_confirmation=(value)
33
+ new_password.secret_confirmation = value
34
+ end
35
+
36
+ def password_confirmation
37
+ password_model.secret_confirmation
38
+ end
39
+
40
+ def password_changed?
41
+ password_model.new_record? || password_model.secret_changed?
42
+ end
43
+
44
+ def after_save
45
+ @new_password = nil
46
+ end
47
+
48
+ private
49
+ def new_password
50
+ @new_password ||= returning(Password.new) { |p| passwords << p }
51
+ end
52
+ end
@@ -0,0 +1,52 @@
1
+ module Auth::Behavior::Core::ControllerExtensions
2
+ def self.included(base)
3
+ base.instance_eval do
4
+ include Auth::Behavior::Core::ControllerExtensions::CurrentUser
5
+ extend Auth::Behavior::Core::ControllerExtensions::ClassMethods
6
+ helper_method :new_session_path, :current_user
7
+ hide_action :current_user, :find_current_session, :require_login, :require_logout, :login!, :logout!,
8
+ :redirect_back_or_default, :new_session_path, :store_location
9
+ end
10
+ end
11
+
12
+ def require_login
13
+ unless current_user
14
+ store_location
15
+ flash[:notice] = @session_timeout_message || Auth.login_required_message
16
+ login_path = Auth.default_login_path ? send(Auth.default_login_path) : Auth.default_destination
17
+ redirect_to login_path
18
+ end
19
+ end
20
+
21
+ def store_location(url = request.request_uri)
22
+ session[:destination] = url
23
+ end
24
+
25
+ def require_logout
26
+ redirect_back_or_default Auth.default_destination, Auth.logout_required_message if current_user
27
+ end
28
+
29
+ # Forcibly logs in the current client as the specified user.
30
+ #
31
+ # The options hash is unused, and is reserved for other behaviors to make use of.
32
+ # For instance, the "remember me" behavior checks for a :remember option and, if true, sets a remembrance token
33
+ # cookie.
34
+ def login!(user, options = {})
35
+ session[:session_token] = user.persistence_token
36
+ session[:active_at] = Time.now
37
+ @current_user = user
38
+ end
39
+
40
+ # Forcibly logs out the current client.
41
+ #
42
+ # The options hash is unused, and is reserved for other behaviors to make use of.
43
+ #
44
+ def logout!(options = {})
45
+ session[:session_token] = session[:active_at] = nil
46
+ end
47
+
48
+ def redirect_back_or_default(path, notice = nil)
49
+ flash[:notice] = notice if notice
50
+ redirect_to session.delete(:destination) || path
51
+ end
52
+ end
@@ -0,0 +1,24 @@
1
+ module Auth::Behavior::Core::ControllerExtensions::ClassMethods
2
+ def require_login_for(*actions)
3
+ before_filter :require_login, actions.extract_options!.merge(:only => actions)
4
+ end
5
+
6
+ def require_logout_for(*actions)
7
+ before_filter :require_logout, actions.extract_options!.merge(:only => actions)
8
+ end
9
+
10
+ def require_login(*args)
11
+ before_filter :require_login, *args
12
+ end
13
+
14
+ def require_logout(*args)
15
+ before_filter :require_logout, *args
16
+ end
17
+
18
+ alias_method :requires_login, :require_login
19
+ alias_method :require_user, :require_login
20
+ alias_method :requires_user, :require_login
21
+ alias_method :requires_logout, :require_logout
22
+ alias_method :require_no_user, :require_logout
23
+ alias_method :requires_no_user, :require_logout
24
+ end
@@ -0,0 +1,54 @@
1
+ module Auth::Behavior::Core::ControllerExtensions::CurrentUser
2
+ def self.included(base)
3
+ base.send(:hide_action, :current_user_from_session, :timeout_current_session, :authenticate_with_persistence_token,
4
+ :authenticate_with_single_access_token, :authenticate_with_session_cookie, :authenticate_current_user)
5
+ end
6
+
7
+ def current_user
8
+ return @current_user unless @current_user.nil?
9
+ @current_user = false
10
+ authenticate_current_user
11
+ @current_user
12
+ end
13
+
14
+ def authenticate_current_user
15
+ if session && session[:session_token]
16
+ authenticate_with_session_cookie
17
+ elsif params && params[:single_access_token] # single access token, useful for WS APIs
18
+ authenticate_with_single_access_token
19
+ end
20
+ end
21
+
22
+ def authenticate_with_session_cookie
23
+ if Auth.session_duration.nil? || session[:active_at] > Auth.session_duration.ago
24
+ authenticate_with_persistence_token
25
+ else
26
+ timeout_current_session
27
+ end
28
+ end
29
+
30
+ def authenticate_with_single_access_token
31
+ # There is no session duration because this works per-request.
32
+ password = Password.find_by_single_access_token(params[:single_access_token], :include => :authenticatable)
33
+ @current_user = password.authenticatable if password
34
+ end
35
+
36
+ def authenticate_with_persistence_token
37
+ password = Password.find_by_persistence_token(session[:session_token], :include => :authenticatable)
38
+ if password
39
+ @current_user = password.authenticatable
40
+ login! @current_user # to refresh session timeout
41
+ else
42
+ # Something weird happened and the user's password data can no longer be found. Log him out to prevent
43
+ # anything else from going wrong.
44
+ logout!
45
+ end
46
+ end
47
+
48
+ def timeout_current_session
49
+ logout!
50
+ # We'll put the message in the notice, but if the current page requires a login, the flash will be over
51
+ # written. That's where @session_timeout_message comes in.
52
+ flash[:notice] = @session_timeout_message = Auth.session_timeout_message
53
+ end
54
+ end
@@ -0,0 +1,65 @@
1
+ module Auth::Behavior::Core::PasswordMethods
2
+ def self.included(base)
3
+ # so apparently dynamic methods haven't been generated by AR yet, so this stuff's been moved to
4
+ # #after_initialize. Less than ideal but whatever.
5
+ # base.send(:alias_method_chain, :secret=, :encryption)
6
+ # base.send(:alias_method_chain, :secret_confirmation=, :encryption)
7
+ end
8
+
9
+ def after_initialize
10
+ # FIXME: HACK - see self.included(base)
11
+
12
+ if attributes.keys.include?('secret')
13
+ self.secret # uh, makes AR define the method, I guess? This feels clunky...
14
+ end
15
+
16
+ class << self
17
+ alias_method_chain :secret=, :encryption
18
+ alias_method_chain :secret_confirmation=, :encryption
19
+ end
20
+ end
21
+
22
+ def expired?
23
+ authenticatable.password_expired?
24
+ end
25
+
26
+ def encrypt(p)
27
+ self.salt ||= Auth::Token.new.to_s
28
+ Auth.encryptor.encrypt(p, salt)
29
+ end
30
+
31
+ def matches?(phrase)
32
+ Auth.encryptor.matches?(secret, phrase, salt)
33
+ end
34
+
35
+ def reset_persistence_token
36
+ self.persistence_token = Auth::Token.new.to_s
37
+ end
38
+
39
+ def reset_single_access_token
40
+ self.single_access_token = Auth::Token.new.to_s
41
+ end
42
+
43
+ def reset_perishable_token
44
+ self.perishable_token = Auth::Token.new.to_s
45
+ end
46
+
47
+ def secret_with_encryption=(phrase)
48
+ @unencrypted_secret = phrase
49
+ encrypted_phrase = phrase.blank? ? phrase : encrypt(phrase)
50
+ returning self.secret_without_encryption = encrypted_phrase do
51
+ reset_persistence_token
52
+ reset_single_access_token unless single_access_token # don't reset after it has a value
53
+ reset_perishable_token
54
+ end
55
+ end
56
+
57
+ def secret_confirmation_with_encryption=(phrase)
58
+ encrypted_phrase = phrase.blank? ? phrase : encrypt(phrase)
59
+ self.secret_confirmation_without_encryption = encrypted_phrase
60
+ end
61
+
62
+ def unencrypted_secret
63
+ @unencrypted_secret
64
+ end
65
+ end
@@ -0,0 +1,17 @@
1
+ module Auth
2
+ module Behavior
3
+ class RememberMe < Auth::Behavior::Base
4
+ migration "create_sparkly_remembered_tokens"
5
+
6
+ def apply_to_passwords(password)
7
+ # no effect
8
+ end
9
+
10
+ def apply_to_accounts(model_config)
11
+ model_config.target.instance_eval do
12
+ has_many :remembrance_tokens, :dependent => :destroy, :as => :authenticatable
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,21 @@
1
+ class Auth::Behavior::RememberMe::Configuration
2
+ # Message to be displayed in flash[:error] when a likely theft of the remember token has been detected.
3
+ attr_accessor :token_theft_message
4
+
5
+ # How long can a user stay logged in?
6
+ attr_accessor :duration
7
+
8
+ # Provides a handle back to the root configuration object.
9
+ attr_reader :configuration
10
+
11
+ # Returns true if the root configuration object's behaviors include :remember_me.
12
+ def enabled?
13
+ configuration.behaviors.include? :remember_me
14
+ end
15
+
16
+ def initialize(configuration)
17
+ @configuration = configuration
18
+ @token_theft_message = "Your account may have been hijacked recently! Verify that all settings are correct."
19
+ @duration = 6.months
20
+ end
21
+ end
@@ -0,0 +1,66 @@
1
+ module Auth::Behavior::RememberMe::ControllerExtensions
2
+ # If a :remember option is given, a remembrance cookie will be set. If omitted, the cookie will be, too.
3
+ def login_with_remembrance!(user, options = {})
4
+ login_without_remembrance!(user)
5
+
6
+ if options[:remember]
7
+ token = RemembranceToken.create!(:authenticatable => user)
8
+ set_remembrance_token_cookie(token)
9
+ end
10
+ end
11
+
12
+ # If a :forget option is given, the remembrance cookie will also be deleted.
13
+ def logout_with_remembrance!(options = {})
14
+ logout_without_remembrance!
15
+ if options[:forget]
16
+ cookies.delete(:remembrance_token)
17
+ end
18
+ end
19
+
20
+ def authenticate_current_user_with_remembrance
21
+ authenticate_current_user_without_remembrance
22
+ if @current_user == false
23
+ authenticate_with_remembrance_token
24
+ end
25
+ end
26
+
27
+ def authenticate_with_remembrance_token
28
+ if token_value = cookies[:remembrance_token]
29
+ token = RemembranceToken.find_by_value(token_value)
30
+ if token
31
+ @current_user = token.authenticatable
32
+ handle_remembrance_token_theft(token) if token.theft?
33
+ token.regenerate
34
+ token.save
35
+ set_remembrance_token_cookie(token)
36
+ end
37
+ end
38
+ end
39
+
40
+ def handle_remembrance_token_theft(token)
41
+ flash[:error] = Auth.remember_me.token_theft_message
42
+ token.authenticatable.remembrance_tokens.destroy_all
43
+ end
44
+
45
+ def set_remembrance_token_cookie(token)
46
+ cookies[:remembrance_token] = {
47
+ :value => token.value,
48
+ :expires => Auth.remember_me.duration.from_now
49
+ }
50
+ end
51
+
52
+ def self.included(base)
53
+ base.class_eval do
54
+ hide_action :login_with_remembrance!, :login_without_remembrance!, :authenticate_current_user_without_remembrance,
55
+ :authenticate_current_user_with_remembrance, :authenticate_with_remembrance_token,
56
+ :set_remembrance_token_cookie, :handle_remembrance_token_theft, :logout_with_remembrance!,
57
+ :logout_without_remembrance!
58
+
59
+ unless method_defined?(:login_without_remembrance!)
60
+ alias_method_chain :login!, :remembrance
61
+ alias_method_chain :logout!, :remembrance
62
+ alias_method_chain :authenticate_current_user, :remembrance
63
+ end
64
+ end
65
+ end
66
+ end