diff options
| author | simon <simon@zagal.(none)> | 2009-02-08 23:15:11 +0100 |
|---|---|---|
| committer | hukl <hukl@eight.local> | 2009-02-15 20:22:01 +0100 |
| commit | 9f94a70c3e3d9bf766cb9663b0a904d30a190d85 (patch) | |
| tree | 4b4bbf567ec60a939d024b083b478d72476700a5 | |
| parent | 48ffd4eb446bcaeba7651758ec3002f342702249 (diff) | |
* initial commit of the stripped restful-authentication
* http basic auth and login from cookie have been removed
* no it does not work yet, it's so f*cking secure, it won't even let legitimate users login
| -rw-r--r-- | app/controllers/application_controller.rb | 4 | ||||
| -rw-r--r-- | app/controllers/sessions_controller.rb | 40 | ||||
| -rw-r--r-- | app/helpers/sessions_helper.rb | 2 | ||||
| -rw-r--r-- | app/models/user.rb | 34 | ||||
| -rw-r--r-- | app/views/sessions/new.html.erb | 16 | ||||
| -rw-r--r-- | config/initializers/site_keys.rb | 37 | ||||
| -rw-r--r-- | config/routes.rb | 6 | ||||
| -rw-r--r-- | lib/authenticated_system.rb | 127 | ||||
| -rw-r--r-- | lib/authenticated_test_helper.rb | 6 | ||||
| -rw-r--r-- | lib/authentication.rb | 103 | ||||
| -rw-r--r-- | test/fixtures/users.yml | 27 | ||||
| -rw-r--r-- | test/functional/sessions_controller_test.rb | 82 | ||||
| -rw-r--r-- | test/unit/user_test.rb | 64 |
13 files changed, 531 insertions, 17 deletions
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 6635a3f..b481317 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb | |||
| @@ -2,9 +2,11 @@ | |||
| 2 | # Likewise, all the methods added will be available for all controllers. | 2 | # Likewise, all the methods added will be available for all controllers. |
| 3 | 3 | ||
| 4 | class ApplicationController < ActionController::Base | 4 | class ApplicationController < ActionController::Base |
| 5 | include AuthenticatedSystem | ||
| 6 | |||
| 5 | helper :all # include all helpers, all the time | 7 | helper :all # include all helpers, all the time |
| 6 | protect_from_forgery # See ActionController::RequestForgeryProtection for details | 8 | protect_from_forgery # See ActionController::RequestForgeryProtection for details |
| 7 | 9 | ||
| 8 | # Scrub sensitive parameters from your log | 10 | # Scrub sensitive parameters from your log |
| 9 | # filter_parameter_logging :password | 11 | filter_parameter_logging :password, :password_confirmation |
| 10 | end | 12 | end |
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb new file mode 100644 index 0000000..7c06ac8 --- /dev/null +++ b/app/controllers/sessions_controller.rb | |||
| @@ -0,0 +1,40 @@ | |||
| 1 | # This controller handles the login/logout function of the site. | ||
| 2 | class SessionsController < ApplicationController | ||
| 3 | |||
| 4 | # render new.rhtml | ||
| 5 | def new | ||
| 6 | end | ||
| 7 | |||
| 8 | def create | ||
| 9 | logout_keeping_session! | ||
| 10 | user = User.authenticate(params[:login], params[:password]) | ||
| 11 | if user | ||
| 12 | # Protects against session fixation attacks, causes request forgery | ||
| 13 | # protection if user resubmits an earlier form using back | ||
| 14 | # button. Uncomment if you understand the tradeoffs. | ||
| 15 | reset_session | ||
| 16 | |||
| 17 | self.current_user = user | ||
| 18 | redirect_back_or_default('/') # TODO: insert appropriate path to cms main page | ||
| 19 | flash[:notice] = "Logged in successfully" | ||
| 20 | else | ||
| 21 | note_failed_signin | ||
| 22 | @login = params[:login] | ||
| 23 | @remember_me = params[:remember_me] | ||
| 24 | render :action => 'new' | ||
| 25 | end | ||
| 26 | end | ||
| 27 | |||
| 28 | def destroy | ||
| 29 | logout_killing_session! | ||
| 30 | flash[:notice] = "You have been logged out." | ||
| 31 | redirect_back_or_default('/login') | ||
| 32 | end | ||
| 33 | |||
| 34 | protected | ||
| 35 | # Track failed login attempts | ||
| 36 | def note_failed_signin | ||
| 37 | flash[:error] = "Couldn't log you in as '#{params[:login]}'" | ||
| 38 | logger.warn "Failed login for '#{params[:login]}' from #{request.remote_ip} at #{Time.now.utc}" | ||
| 39 | end | ||
| 40 | end | ||
diff --git a/app/helpers/sessions_helper.rb b/app/helpers/sessions_helper.rb new file mode 100644 index 0000000..f54a8fc --- /dev/null +++ b/app/helpers/sessions_helper.rb | |||
| @@ -0,0 +1,2 @@ | |||
| 1 | module SessionsHelper | ||
| 2 | end \ No newline at end of file | ||
diff --git a/app/models/user.rb b/app/models/user.rb index 4a57cf0..3ac0712 100644 --- a/app/models/user.rb +++ b/app/models/user.rb | |||
| @@ -1,2 +1,36 @@ | |||
| 1 | require 'digest/sha1' | ||
| 2 | |||
| 1 | class User < ActiveRecord::Base | 3 | class User < ActiveRecord::Base |
| 4 | include Authentication | ||
| 5 | include Authentication::ByPassword | ||
| 6 | |||
| 7 | validates_presence_of :login | ||
| 8 | validates_length_of :login, :within => 3..40 | ||
| 9 | validates_uniqueness_of :login | ||
| 10 | validates_format_of :login, :with => Authentication.login_regex, | ||
| 11 | :message => Authentication.bad_login_message | ||
| 12 | |||
| 13 | validates_presence_of :email | ||
| 14 | validates_length_of :email, :within => 6..100 #r@a.wk | ||
| 15 | validates_uniqueness_of :email | ||
| 16 | validates_format_of :email, :with => Authentication.email_regex, | ||
| 17 | :message => Authentication.bad_email_message | ||
| 18 | |||
| 19 | attr_accessible :login, :email, :password, :password_confirmation | ||
| 20 | |||
| 21 | # Authenticates a user by their login name and unencrypted password. Returns the user or nil. | ||
| 22 | def self.authenticate(login, password) | ||
| 23 | return nil if login.blank? || password.blank? | ||
| 24 | u = find_by_login(login) # need to get the salt | ||
| 25 | u && u.authenticated?(password) ? u : nil | ||
| 26 | end | ||
| 27 | |||
| 28 | # TODO: Do we really want to have downcase logins only? | ||
| 29 | def login=(value) | ||
| 30 | write_attribute :login, (value ? value.downcase : nil) | ||
| 31 | end | ||
| 32 | |||
| 33 | def email=(value) | ||
| 34 | write_attribute :email, (value ? value.downcase : nil) | ||
| 35 | end | ||
| 2 | end | 36 | end |
diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb new file mode 100644 index 0000000..84f8469 --- /dev/null +++ b/app/views/sessions/new.html.erb | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | <h1>Log In</h1> | ||
| 2 | |||
| 3 | <% form_tag session_path do -%> | ||
| 4 | <p><%= label_tag 'login' %><br /> | ||
| 5 | <%= text_field_tag 'login', @login %></p> | ||
| 6 | |||
| 7 | <p><%= label_tag 'password' %><br/> | ||
| 8 | <%= password_field_tag 'password', nil %></p> | ||
| 9 | |||
| 10 | <!-- Uncomment this if you want this functionality | ||
| 11 | <p><%= label_tag 'remember_me', 'Remember me' %> | ||
| 12 | <%= check_box_tag 'remember_me', '1', @remember_me %></p> | ||
| 13 | --> | ||
| 14 | |||
| 15 | <p><%= submit_tag 'Log in' %></p> | ||
| 16 | <% end -%> | ||
diff --git a/config/initializers/site_keys.rb b/config/initializers/site_keys.rb new file mode 100644 index 0000000..c66fee2 --- /dev/null +++ b/config/initializers/site_keys.rb | |||
| @@ -0,0 +1,37 @@ | |||
| 1 | # A Site key gives additional protection against a dictionary attack if your | ||
| 2 | # DB is ever compromised. With no site key, we store | ||
| 3 | # DB_password = hash(user_password, DB_user_salt) | ||
| 4 | # If your database were to be compromised you'd be vulnerable to a dictionary | ||
| 5 | # attack on all your stupid users' passwords. With a site key, we store | ||
| 6 | # DB_password = hash(user_password, DB_user_salt, Code_site_key) | ||
| 7 | # That means an attacker needs access to both your site's code *and* its | ||
| 8 | # database to mount an "offline dictionary attack.":http://www.dwheeler.com/secure-programs/Secure-Programs-HOWTO/web-authentication.html | ||
| 9 | # | ||
| 10 | # It's probably of minor importance, but recommended by best practices: 'defense | ||
| 11 | # in depth'. Needless to say, if you upload this to github or the youtubes or | ||
| 12 | # otherwise place it in public view you'll kinda defeat the point. Your users' | ||
| 13 | # passwords are still secure, and the world won't end, but defense_in_depth -= 1. | ||
| 14 | # | ||
| 15 | # Please note: if you change this, all the passwords will be invalidated, so DO | ||
| 16 | # keep it someplace secure. Use the random value given or type in the lyrics to | ||
| 17 | # your favorite Jay-Z song or something; any moderately long, unpredictable text. | ||
| 18 | # TODO: Change the site key when deploying | ||
| 19 | REST_AUTH_SITE_KEY = 'THIS_KEY_SHOULD_BE_CHANGED_UPON_DEPLOYMENT!!!1ELF' | ||
| 20 | |||
| 21 | # Repeated applications of the hash make brute force (even with a compromised | ||
| 22 | # database and site key) harder, and scale with Moore's law. | ||
| 23 | # | ||
| 24 | # bq. "To squeeze the most security out of a limited-entropy password or | ||
| 25 | # passphrase, we can use two techniques [salting and stretching]... that are | ||
| 26 | # so simple and obvious that they should be used in every password system. | ||
| 27 | # There is really no excuse not to use them." http://tinyurl.com/37lb73 | ||
| 28 | # Practical Security (Ferguson & Scheier) p350 | ||
| 29 | # | ||
| 30 | # A modest 10 foldings (the default here) adds 3ms. This makes brute forcing 10 | ||
| 31 | # times harder, while reducing an app that otherwise serves 100 reqs/s to 78 signin | ||
| 32 | # reqs/s, an app that does 10reqs/s to 9.7 reqs/s | ||
| 33 | # | ||
| 34 | # More: | ||
| 35 | # * http://www.owasp.org/index.php/Hashing_Java | ||
| 36 | # * "An Illustrated Guide to Cryptographic Hashes":http://www.unixwiz.net/techtips/iguide-crypto-hashes.html | ||
| 37 | REST_AUTH_DIGEST_STRETCHES = 10 | ||
diff --git a/config/routes.rb b/config/routes.rb index c436b00..2ab68c2 100644 --- a/config/routes.rb +++ b/config/routes.rb | |||
| @@ -1,7 +1,11 @@ | |||
| 1 | ActionController::Routing::Routes.draw do |map| | 1 | ActionController::Routing::Routes.draw do |map| |
| 2 | map.resources :pages | 2 | map.resources :pages |
| 3 | map.resources :nodes | 3 | map.resources :nodes |
| 4 | 4 | ||
| 5 | map.logout '/logout', :controller => 'sessions', :action => 'destroy' | ||
| 6 | map.login '/login', :controller => 'sessions', :action => 'new' | ||
| 7 | map.resources :users | ||
| 8 | map.resource :session | ||
| 5 | 9 | ||
| 6 | map.connect ':language/*page_path', | 10 | map.connect ':language/*page_path', |
| 7 | :controller => 'content', :action => 'render_page', | 11 | :controller => 'content', :action => 'render_page', |
diff --git a/lib/authenticated_system.rb b/lib/authenticated_system.rb new file mode 100644 index 0000000..7b813f4 --- /dev/null +++ b/lib/authenticated_system.rb | |||
| @@ -0,0 +1,127 @@ | |||
| 1 | module AuthenticatedSystem | ||
| 2 | protected | ||
| 3 | # Returns true or false if the user is logged in. | ||
| 4 | # Preloads @current_user with the user model if they're logged in. | ||
| 5 | def logged_in? | ||
| 6 | !!current_user | ||
| 7 | end | ||
| 8 | |||
| 9 | # Accesses the current user from the session. | ||
| 10 | # Future calls avoid the database because nil is not equal to false. | ||
| 11 | def current_user | ||
| 12 | @current_user ||= login_from_session unless @current_user == false | ||
| 13 | end | ||
| 14 | |||
| 15 | # Store the given user id in the session. | ||
| 16 | def current_user=(new_user) | ||
| 17 | session[:user_id] = new_user ? new_user.id : nil | ||
| 18 | @current_user = new_user || false | ||
| 19 | end | ||
| 20 | |||
| 21 | # Check if the user is authorized | ||
| 22 | # | ||
| 23 | # Override this method in your controllers if you want to restrict access | ||
| 24 | # to only a few actions or if you want to check if the user | ||
| 25 | # has the correct rights. | ||
| 26 | # | ||
| 27 | # Example: | ||
| 28 | # | ||
| 29 | # # only allow nonbobs | ||
| 30 | # def authorized? | ||
| 31 | # current_user.login != "bob" | ||
| 32 | # end | ||
| 33 | # | ||
| 34 | def authorized?(action = action_name, resource = nil) | ||
| 35 | logged_in? | ||
| 36 | end | ||
| 37 | |||
| 38 | # Filter method to enforce a login requirement. | ||
| 39 | # | ||
| 40 | # To require logins for all actions, use this in your controllers: | ||
| 41 | # | ||
| 42 | # before_filter :login_required | ||
| 43 | # | ||
| 44 | # To require logins for specific actions, use this in your controllers: | ||
| 45 | # | ||
| 46 | # before_filter :login_required, :only => [ :edit, :update ] | ||
| 47 | # | ||
| 48 | # To skip this in a subclassed controller: | ||
| 49 | # | ||
| 50 | # skip_before_filter :login_required | ||
| 51 | # | ||
| 52 | def login_required | ||
| 53 | authorized? || access_denied | ||
| 54 | end | ||
| 55 | |||
| 56 | # Redirect as appropriate when an access request fails. | ||
| 57 | # | ||
| 58 | # The default action is to redirect to the login screen. | ||
| 59 | # | ||
| 60 | # Override this method in your controllers if you want to have special | ||
| 61 | # behavior in case the user is not authorized | ||
| 62 | # to access the requested action. For example, a popup window might | ||
| 63 | # simply close itself. | ||
| 64 | def access_denied | ||
| 65 | respond_to do |format| | ||
| 66 | format.html do | ||
| 67 | store_location | ||
| 68 | redirect_to new_session_path | ||
| 69 | end | ||
| 70 | end | ||
| 71 | end | ||
| 72 | |||
| 73 | # Store the URI of the current request in the session. | ||
| 74 | # | ||
| 75 | # We can return to this location by calling #redirect_back_or_default. | ||
| 76 | def store_location | ||
| 77 | session[:return_to] = request.request_uri | ||
| 78 | end | ||
| 79 | |||
| 80 | # Redirect to the URI stored by the most recent store_location call or | ||
| 81 | # to the passed default. Set an appropriately modified | ||
| 82 | # after_filter :store_location, :only => [:index, :new, :show, :edit] | ||
| 83 | # for any controller you want to be bounce-backable. | ||
| 84 | def redirect_back_or_default(default) | ||
| 85 | redirect_to(session[:return_to] || default) | ||
| 86 | session[:return_to] = nil | ||
| 87 | end | ||
| 88 | |||
| 89 | # Inclusion hook to make #current_user and #logged_in? | ||
| 90 | # available as ActionView helper methods. | ||
| 91 | def self.included(base) | ||
| 92 | base.send :helper_method, :current_user, :logged_in?, :authorized? if base.respond_to? :helper_method | ||
| 93 | end | ||
| 94 | |||
| 95 | # | ||
| 96 | # Login | ||
| 97 | # | ||
| 98 | |||
| 99 | # Called from #current_user. First attempt to login by the user id stored in the session. | ||
| 100 | def login_from_session | ||
| 101 | self.current_user = User.find_by_id(session[:user_id]) if session[:user_id] | ||
| 102 | end | ||
| 103 | |||
| 104 | # | ||
| 105 | # Logout | ||
| 106 | # | ||
| 107 | |||
| 108 | # This is ususally what you want; resetting the session willy-nilly wreaks | ||
| 109 | # havoc with forgery protection, and is only strictly necessary on login. | ||
| 110 | # However, **all session state variables should be unset here**. | ||
| 111 | def logout_keeping_session! | ||
| 112 | # Kill server-side auth cookie | ||
| 113 | @current_user.forget_me if @current_user.is_a? User | ||
| 114 | @current_user = false # not logged in, and don't do it for me | ||
| 115 | kill_remember_cookie! # Kill client-side auth cookie | ||
| 116 | session[:user_id] = nil # keeps the session but kill our variable | ||
| 117 | # explicitly kill any other session variables you set | ||
| 118 | end | ||
| 119 | |||
| 120 | # The session should only be reset at the tail end of a form POST -- | ||
| 121 | # otherwise the request forgery protection fails. It's only really necessary | ||
| 122 | # when you cross quarantine (logged-out to logged-in). | ||
| 123 | def logout_killing_session! | ||
| 124 | logout_keeping_session! | ||
| 125 | reset_session | ||
| 126 | end | ||
| 127 | end | ||
diff --git a/lib/authenticated_test_helper.rb b/lib/authenticated_test_helper.rb new file mode 100644 index 0000000..c0ec5f4 --- /dev/null +++ b/lib/authenticated_test_helper.rb | |||
| @@ -0,0 +1,6 @@ | |||
| 1 | module AuthenticatedTestHelper | ||
| 2 | # Sets the current user in the session from the user fixtures. | ||
| 3 | def login_as(user) | ||
| 4 | @request.session[:user_id] = user ? users(user).id : nil | ||
| 5 | end | ||
| 6 | end | ||
diff --git a/lib/authentication.rb b/lib/authentication.rb new file mode 100644 index 0000000..a936589 --- /dev/null +++ b/lib/authentication.rb | |||
| @@ -0,0 +1,103 @@ | |||
| 1 | module Authentication | ||
| 2 | mattr_accessor :login_regex, :bad_login_message, | ||
| 3 | :name_regex, :bad_name_message, | ||
| 4 | :email_name_regex, :domain_head_regex, :domain_tld_regex, :email_regex, :bad_email_message | ||
| 5 | |||
| 6 | self.login_regex = /\A\w[\w\.\-_@]+\z/ # ASCII, strict | ||
| 7 | # self.login_regex = /\A[[:alnum:]][[:alnum:]\.\-_@]+\z/ # Unicode, strict | ||
| 8 | # self.login_regex = /\A[^[:cntrl:]\\<>\/&]*\z/ # Unicode, permissive | ||
| 9 | |||
| 10 | self.bad_login_message = "use only letters, numbers, and .-_@ please.".freeze | ||
| 11 | |||
| 12 | self.name_regex = /\A[^[:cntrl:]\\<>\/&]*\z/ # Unicode, permissive | ||
| 13 | self.bad_name_message = "avoid non-printing characters and \\><&/ please.".freeze | ||
| 14 | |||
| 15 | self.email_name_regex = '[\w\.%\+\-]+'.freeze | ||
| 16 | self.domain_head_regex = '(?:[A-Z0-9\-]+\.)+'.freeze | ||
| 17 | self.domain_tld_regex = '(?:[A-Z]{2}|com|org|net|edu|gov|mil|biz|info|mobi|name|aero|jobs|museum)'.freeze | ||
| 18 | self.email_regex = /\A#{email_name_regex}@#{domain_head_regex}#{domain_tld_regex}\z/i | ||
| 19 | self.bad_email_message = "should look like an email address.".freeze | ||
| 20 | |||
| 21 | def self.included(recipient) | ||
| 22 | recipient.extend(ModelClassMethods) | ||
| 23 | recipient.class_eval do | ||
| 24 | include ModelInstanceMethods | ||
| 25 | end | ||
| 26 | end | ||
| 27 | |||
| 28 | module ModelClassMethods | ||
| 29 | def secure_digest(*args) | ||
| 30 | Digest::SHA1.hexdigest(args.flatten.join('--')) | ||
| 31 | end | ||
| 32 | |||
| 33 | def make_token | ||
| 34 | secure_digest(Time.now, (1..10).map{ rand.to_s }) | ||
| 35 | end | ||
| 36 | end # class methods | ||
| 37 | |||
| 38 | module ModelInstanceMethods | ||
| 39 | end # instance methods | ||
| 40 | |||
| 41 | module ByPassword | ||
| 42 | # Stuff directives into including module | ||
| 43 | def self.included(recipient) | ||
| 44 | recipient.extend(ModelClassMethods) | ||
| 45 | recipient.class_eval do | ||
| 46 | include ModelInstanceMethods | ||
| 47 | |||
| 48 | # Virtual attribute for the unencrypted password | ||
| 49 | attr_accessor :password | ||
| 50 | validates_presence_of :password, :if => :password_required? | ||
| 51 | validates_presence_of :password_confirmation, :if => :password_required? | ||
| 52 | validates_confirmation_of :password, :if => :password_required? | ||
| 53 | validates_length_of :password, :within => 6..40, :if => :password_required? | ||
| 54 | before_save :encrypt_password | ||
| 55 | end | ||
| 56 | end # #included directives | ||
| 57 | |||
| 58 | # | ||
| 59 | # Class Methods | ||
| 60 | # | ||
| 61 | module ModelClassMethods | ||
| 62 | # This provides a modest increased defense against a dictionary attack if | ||
| 63 | # your db were ever compromised, but will invalidate existing passwords. | ||
| 64 | # See the README and the file config/initializers/site_keys.rb | ||
| 65 | # | ||
| 66 | # It may not be obvious, but if you set REST_AUTH_SITE_KEY to nil and | ||
| 67 | # REST_AUTH_DIGEST_STRETCHES to 1 you'll have backwards compatibility with | ||
| 68 | # older versions of restful-authentication. | ||
| 69 | def password_digest(password, salt) | ||
| 70 | digest = REST_AUTH_SITE_KEY | ||
| 71 | REST_AUTH_DIGEST_STRETCHES.times do | ||
| 72 | digest = secure_digest(digest, salt, password, REST_AUTH_SITE_KEY) | ||
| 73 | end | ||
| 74 | digest | ||
| 75 | end | ||
| 76 | end # class methods | ||
| 77 | |||
| 78 | # | ||
| 79 | # Instance Methods | ||
| 80 | # | ||
| 81 | module ModelInstanceMethods | ||
| 82 | |||
| 83 | # Encrypts the password with the user salt | ||
| 84 | def encrypt(password) | ||
| 85 | self.class.password_digest(password, salt) | ||
| 86 | end | ||
| 87 | |||
| 88 | def authenticated?(password) | ||
| 89 | crypted_password == encrypt(password) | ||
| 90 | end | ||
| 91 | |||
| 92 | # before filter | ||
| 93 | def encrypt_password | ||
| 94 | return if password.blank? | ||
| 95 | self.salt = self.class.make_token if new_record? | ||
| 96 | self.crypted_password = encrypt(password) | ||
| 97 | end | ||
| 98 | def password_required? | ||
| 99 | crypted_password.blank? || !password.blank? | ||
| 100 | end | ||
| 101 | end # instance methods | ||
| 102 | end | ||
| 103 | end \ No newline at end of file | ||
diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index 74fafbd..3abe206 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml | |||
| @@ -1,13 +1,18 @@ | |||
| 1 | # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html | ||
| 2 | 1 | ||
| 3 | one: | 2 | quentin: |
| 4 | login: MyString | 3 | id: 1 |
| 5 | email: MyString | 4 | login: quentin |
| 6 | crypted_password: MyString | 5 | email: quentin@example.com |
| 7 | salt: MyString | 6 | salt: 356a192b7913b04c54574d18c28d46e6395428ab # SHA1('0') |
| 7 | crypted_password: 89e27e324f2dee0fb72034631aa1bc3ca28ea574 # 'monkey' | ||
| 8 | created_at: <%= 5.days.ago.to_s :db %> | ||
| 9 | |||
| 10 | aaron: | ||
| 11 | id: 2 | ||
| 12 | login: aaron | ||
| 13 | email: aaron@example.com | ||
| 14 | salt: da4b9237bacccdf19c0760cab7aec4a8359010b0 # SHA1('1') | ||
| 15 | crypted_password: cf39f8e6972c25ac72ccc801cab755ef15bca09b # 'monkey' | ||
| 16 | created_at: <%= 1.days.ago.to_s :db %> | ||
| 17 | |||
| 8 | 18 | ||
| 9 | two: | ||
| 10 | login: MyString | ||
| 11 | email: MyString | ||
| 12 | crypted_password: MyString | ||
| 13 | salt: MyString | ||
diff --git a/test/functional/sessions_controller_test.rb b/test/functional/sessions_controller_test.rb new file mode 100644 index 0000000..e53bcd8 --- /dev/null +++ b/test/functional/sessions_controller_test.rb | |||
| @@ -0,0 +1,82 @@ | |||
| 1 | require File.dirname(__FILE__) + '/../test_helper' | ||
| 2 | require 'sessions_controller' | ||
| 3 | |||
| 4 | # Re-raise errors caught by the controller. | ||
| 5 | class SessionsController; def rescue_action(e) raise e end; end | ||
| 6 | |||
| 7 | class SessionsControllerTest < ActionController::TestCase | ||
| 8 | # Be sure to include AuthenticatedTestHelper in test/test_helper.rb instead | ||
| 9 | # Then, you can remove it from this and the units test. | ||
| 10 | include AuthenticatedTestHelper | ||
| 11 | |||
| 12 | fixtures :users | ||
| 13 | |||
| 14 | def test_should_login_and_redirect | ||
| 15 | post :create, :login => 'quentin', :password => 'monkey' | ||
| 16 | assert session[:user_id] | ||
| 17 | assert_response :redirect | ||
| 18 | end | ||
| 19 | |||
| 20 | def test_should_fail_login_and_not_redirect | ||
| 21 | post :create, :login => 'quentin', :password => 'bad password' | ||
| 22 | assert_nil session[:user_id] | ||
| 23 | assert_response :success | ||
| 24 | end | ||
| 25 | |||
| 26 | def test_should_logout | ||
| 27 | login_as :quentin | ||
| 28 | get :destroy | ||
| 29 | assert_nil session[:user_id] | ||
| 30 | assert_response :redirect | ||
| 31 | end | ||
| 32 | |||
| 33 | def test_should_remember_me | ||
| 34 | @request.cookies["auth_token"] = nil | ||
| 35 | post :create, :login => 'quentin', :password => 'monkey', :remember_me => "1" | ||
| 36 | assert_not_nil @response.cookies["auth_token"] | ||
| 37 | end | ||
| 38 | |||
| 39 | def test_should_not_remember_me | ||
| 40 | @request.cookies["auth_token"] = nil | ||
| 41 | post :create, :login => 'quentin', :password => 'monkey', :remember_me => "0" | ||
| 42 | puts @response.cookies["auth_token"] | ||
| 43 | assert @response.cookies["auth_token"].blank? | ||
| 44 | end | ||
| 45 | |||
| 46 | def test_should_delete_token_on_logout | ||
| 47 | login_as :quentin | ||
| 48 | get :destroy | ||
| 49 | assert @response.cookies["auth_token"].blank? | ||
| 50 | end | ||
| 51 | |||
| 52 | def test_should_login_with_cookie | ||
| 53 | users(:quentin).remember_me | ||
| 54 | @request.cookies["auth_token"] = cookie_for(:quentin) | ||
| 55 | get :new | ||
| 56 | assert @controller.send(:logged_in?) | ||
| 57 | end | ||
| 58 | |||
| 59 | def test_should_fail_expired_cookie_login | ||
| 60 | users(:quentin).remember_me | ||
| 61 | users(:quentin).update_attribute :remember_token_expires_at, 5.minutes.ago | ||
| 62 | @request.cookies["auth_token"] = cookie_for(:quentin) | ||
| 63 | get :new | ||
| 64 | assert !@controller.send(:logged_in?) | ||
| 65 | end | ||
| 66 | |||
| 67 | def test_should_fail_cookie_login | ||
| 68 | users(:quentin).remember_me | ||
| 69 | @request.cookies["auth_token"] = auth_token('invalid_auth_token') | ||
| 70 | get :new | ||
| 71 | assert !@controller.send(:logged_in?) | ||
| 72 | end | ||
| 73 | |||
| 74 | protected | ||
| 75 | def auth_token(token) | ||
| 76 | CGI::Cookie.new('name' => 'auth_token', 'value' => token) | ||
| 77 | end | ||
| 78 | |||
| 79 | def cookie_for(user) | ||
| 80 | auth_token users(user).remember_token | ||
| 81 | end | ||
| 82 | end | ||
diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index a64d2d3..47e3129 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb | |||
| @@ -1,8 +1,64 @@ | |||
| 1 | require 'test_helper' | 1 | require File.dirname(__FILE__) + '/../test_helper' |
| 2 | 2 | ||
| 3 | class UserTest < ActiveSupport::TestCase | 3 | class UserTest < ActiveSupport::TestCase |
| 4 | # Replace this with your real tests. | 4 | # Be sure to include AuthenticatedTestHelper in test/test_helper.rb instead. |
| 5 | test "the truth" do | 5 | # Then, you can remove it from this and the functional test. |
| 6 | assert true | 6 | include AuthenticatedTestHelper |
| 7 | fixtures :users | ||
| 8 | |||
| 9 | def test_should_create_user | ||
| 10 | assert_difference 'User.count' do | ||
| 11 | user = create_user | ||
| 12 | assert !user.new_record?, "#{user.errors.full_messages.to_sentence}" | ||
| 13 | end | ||
| 14 | end | ||
| 15 | |||
| 16 | def test_should_require_login | ||
| 17 | assert_no_difference 'User.count' do | ||
| 18 | u = create_user(:login => nil) | ||
| 19 | assert u.errors.on(:login) | ||
| 20 | end | ||
| 21 | end | ||
| 22 | |||
| 23 | def test_should_require_password | ||
| 24 | assert_no_difference 'User.count' do | ||
| 25 | u = create_user(:password => nil) | ||
| 26 | assert u.errors.on(:password) | ||
| 27 | end | ||
| 28 | end | ||
| 29 | |||
| 30 | def test_should_require_password_confirmation | ||
| 31 | assert_no_difference 'User.count' do | ||
| 32 | u = create_user(:password_confirmation => nil) | ||
| 33 | assert u.errors.on(:password_confirmation) | ||
| 34 | end | ||
| 35 | end | ||
| 36 | |||
| 37 | def test_should_require_email | ||
| 38 | assert_no_difference 'User.count' do | ||
| 39 | u = create_user(:email => nil) | ||
| 40 | assert u.errors.on(:email) | ||
| 41 | end | ||
| 42 | end | ||
| 43 | |||
| 44 | def test_should_reset_password | ||
| 45 | users(:quentin).update_attributes(:password => 'new password', :password_confirmation => 'new password') | ||
| 46 | assert_equal users(:quentin), User.authenticate('quentin', 'new password') | ||
| 47 | end | ||
| 48 | |||
| 49 | def test_should_not_rehash_password | ||
| 50 | users(:quentin).update_attributes(:login => 'quentin2') | ||
| 51 | assert_equal users(:quentin), User.authenticate('quentin2', 'monkey') | ||
| 52 | end | ||
| 53 | |||
| 54 | def test_should_authenticate_user | ||
| 55 | assert_equal users(:quentin), User.authenticate('quentin', 'monkey') | ||
| 56 | end | ||
| 57 | |||
| 58 | protected | ||
| 59 | def create_user(options = {}) | ||
| 60 | record = User.new({ :login => 'quire', :email => 'quire@example.com', :password => 'quire69', :password_confirmation => 'quire69' }.merge(options)) | ||
| 61 | record.save | ||
| 62 | record | ||
| 7 | end | 63 | end |
| 8 | end | 64 | end |
