From 35f14bafffbb64936e0c05dd7628872816248635 Mon Sep 17 00:00:00 2001 From: binarylogic Date: Mon, 27 Oct 2008 17:43:12 -0400 Subject: [PATCH] Released v0.10.0 --- CHANGELOG.rdoc | 8 + Manifest | 6 +- README.rdoc | 28 +- init.rb | 1 - lib/authgasm.rb | 5 +- lib/authgasm/acts_as_authentic.rb | 38 ++- lib/authgasm/controller.rb | 16 -- .../controller_adapters/abstract_adapter.rb | 25 ++ .../controller_adapters/rails_adapter.rb | 39 +++ .../session/active_record_trickery.rb | 2 +- lib/authgasm/session/base.rb | 239 ++++++++++-------- lib/authgasm/session/callbacks.rb | 40 +-- lib/authgasm/session/config.rb | 10 +- lib/authgasm/session/errors.rb | 6 + lib/authgasm/version.rb | 4 +- .../controllers/user_sessions_controller.rb | 2 +- test_app/db/development.sqlite3 | Bin 7168 -> 7168 bytes test_app/db/test.sqlite3 | Bin 5120 -> 5120 bytes test_app/test/fixtures/users.yml | 4 +- .../user_sessions_controller_test.rb | 27 +- .../integration/user_sesion_stories_test.rb | 85 +++++++ .../test/integration/user_session_test.rb | 158 ++++++++++++ test_app/test/test_helper.rb | 47 +++- test_app/test/unit/ass_test.rb | 8 - 24 files changed, 624 insertions(+), 174 deletions(-) delete mode 100644 lib/authgasm/controller.rb create mode 100644 lib/authgasm/controller_adapters/abstract_adapter.rb create mode 100644 lib/authgasm/controller_adapters/rails_adapter.rb create mode 100644 test_app/test/integration/user_sesion_stories_test.rb create mode 100644 test_app/test/integration/user_session_test.rb delete mode 100644 test_app/test/unit/ass_test.rb diff --git a/CHANGELOG.rdoc b/CHANGELOG.rdoc index 728e5adf..666bf4a5 100644 --- a/CHANGELOG.rdoc +++ b/CHANGELOG.rdoc @@ -1,3 +1,11 @@ +== 0.10.0 released 2008-10-24 + +* Do not allow instantiation if the session has not been activated with a controller object. Just like ActiveRecord won't let you do anything without a DB connection. +* Abstracted controller implementation to allow for rails, merb, etc adapters. So this is not confined to the rails framework. +* Removed create and update methods and added save, like ActiveRecord. +* after_validation should be able to change the result if it adds errors on callbacks. +* Completed tests. + == 0.9.1 released 2008-10-24 * Changed scope to id. Makes more sense to call it an id and fits better with the ActiveRecord model. diff --git a/Manifest b/Manifest index 7f1f7fff..c57c9881 100644 --- a/Manifest +++ b/Manifest @@ -1,7 +1,8 @@ CHANGELOG.rdoc init.rb lib/authgasm/acts_as_authentic.rb -lib/authgasm/controller.rb +lib/authgasm/controller_adapters/abstract_adapter.rb +lib/authgasm/controller_adapters/rails_adapter.rb lib/authgasm/session/active_record_trickery.rb lib/authgasm/session/base.rb lib/authgasm/session/callbacks.rb @@ -77,6 +78,7 @@ test_app/script/server test_app/test/fixtures/users.yml test_app/test/functional/user_sessions_controller_test.rb test_app/test/functional/users_controller_test.rb +test_app/test/integration/user_sesion_stories_test.rb +test_app/test/integration/user_session_test.rb test_app/test/test_helper.rb -test_app/test/unit/ass_test.rb test_app/test/unit/user_test.rb diff --git a/README.rdoc b/README.rdoc index ca7fb891..65d6680f 100644 --- a/README.rdoc +++ b/README.rdoc @@ -19,7 +19,7 @@ What if your user sessions controller could look just like your other controller def create @user_session = UserSession.new(params[:user_session]) - if @user_session.create + if @user_session.save redirect_to account_url else render :action => :new @@ -134,6 +134,8 @@ Authgasm tries to check the state of the record before creating the session. If What's neat about this is that these are checked upon any type of login. When logging in explicitly, by cookie, session, or basic http auth. So if you mark a user inactive in the middle of their session they wont be logged back in next time they refresh the page. Giving you complete control. +Need Authgasm to check your own "state"? No problem, check out the hooks section below. Add in a before_validation or after_validation to do your own checking. + == Hooks / Callbacks Just like ActiveRecord you can create your own hooks / callbacks so that you can do whatever you want when certain actions are performed. Here they are: @@ -142,11 +144,27 @@ Just like ActiveRecord you can create your own hooks / callbacks so that you can after_create before_destroy after_destroy + before_save + after_save before_update after_update before_validation after_validation +== Errors + +The errors in Authgasm work JUST LIKE ActiveRecord. In fact, it uses the exact same ActiveRecord errors class. Use it the same way: + + class UserSession + before_validation :check_if_awesome + + private + def check_if_awesome + errors.add(:login, "must contain awesome") if login && !login.include?("awesome") + errors.add_to_base("You must be awesome to log in") unless record.awesome? + end + end + == Automatic Session Updating This is one of my favorite features that I think its pretty cool. It's things like this that make a library great and let you know you are on the right track. @@ -183,7 +201,7 @@ When things come together like this I think its a sign that you are doing someth You're asking: "why would I want multiple sessions?". Take this example: -You have an app where users login and then need to re-login to view / change their billing information. Similar to how Apples' me.com works, if you've ever used it. What you could do is have the user login with their normal session, then have an entirely new session that represents their "secure" session. But wait, this is 2 users sessions. No problem: +You have an app where users login and then need to re-login to view / change their billing information. Similar to how Apple's me.com works. What you could do is have the user login with their normal session, then have an entirely new session that represents their "secure" session. But wait, this is 2 users sessions. No problem: # regular user session @user_session = UserSession.new @@ -202,9 +220,13 @@ This will keep everything separate. The :secure session will store its info in a For more information on ids checkout Authgasm::Session::Base#initialize +== What about [insert framework here]? + +As of now, authgasm supports rails right out of the box. But I designed authgasm to be framework agnostic. The only thing stopping Authgasm from being implemented in merb, or any other framework, is a simple adapter. I have not had the opportunity to use Authgasm in anything other than rails. If you want to use this in merb or any other framework take a look at authgasm/controller/rails_adapter.rb. + == How it works -Interested in how all of this all works? Basically a before_filter is automatically set in your controller which lets Authgasm know about the current controller object. This allows Authgasm to set sessions, cookies, login via basic http auth, etc. If you are using rails in a multiple thread environment, don't worry. I kept that in mind and made this is thread safe. +Interested in how all of this all works? Basically a before_filter is automatically set in your controller which lets Authgasm know about the current controller object. This allows Authgasm to set sessions, cookies, login via basic http auth, etc. If you are using rails in a multiple thread environment, don't worry. I kept that in mind and made this thread safe. From there it is pretty simple. When you try to create a new session the record is authenticated and then all of the session / cookie magic is done for you. The sky is the limit. diff --git a/init.rb b/init.rb index e1bb3096..05d07f86 100644 --- a/init.rb +++ b/init.rb @@ -1,2 +1 @@ -require "digest/sha2" require "authgasm" \ No newline at end of file diff --git a/lib/authgasm.rb b/lib/authgasm.rb index 52e01242..5d018b61 100644 --- a/lib/authgasm.rb +++ b/lib/authgasm.rb @@ -1,5 +1,8 @@ +require "digest/sha2" require File.dirname(__FILE__) + "/authgasm/version" -require File.dirname(__FILE__) + "/authgasm/controller" + +require File.dirname(__FILE__) + "/authgasm/controller_adapters/rails_adapter" if defined?(Rails) + require File.dirname(__FILE__) + "/authgasm/sha256_crypto_provider" require File.dirname(__FILE__) + "/authgasm/acts_as_authentic" require File.dirname(__FILE__) + "/authgasm/session/active_record_trickery" diff --git a/lib/authgasm/acts_as_authentic.rb b/lib/authgasm/acts_as_authentic.rb index 98620322..6536a3a6 100644 --- a/lib/authgasm/acts_as_authentic.rb +++ b/lib/authgasm/acts_as_authentic.rb @@ -1,5 +1,5 @@ module Authgasm - module ActsAsAuthenticated # :nodoc: + module ActsAsAuthentic # :nodoc: def self.included(base) base.extend(ClassMethods) end @@ -20,6 +20,7 @@ module ClassMethods # Class method name Description # User.unique_token returns unique token generated by your :crypto_provider # User.crypto_provider The class that you set in your :crypto_provider option + # User.forget_all! Resets all records so they will not be remembered on their next visit. Basically makes their cookies invalid # # Named Scopes # User.logged_in Find all users who are logged in, based on your :logged_in_timeout option @@ -31,6 +32,7 @@ module ClassMethods # user.valid_password?(pass) Based on the valid of :password_field. Determines if the password passed is valid. The password could be encrypted or raw. # user.randomize_password! Basically resets the password to a random password using only letters and numbers # user.logged_in? Based on the :logged_in_timeout option. Tells you if the user is logged in or not + # user.forget! Changes their remember token, making their cookie invalid. # # === Options # * session_class: default: "#{name}Session", the related session class. Used so that you don't have to repeat yourself here. A lot of the configuration will be based off of the configuration values of this class. @@ -107,6 +109,17 @@ def self.unique_token def self.crypto_provider #{options[:crypto_provider]} end + + def self.forget_all! + # Paginate these to save on memory + records = nil + i = 0 + begin + records = find(:all, :limit => 50, :offset => i) + records.each { |record| records.update_attribute(:#{options[:remember_token_field]}, unique_token) } + i += 50 + end while !records.blank? + end end_eval # Instance methods @@ -125,12 +138,13 @@ def #{options[:password_field]}=(pass) return if pass.blank? self.tried_to_set_#{options[:password_field]} = true @#{options[:password_field]} = pass - salt = [Array.new(6) {rand(256).chr}.join].pack("m").chomp self.#{options[:remember_token_field]} = self.class.unique_token - self.#{options[:password_salt_field]}, self.#{options[:crypted_password_field]} = salt, crypto_provider.encrypt(@#{options[:password_field]} + salt) + self.#{options[:password_salt_field]} = self.class.unique_token + self.#{options[:crypted_password_field]} = crypto_provider.encrypt(@#{options[:password_field]} + #{options[:password_salt_field]}) end def valid_#{options[:password_field]}?(attempted_password) + return false if attempted_password.blank? attempted_password == #{options[:crypted_password_field]} || #{options[:crypted_password_field]} == crypto_provider.encrypt(attempted_password + #{options[:password_salt_field]}) end end_eval @@ -145,6 +159,7 @@ def #{options[:password_field]}=(pass) end def valid_#{options[:password_field]}?(attemtped_password) + return false if attempted_password.blank? attempted_password == #{options[:crypted_password_field]} || #{options[:crypted_password_field]} = crypto_provider.decrypt(attempted_password) end end_eval @@ -158,6 +173,10 @@ def crypto_provider self.class.crypto_provider end + def forget! + update_attribute(:#{options[:remember_token_field]}, self.class.unique_token) + end + def randomize_#{options[:password_field]}! chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a newpass = "" @@ -166,6 +185,13 @@ def randomize_#{options[:password_field]}! self.confirm_#{options[:password_field]} = newpass end + def save_from_session(*args) + @saving_from_session = true + result = save(*args) + @saving_from_session = false + result + end + protected def create_sessions! return if !#{options[:session_class]}.activated? || #{options[:session_ids].inspect}.blank? @@ -183,7 +209,7 @@ def create_sessions! end def update_sessions! - return if !#{options[:session_class]}.activated? + return if @saving_from_session || !#{options[:session_class]}.activated? #{options[:session_ids].inspect}.each do |session_id| session = #{options[:session_class]}.find(*[session_id].compact) @@ -192,7 +218,7 @@ def update_sessions! next if !session || session.record != self # We know we are logged in and this is our record, update the session - session.update + session.save end end @@ -215,4 +241,4 @@ def validate_password end end -ActiveRecord::Base.send(:include, Authgasm::ActsAsAuthenticated) \ No newline at end of file +ActiveRecord::Base.send(:include, Authgasm::ActsAsAuthentic) \ No newline at end of file diff --git a/lib/authgasm/controller.rb b/lib/authgasm/controller.rb deleted file mode 100644 index 60620b95..00000000 --- a/lib/authgasm/controller.rb +++ /dev/null @@ -1,16 +0,0 @@ -module Authgasm - # = Controller - # Adds a before_filter to set the controller object so that Authgasm can do its session and cookie magic - module Controller - def self.included(klass) # :nodoc: - klass.prepend_before_filter :set_controller - end - - private - def set_controller - Authgasm::Session::Base.controller = self - end - end -end - -ActionController::Base.send(:include, Authgasm::Controller) \ No newline at end of file diff --git a/lib/authgasm/controller_adapters/abstract_adapter.rb b/lib/authgasm/controller_adapters/abstract_adapter.rb new file mode 100644 index 00000000..4284f1ef --- /dev/null +++ b/lib/authgasm/controller_adapters/abstract_adapter.rb @@ -0,0 +1,25 @@ +module Authgasm + module ControllerAdapters # :nodoc: + # = Abstract Adapter + # Allows you to use Authgasm in any framework you want, not just rails. See tha RailsAdapter for an example of how to adapter Authgasm to work with your framework. + class AbstractAdapter + attr_accessor :controller + + def initialize(controller) + self.controller = controller + end + + def authenticate_with_http_basic(*args, &block) + end + + def cookies + end + + def request + end + + def session + end + end + end +end \ No newline at end of file diff --git a/lib/authgasm/controller_adapters/rails_adapter.rb b/lib/authgasm/controller_adapters/rails_adapter.rb new file mode 100644 index 00000000..92add8f0 --- /dev/null +++ b/lib/authgasm/controller_adapters/rails_adapter.rb @@ -0,0 +1,39 @@ +module Authgasm + module ControllerAdapters + # = Rails Adapter + # Adapts authgasm to work with rails. The point is to close the gap between what authgasm expects and what the rails controller object + # provides. Similar to how ActiveRecord has an adapter for MySQL, PostgreSQL, SQLite, etc. + class RailsAdapter < AbstractAdapter + def authenticate_with_http_basic(*args, &block) + controller.authenticate_with_http_basic(*args, &block) + end + + def cookies + controller.send(:cookies) + end + + def request + controller.request + end + + def session + controller.session + end + end + + # = Rails Implementation + # Lets Authgasm know about the controller object, AKA "activates" authgasm. + module RailsImplementation + def self.included(klass) # :nodoc: + klass.prepend_before_filter :set_controller + end + + private + def set_controller + Authgasm::Session::Base.controller = RailsAdapter.new(self) + end + end + end +end + +ActionController::Base.send(:include, Authgasm::ControllerAdapters::RailsImplementation) \ No newline at end of file diff --git a/lib/authgasm/session/active_record_trickery.rb b/lib/authgasm/session/active_record_trickery.rb index dd5b5429..4c97ace0 100644 --- a/lib/authgasm/session/active_record_trickery.rb +++ b/lib/authgasm/session/active_record_trickery.rb @@ -18,7 +18,7 @@ def human_attribute_name(attribute_key_name, options = {}) module InstanceMethods # :nodoc: def new_record? - true + new_session? end end end diff --git a/lib/authgasm/session/base.rb b/lib/authgasm/session/base.rb index e88ff4d2..c970a35d 100644 --- a/lib/authgasm/session/base.rb +++ b/lib/authgasm/session/base.rb @@ -7,7 +7,9 @@ class Base include Config class << self - # Returns true if a controller have been set and can be used properly. + # Returns true if a controller have been set and can be used properly. This MUST be set before anything can be done. Similar to how ActiveRecord won't allow you to do anything + # without establishing a DB connection. By default this is done for you automatically, but if you are using Authgasm in a unique way outside of rails, you need to assign a controller + # object to Authgasm via Authgasm::Session::Base.controller = obj. def activated? !controller.blank? end @@ -26,13 +28,13 @@ def controller # :nodoc: # session.create def create(*args) session = new(*args) - session.create + session.save end # Same as create but calls create!, which raises an exception when authentication fails def create!(*args) session = new(*args) - session.create! + session.save! end # Finds your session by session, then cookie, and finally basic http auth. Perfect for that global before_filter to find your logged in user: @@ -49,9 +51,13 @@ def find(id = nil) args = [id].compact session = new(*args) find_with.each do |find_method| - args = [] - args << true unless find_method == :session - return session if session.send("valid_#{find_method}?", *args) + if session.send("valid_#{find_method}?") + if session.record.class.column_names.include?("last_request_at") + session.record.last_request_at = Time.now + session.record.save_from_session(false) + end + return session + end end nil end @@ -72,29 +78,15 @@ def klass_name # :nodoc: end end - # Convenience method. The same as: - # - # session = UserSession.new - # session.update - def update(*args) - session = new(*args) - session.update - end - - # The same as update but calls update!, which raises an exception when authentication fails - def update!(*args) - session = new(*args) - session.update! - end - private def controllers @@controllers ||= {} end end - attr_accessor :login_with, :remember_me, :id + attr_accessor :login_with, :new_session, :remember_me attr_reader :record, :unauthorized_record + attr_writer :id # You can initialize a session by doing any of the following: # @@ -111,6 +103,8 @@ def controllers # Ids are rarely used, but they can be useful. For example, what if users allow other users to login into their account via proxy? Now that user can "technically" be logged into 2 accounts at once. # To solve this just pass a id called :proxy, or whatever you want. Authgasm will separate everything out. def initialize(*args) + raise NotActivated.new(self) unless self.class.activated? + create_configurable_methods! self.id = args.pop if args.last.is_a?(Symbol) @@ -131,45 +125,6 @@ def initialize(*args) end end - # Creates a new user session for you. It does all of the magic: - # - # 1. validates - # 2. sets session - # 3. sets cookie - # 4. updates magic fields - def create(updating = false) - if valid?(true) - cookies[cookie_key] = { - :value => record.send(remember_token_field), - :expires => remember_me? ? remember_me_for.from_now : nil - } - - if !updating - record.login_count = record.login_count + 1 if record.respond_to?(:login_count) - - if record.respond_to?(:current_login_at) - record.last_login_at = record.current_login_at if record.respond_to?(:last_login_at) - record.current_login_at = Time.now - end - - if record.respond_to?(:current_login_ip) - record.last_login_ip = record.current_login_ip if record.respond_to?(:last_login_ip) - record.current_login_ip = controller.request.remote_ip - end - - record.save(false) - end - - self - end - end - - # Same as create but raises an exception when authentication fails - def create!(updating = false) - raise SessionInvalid.new(self) unless create(updating) - end - alias_method :start!, :create! - # Your login credentials in hash format. Usually {:login => "my login", :password => ""} depending on your configuration. # Password is protected as a security measure. The raw password should never be publicly accessible. def credentials @@ -178,8 +133,8 @@ def credentials # Lets you set your loging and password via a hash format. def credentials=(values) - values.symbolize_keys! - raise(ArgumentError, "Only 2 credentials are allowed: #{login_field} and #{password_field}") if !values.is_a?(Hash) || values.keys.size > 2 || !values.key?(login_field) || !values.key?(password_field) + return if values.blank? || !values.is_a?(Hash) + raise(ArgumentError, "Only 2 credentials are allowed: #{login_field} and #{password_field}") if (values.keys - [login_field.to_sym, login_field.to_s, password_field.to_sym, password_field.to_s]).size > 0 values.each { |field, value| send("#{field}=", value) } end @@ -187,21 +142,49 @@ def credentials=(values) def destroy errors.clear @record = nil - cookies.delete cookie_key - session[session_key] = nil + controller.cookies.delete cookie_key + controller.session[session_key] = nil true end - # Errors when authentication fails, just like ActiveRecord errors. In fact it uses the same exact class. + # The errors in Authgasm work JUST LIKE ActiveRecord. In fact, it uses the exact same ActiveRecord errors class. Use it the same way: + # + # === Example + # + # class UserSession + # before_validation :check_if_awesome + # + # private + # def check_if_awesome + # errors.add(:login, "must contain awesome") if login && !login.include?("awesome") + # errors.add_to_base("You must be awesome to log in") unless record.awesome? + # end + # end def errors @errors ||= Errors.new(self) end + # Allows you to set a unique identifier for your session, so that you can have more than 1 session at a time. A good example when this might be needed is when you want to have a normal user session + # and a "secure" user session. The secure user session would be created only when they want to modify their billing information, or other sensative information. Similar to me.com. This requires 2 + # user sessions. Just use an id for the "secure" session and you should be good. + # + # You can set the id a number of ways: + # + # session = Session.new(:secure) + # session = Session.new("username", "password", :secure) + # session = Session.new({:username => "username", :password => "password"}, :secure) + # session.id = :secure + # + # Just be sure and set your id before you validate / create / update your session. + def id + @id + end + def inspect # :nodoc: details = {} case login_with when :unauthorized_record - details[:unauthorized_record] = unauthorized_record + details[:unauthorized_record] = "" else details[login_field.to_sym] = send(login_field) details[password_field.to_sym] = "" @@ -209,16 +192,62 @@ def inspect # :nodoc: "#<#{self.class.name} #{details.inspect}>" end + + def new_session? + new_session != false + end + # Allows users to be remembered via a cookie. def remember_me? - remember_me == true || remember_me = "true" || remember_me == "1" + remember_me == true || remember_me == "true" || remember_me == "1" end # When to expire the cookie. See remember_me_for configuration option to change this. def remember_me_until + return unless remember_me? remember_me_for.from_now end + # Creates / updates a new user session for you. It does all of the magic: + # + # 1. validates + # 2. sets session + # 3. sets cookie + # 4. updates magic fields + def save + if valid? + update_session! + controller.cookies[cookie_key] = { + :value => record.send(remember_token_field), + :expires => remember_me_until + } + + record.login_count = record.login_count + 1 if record.respond_to?(:login_count) + + if record.respond_to?(:current_login_at) + record.last_login_at = record.current_login_at if record.respond_to?(:last_login_at) + record.current_login_at = Time.now + end + + if record.respond_to?(:current_login_ip) + record.last_login_ip = record.current_login_ip if record.respond_to?(:last_login_ip) + record.current_login_ip = controller.request.remote_ip + end + + record.save_from_session(false) + + self.new_session = false + self + end + end + + # Same as save but raises an exception when authentication fails + def save! + result = save + raise SessionInvalid.new(self) unless result + result + end + # Sometimes you don't want to create a session via credentials (login and password). Maybe you already have the record. Just set this record to this and it will be authenticated when you try to validate # the session. Basically this is another form of credentials, you are just skipping username and password validation. def unauthorized_record=(value) @@ -226,21 +255,12 @@ def unauthorized_record=(value) @unauthorized_record = value end - # Updates the session with any new information. Resets the session and cookie. - def update - create(true) - end - - # Same as update but raises an exception if validation is failed - def update! - create!(true) - end - - def valid?(set_session = false) + def valid? errors.clear temp_record = unauthorized_record - if login_with == :credentials + case login_with + when :credentials errors.add(login_field, "can not be blank") if login.blank? errors.add(password_field, "can not be blank") if protected_password.blank? return false if errors.count > 0 @@ -256,6 +276,19 @@ def valid?(set_session = false) errors.add(password_field, "is invalid") return false end + when :unauthorized_record + if temp_record.blank? + errors.add_to_base("You can not log in with a blank record.") + return false + end + + if temp_record.new_record? + errors.add_to_base("You can not login with a new record.") if temp_record.new_record? + return false + end + else + errors.add_to_base("You must provide some form of credentials before logging in.") + return false end [:approved, :confirmed, :inactive].each do |required_status| @@ -268,34 +301,34 @@ def valid?(set_session = false) # All is good, lets set the record @record = temp_record - # Now lets set the session to make things easier on successive requests. This is nice when logging in from a cookie, the next requests will be right from the session, which is quicker. - if set_session - session[session_key] = record.id - if record.class.column_names.include?("last_request_at") - record.last_request_at = Time.now - record.save(false) - end - end - true end - def valid_http_auth?(set_session = false) + def valid_http_auth? controller.authenticate_with_http_basic do |login, password| if !login.blank? && !password.blank? send("#{login_method}=", login) send("#{password_method}=", password) - return valid?(set_session) + result = valid? + if result + update_session! + return result + end end end false end - def valid_cookie?(set_session = false) + def valid_cookie? if cookie_credentials self.unauthorized_record = klass.send("find_by_#{remember_token_field}", cookie_credentials) - valid?(set_session) + result = valid? + if result + update_session! + self.new_session = false + return result + end end false @@ -304,7 +337,11 @@ def valid_cookie?(set_session = false) def valid_session? if session_credentials self.unauthorized_record = klass.find_by_id(session_credentials) - return valid? + result = valid? + if result + self.new_session = false + return result + end end false @@ -315,12 +352,8 @@ def controller self.class.controller end - def cookies - controller.send(:cookies) - end - def cookie_credentials - cookies[cookie_key] + controller.cookies[cookie_key] end def create_configurable_methods! @@ -356,12 +389,12 @@ def protected_password @password end - def session - controller.session + def session_credentials + controller.session[session_key] end - def session_credentials - session[session_key] + def update_session! + controller.session[session_key] = record && record.id end end end diff --git a/lib/authgasm/session/callbacks.rb b/lib/authgasm/session/callbacks.rb index be58b7f1..329dcf4b 100644 --- a/lib/authgasm/session/callbacks.rb +++ b/lib/authgasm/session/callbacks.rb @@ -4,23 +4,16 @@ module Session # # Just like in ActiveRecord you have before_save, before_validation, etc. You have similar callbacks with Authgasm, see all callbacks below. module Callbacks - CALLBACKS = %w(before_create after_create before_destroy after_destroy before_update after_update before_validation after_validation) + CALLBACKS = %w(before_create after_create before_destroy after_destroy before_save after_save before_update after_update before_validation after_validation) def self.included(base) #:nodoc: - [:create, :destroy, :update, :valid?].each do |method| + [:destroy, :save, :valid?].each do |method| base.send :alias_method_chain, method, :callbacks end base.send :include, ActiveSupport::Callbacks base.define_callbacks *CALLBACKS end - - def create_with_callbacks(updating = false) # :nodoc: - run_callbacks(:before_create) - result = create_without_callbacks(updating) - run_callbacks(:after_create) if result - result - end def destroy_with_callbacks # :nodoc: run_callbacks(:before_destroy) @@ -29,17 +22,32 @@ def destroy_with_callbacks # :nodoc: result end - def update_with_callbacks # :nodoc: - run_callbacks(:before_update) - result = update_without_callbacks - run_callbacks(:after_update) if result + def save_with_callbacks # :nodoc: + if new_session? + run_callbacks(:before_create) + else + run_callbacks(:before_update) + end + run_callbacks(:before_save) + result = save_without_callbacks + if result + if new_session? + run_callbacks(:after_create) + else + run_callbacks(:after_update) + end + run_callbacks(:after_save) + end result end - def valid_with_callbacks?(set_session = false) # :nodoc: + def valid_with_callbacks? # :nodoc: run_callbacks(:before_validation) - result = valid_without_callbacks?(set_session) - run_callbacks(:after_validation) if result + result = valid_without_callbacks? + if result + run_callbacks(:after_validation) + result = errors.empty? + end result end end diff --git a/lib/authgasm/session/config.rb b/lib/authgasm/session/config.rb index ed55fa8f..cae225fc 100644 --- a/lib/authgasm/session/config.rb +++ b/lib/authgasm/session/config.rb @@ -22,15 +22,6 @@ def self.included(klass) # # ... more configuration # end # - # or... - # - # class UserSession < Authgasm::Session::Base - # configure do |config| - # config.authenticate_with = User - # # ... more configuration - # end - # end - # # See the methods belows for all configuration options. module ClassMethods # Lets you change which model to use for authentication. @@ -181,6 +172,7 @@ def password_field end def remember_me_for + return unless remember_me? self.class.remember_me_for end diff --git a/lib/authgasm/session/errors.rb b/lib/authgasm/session/errors.rb index 0c5f7efc..06d83e6f 100644 --- a/lib/authgasm/session/errors.rb +++ b/lib/authgasm/session/errors.rb @@ -3,6 +3,12 @@ module Session class Errors < ::ActiveRecord::Errors # :nodoc: end + class NotActivated < ::StandardError # :nodoc: + def initialize(session) + super("You must activate the Authgasm::Session::Base.controller with a controller object before creating objects") + end + end + class SessionInvalid < ::StandardError # :nodoc: def initialize(session) super("Authentication failed: #{session.errors.full_messages.to_sentence}") diff --git a/lib/authgasm/version.rb b/lib/authgasm/version.rb index 3561b91f..ea4ce970 100644 --- a/lib/authgasm/version.rb +++ b/lib/authgasm/version.rb @@ -43,8 +43,8 @@ def to_a end MAJOR = 0 - MINOR = 9 - TINY = 1 + MINOR = 10 + TINY = 0 # The current version as a Version instance CURRENT = new(MAJOR, MINOR, TINY) diff --git a/test_app/app/controllers/user_sessions_controller.rb b/test_app/app/controllers/user_sessions_controller.rb index 7a80dd58..6379477b 100644 --- a/test_app/app/controllers/user_sessions_controller.rb +++ b/test_app/app/controllers/user_sessions_controller.rb @@ -9,7 +9,7 @@ def new def create @user_session = UserSession.new(params[:user_session]) - if @user_session.create + if @user_session.save flash[:notice] = "Login successful!" redirect_back_or_default(account_url) else diff --git a/test_app/db/development.sqlite3 b/test_app/db/development.sqlite3 index 0d75c83da7e6fa6bbbcb1289c6659d8645f718b7..a27c58de4b276269f9a3241fee766efa8f42711d 100644 GIT binary patch delta 255 zcmWlTF-}843^7XiW{dxMky}{KLPk6e&exBy#Fl|>v>xX4t=I@W~?w~tNd&Yg{&AWeQ?bsq9 za_gl54xwO)z}{(uv~D?_RW~;rg`lguaaO7x1xz%WjxZIdgawKZsYWt@K#0OsQ>CQN z?#iN7-!hwoVSqG^l#5#@sX1}Z;n5wEboOD9#)U?7GlT}ps+S*lUJkD3-cw4am%HE1 EKM~nQt^fc4 delta 198 zcmV~$Jx+o_5CBm4FgAk5P*|u57a;8H?ELK4*cgvsOLk{=3n>YSop1vaPhvOSqSm){;_%8U?YaqsWU?>UK=(5@lsV| egq$p96H)IEBpEY?9*nqWQ>$*x^t`^i%>Mzmm^8=$ diff --git a/test_app/db/test.sqlite3 b/test_app/db/test.sqlite3 index 9bb3a9cfff79f608a7c5c0415d27a41326ced161..7be817fd134717c25d8a82f8fcc9c41d43407414 100644 GIT binary patch delta 377 zcmZvXyGlbr6h$*PN>DT)7Ac}|u}Tw|IrFv?qCp7JLTj0sGZSCAff#FzU}cjK@JG`6 z6XI70)~UVrvb(ilZ}xNRxixE&+I+9S*{x*nS%-X*?tJ>SoRM>~F(9=@qd{KVi|_31 z;3|wBi^)NLQ%vF@j`Pm($>}@9_Iiu^s;iIVFb;hjUrk1Lu~>XmPuH_m zC0i|b)_$_5ml~;8I4Aql?Z50cfl||Ev<*53b|}~(gMU!P-;S<}hf#rGK^bKLtgs?b znJ7bpsGLY%`y?=UL`a@02MRD^VzTtwY8#|`a;o%Na$e3FrZFWgjW$-KWSntc1nJAl z(g>%h#S&rC%t8RqV*mv180w%@5>k04ypX_{b4scZjdz+#NLYRd2uQV)fdkhTEH?>U L#>8?}+uZsEkaTGc delta 198 zcmZqBXwaA-&B!`Y#*b?P1Jiw`UM4BV37Z93WEeNQGX^kn$}zHwi;FY1NKanC)IB+r zIdbw@W(!v4Y6j-&&4L_-%=L{-j9d)H# + password_salt: <%= salt = User.unique_token %> + crypted_password: <%= Authgasm::Sha256CryptoProvider.encrypt("benrocks" + salt) %> + remember_token: 23a1d7c66f456b14b45211aa656ce8ba7052fd220cd2d07a5c323792938f2a14 first_name: Ben last_name: Johnson diff --git a/test_app/test/functional/user_sessions_controller_test.rb b/test_app/test/functional/user_sessions_controller_test.rb index f68adce7..455fd3ed 100644 --- a/test_app/test/functional/user_sessions_controller_test.rb +++ b/test_app/test/functional/user_sessions_controller_test.rb @@ -6,10 +6,31 @@ def setup @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new end - - def test_truth + + def test_new + get :new + assert @controller.instance_variable_get(:@user_session).is_a?(UserSession) + end + + def test_successful_create get :create, {:user_session => {:login => "bjohnson", :password => "benrocks"}} assert_equal 1, session[:user_id] - assert_equal ["YmpvaG5zb24=\n:::2e8884187c71ff39af9ac05ebcaa0f40ab2432de51035aff8b0f491f890314d0"], cookies["user_credentials"] + assert_equal ["23a1d7c66f456b14b45211aa656ce8ba7052fd220cd2d07a5c323792938f2a14"], cookies["user_credentials"] + assert_redirected_to account_url + end + + def test_unsuccessful_create + get :create, {:user_session => {:login => "bjohnson", :password => "badpassword"}} + assert_equal nil, session[:user_id] + assert_equal nil, cookies["user_credentials"] + assert_template "new" + end + + def test_destroy + get :destroy + assert_equal nil, session[:user_id] + assert_equal nil, cookies["user_credentials"] + assert_redirected_to new_user_session_url + assert flash.key?(:notice) end end diff --git a/test_app/test/integration/user_sesion_stories_test.rb b/test_app/test/integration/user_sesion_stories_test.rb new file mode 100644 index 00000000..d2ec7351 --- /dev/null +++ b/test_app/test/integration/user_sesion_stories_test.rb @@ -0,0 +1,85 @@ +require 'test_helper' + +class UserSessionStoriesTest < ActionController::IntegrationTest + def test_registration + # Try to access the account area without being logged in + get account_url + assert_redirected_to new_user_session_url + follow_redirect! + assert flash.key?(:notice) + assert_template "user_sessions/new" + + # Try to register with no info + post users_url + assert_template "users/new" + + # Register successfully + post users_url, {:user => {:login => "binarylogic", :password => "pass", :confirm_password => "pass", :first_name => "Ben", :last_name => "Johnson"}} + assert_redirected_to account_url + assert flash.key?(:notice) + + access_account(User.find(2)) + end + + def test_login_process + # Try to access the account area without being logged in + get account_url + assert_redirected_to new_user_session_url + follow_redirect! + assert flash.key?(:notice) + assert_template "user_sessions/new" + + login_unsuccessfully + login_unsuccessfully("bjohnson", "badpassword") + login_successfully("bjohnson", "benrocks") + + # Try to log in again after a successful login + get new_user_session_url + assert_redirected_to account_url + follow_redirect! + assert flash.key?(:notice) + assert_template "users/show" + + # Try to register after a successful login + get new_user_url + assert_redirected_to account_url + follow_redirect! + assert flash.key?(:notice) + assert_template "users/show" + + access_account + logout(new_user_url) # before I tried to register, it stored my location + + # Try to access my account again + get account_url + assert_redirected_to new_user_session_url + assert flash.key?(:notice) + end + + def test_changing_password + # Try logging in with correct credentials + login_successfully("bjohnson", "benrocks") + + # Go to edit form + get edit_account_path + assert_template "users/edit" + + # Edit password + put account_path, :user => {:login => "bjohnson", :password => "sillywilly", :confirm_password => "sillywilly", :first_name => "Ben", :last_name => "Johnson"} + assert_redirected_to account_url + follow_redirect! + assert flash.key?(:notice) + assert_template "users/show" + + access_account + logout + + # Try to access my account again + get account_url + assert_redirected_to new_user_session_url + assert flash.key?(:notice) + + login_successfully("bjohnson", "sillywilly") + access_account + end +end \ No newline at end of file diff --git a/test_app/test/integration/user_session_test.rb b/test_app/test/integration/user_session_test.rb new file mode 100644 index 00000000..574605b6 --- /dev/null +++ b/test_app/test/integration/user_session_test.rb @@ -0,0 +1,158 @@ +require 'test_helper' + +# I know these tests are not really integration tests, but since UserSessions deals with cookies, models, etc. It was easiest and best to test it via an integration. +class UserSessionTest < ActionController::IntegrationTest + def test_activated + UserSession.controller = nil + assert !UserSession.activated? + get new_user_session_url # reactive + assert UserSession.activated? + end + + def test_create + assert !UserSession.create("unknown", "bad") + assert UserSession.create("bjohnson", "benrocks") + assert_raise(Authgasm::Session::SessionInvalid) { assert !UserSession.create!("unknown", "bad") } + assert_nothing_raised { UserSession.create!("bjohnson", "benrocks") } + end + + def test_klass + assert_equal User, UserSession.klass + end + + def test_klass_name + assert_equal "User", UserSession.klass_name + end + + def test_find + assert_equal nil, UserSession.find + post user_sessions_url, {:user_session => {:login => "bjohnson", :password => "benrocks"}} + assert UserSession.find + end + + def test_initialize + session = UserSession.new + assert !session.valid? + assert_equal nil, session.login + assert_equal nil, session.unauthorized_record + + session = UserSession.new(:secure) + assert_equal :secure, session.id + assert !session.valid? + assert_equal nil, session.login + assert_equal nil, session.unauthorized_record + + session = UserSession.new("user", "pass") + assert_equal nil, session.id + assert !session.valid? + assert_equal "user", session.login + assert_equal nil, session.unauthorized_record + + session = UserSession.new("user", "pass", :secure) + assert_equal :secure, session.id + assert !session.valid? + assert_equal "user", session.login + assert_equal nil, session.unauthorized_record + + session = UserSession.new(:login => "user", :password => "pass") + assert_equal nil, session.id + assert !session.valid? + assert_equal "user", session.login + assert_equal nil, session.unauthorized_record + + session = UserSession.new({:login => "user", :password => "pass"}, :secure) + assert_equal :secure, session.id + assert !session.valid? + assert_equal "user", session.login + assert_equal nil, session.unauthorized_record + + session = UserSession.new(users(:ben)) + assert_equal nil, session.id + assert session.valid? + assert_equal nil, session.login + assert_equal users(:ben), session.unauthorized_record + + session = UserSession.new(users(:ben), :secure) + assert_equal :secure, session.id + assert session.valid? + assert_equal nil, session.login + assert_equal users(:ben), session.unauthorized_record + end + + def test_credentials + session = UserSession.new + session.credentials = nil + assert_equal({:login => nil, :password => ""}, session.credentials) + + session = UserSession.new + session.credentials = {:login => "ben"} + assert_equal({:login => "ben", :password => ""}, session.credentials) + + session = UserSession.new + assert_raise(ArgumentError) { session.credentials = {:login => "ben", :random_field => "test"} } + + session = UserSession.new + session.credentials = {:login => "ben", :password => "awesome"} + assert_equal({:login => "ben", :password => ""}, session.credentials) + assert_equal "awesome", session.send(:protected_password) + end + + def test_destroy + # tested thoroughly in stories + end + + def test_errors + # don't need to go crazy here since we are using ActiveRecord's error class, which has been thorough tested there + session = UserSession.new + assert !session.valid? + assert session.errors.on(:login) + assert session.errors.on(:password) + end + + def test_id + session = UserSession.new + assert_equal nil, session.id + session.id = :secure + assert_equal :secure, session.id + end + + def test_inspect + session = UserSession.new + assert_equal "#nil, :password=>\"\"}>", session.inspect + + session = UserSession.new("user", "pass") + assert_equal "#\"user\", :password=>\"\"}>", session.inspect + + session = UserSession.new(users(:ben)) + assert_equal "#\"\"}>", session.inspect + end + + def test_new_session + session = UserSession.new + assert session.new_session? + + session.login = "bjohnson" + session.password = "benrocks" + session.save + assert !session.new_session? + + login_successfully("bjohnson", "benrocks") + session = UserSession.find + assert !session.new_session? + end + + def test_remember_me + session = UserSession.new + session.remember_me = true + assert_equal 3.months, session.remember_me_for + assert session.remember_me_until > Time.now + + session.remember_me = false + assert_equal nil, session.remember_me_for + assert_equal nil, session.remember_me_until + end + + def test_save + # tested thoroughly in stories + end +end \ No newline at end of file diff --git a/test_app/test/test_helper.rb b/test_app/test/test_helper.rb index 9f192695..5b8d4132 100644 --- a/test_app/test/test_helper.rb +++ b/test_app/test/test_helper.rb @@ -33,6 +33,51 @@ class Test::Unit::TestCase # Note: You'll currently still have to declare fixtures explicitly in integration tests # -- they do not yet inherit this setting fixtures :all +end - # Add more helper methods to be used by all tests here... +class ActionController::IntegrationTest + def setup + get new_user_session_url # to active authgasm + end + + def teardown + Authgasm::Session::Base.controller = nil + end + + private + def login_successfully(login, password) + post user_sessions_url, :user_session => {:login => login, :password => password} + assert_redirected_to account_url + follow_redirect! + assert_template "users/show" + end + + def login_unsuccessfully(login = nil, password = nil) + params = (login || password) ? {:user_session => {:login => login, :password => password}} : nil + post user_sessions_url, params + assert_template "user_sessions/new" + end + + def access_account(user = nil) + user ||= users(:ben) + # Perform multiple requests to make sure the session is persisting properly, just being anal here + 3.times do + get account_url + assert_equal user.id, session["user_id"] + assert_equal user.remember_token, cookies["user_credentials"] + assert_response :success + assert_template "users/show" + end + end + + def logout(alt_redirect = nil) + redirecting_to = alt_redirect || new_user_session_url + get logout_url + assert_redirected_to redirecting_to # because I tried to access registration above, and it stored it + follow_redirect! + assert flash.key?(:notice) + assert_equal nil, session["user_id"] + assert_equal "", cookies["user_credentials"] + assert_template redirecting_to.gsub("http://www.example.com/", "") + end end diff --git a/test_app/test/unit/ass_test.rb b/test_app/test/unit/ass_test.rb deleted file mode 100644 index 2d04232b..00000000 --- a/test_app/test/unit/ass_test.rb +++ /dev/null @@ -1,8 +0,0 @@ -require 'test_helper' - -class AssTest < ActiveSupport::TestCase - # Replace this with your real tests. - def test_truth - assert true - end -end