sparkly-auth 1.0.0

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