authpwn_rails 0.12.0 → 0.12.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/.travis.yml +7 -2
  2. data/VERSION +1 -1
  3. data/app/models/credentials/password.rb +16 -8
  4. data/app/models/credentials/token.rb +8 -0
  5. data/app/models/tokens/email_verification.rb +3 -0
  6. data/app/models/tokens/password_reset.rb +5 -2
  7. data/app/models/tokens/session_uid.rb +54 -0
  8. data/authpwn_rails.gemspec +8 -2
  9. data/lib/authpwn_rails.rb +3 -2
  10. data/lib/authpwn_rails/current_user.rb +1 -10
  11. data/lib/authpwn_rails/engine.rb +2 -2
  12. data/lib/authpwn_rails/expires.rb +23 -0
  13. data/lib/authpwn_rails/generators/all_generator.rb +9 -4
  14. data/lib/authpwn_rails/generators/templates/credential.rb +1 -1
  15. data/lib/authpwn_rails/generators/templates/credentials.yml +16 -0
  16. data/lib/authpwn_rails/generators/templates/initializer.rb +18 -0
  17. data/lib/authpwn_rails/generators/templates/session/forbidden.html.erb +1 -1
  18. data/lib/authpwn_rails/generators/templates/session/home.html.erb +1 -1
  19. data/lib/authpwn_rails/generators/templates/session/new.html.erb +3 -3
  20. data/lib/authpwn_rails/generators/templates/session/welcome.html.erb +1 -1
  21. data/lib/authpwn_rails/generators/templates/session_controller.rb +13 -4
  22. data/lib/authpwn_rails/generators/templates/session_controller_test.rb +12 -2
  23. data/lib/authpwn_rails/generators/templates/session_mailer.rb +3 -3
  24. data/lib/authpwn_rails/generators/templates/session_mailer/email_verification_email.html.erb +3 -3
  25. data/lib/authpwn_rails/generators/templates/session_mailer/reset_password_email.html.erb +3 -3
  26. data/lib/authpwn_rails/generators/templates/session_mailer_test.rb +4 -4
  27. data/lib/authpwn_rails/routes.rb +4 -4
  28. data/lib/authpwn_rails/session.rb +31 -8
  29. data/lib/authpwn_rails/session_controller.rb +27 -18
  30. data/lib/authpwn_rails/test_extensions.rb +16 -6
  31. data/lib/authpwn_rails/user_model.rb +10 -10
  32. data/test/cookie_controller_test.rb +165 -16
  33. data/test/credentials/email_verification_token_test.rb +11 -11
  34. data/test/credentials/password_credential_test.rb +31 -12
  35. data/test/credentials/session_uid_token_test.rb +98 -0
  36. data/test/credentials/token_crendential_test.rb +46 -12
  37. data/test/helpers/db_setup.rb +6 -5
  38. data/test/helpers/routes.rb +5 -2
  39. data/test/initializer_test.rb +18 -0
  40. data/test/session_controller_api_test.rb +127 -53
  41. data/test/test_extensions_test.rb +41 -0
  42. data/test/test_helper.rb +3 -0
  43. data/test/user_test.rb +11 -10
  44. metadata +9 -3
data/.travis.yml CHANGED
@@ -1,5 +1,10 @@
1
+ language: ruby
2
+ env:
3
+ - DB=mysql
4
+ - DB=pg DB_USER=postgres
5
+ - DB=sqlite
1
6
  rvm:
2
7
  - 1.8.7
3
- - 1.9.2
4
8
  - 1.9.3
5
- - rbx-head
9
+ - rbx-18mode
10
+ - rbx-19mode
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.12.0
1
+ 0.12.1
@@ -1,6 +1,6 @@
1
1
  # :namespace
2
2
  module Credentials
3
-
3
+
4
4
  # Associates a password with the user account.
5
5
  class Password < ::Credential
6
6
  # Virtual attribute: the user's password.
@@ -11,9 +11,16 @@ class Password < ::Credential
11
11
  # Virtual attribute: confirmation for the user's password.
12
12
  attr_accessor :password_confirmation
13
13
 
14
- # A user can have a single password
14
+ # A user can have a single password.
15
15
  validates :user_id, :uniqueness => true
16
16
 
17
+ # Passwords can expire, if users don't change them often enough.
18
+ include Authpwn::Expires
19
+ # Passwords don't expire by default, because it is non-trivial to get e-mail
20
+ # delivery working in Rails, which is necessary for recovering from expired
21
+ # passwords.
22
+ self.expires_after = nil
23
+
17
24
  # Compares a plain-text password against the password hash in this credential.
18
25
  #
19
26
  # Returns +true+ for a match, +false+ otherwise.
@@ -21,16 +28,17 @@ class Password < ::Credential
21
28
  return false unless key
22
29
  key == self.class.hash_password(password, key.split('|', 2).first)
23
30
  end
24
-
31
+
25
32
  # Compares a plain-text password against the password hash in this credential.
26
33
  #
27
34
  # Returns the authenticated User instance, or a symbol indicating the reason
28
35
  # why the (potentially valid) password was rejected.
29
36
  def authenticate(password)
37
+ return :expired if expired?
30
38
  return :invalid unless check_password(password)
31
39
  user.auth_bounce_reason(self) || user
32
40
  end
33
-
41
+
34
42
  # Password virtual attribute.
35
43
  def password=(new_password)
36
44
  @password = new_password
@@ -50,7 +58,7 @@ class Password < ::Credential
50
58
  def self.authenticate_email(email, password)
51
59
  user = Credentials::Email.authenticate email
52
60
  return user if user.is_a? Symbol
53
-
61
+
54
62
  credential = user.credentials.find { |c| c.kind_of? Credentials::Password }
55
63
  credential ? credential.authenticate(password) : :invalid
56
64
  end
@@ -59,14 +67,14 @@ class Password < ::Credential
59
67
  def self.hash_password(password, salt)
60
68
  salt + '|' + Digest::SHA2.hexdigest(password + salt)
61
69
  end
62
-
70
+
63
71
  # Generates a random salt value.
64
72
  def self.random_salt
65
73
  [(0...12).map { |i| 1 + rand(255) }.pack('C*')].pack('m').strip
66
74
  end
67
-
75
+
68
76
  # Forms can only change the plain-text password fields.
69
- attr_accessible :password, :password_confirmation
77
+ attr_accessible :password, :password_confirmation
70
78
  end # class Credentials::Password
71
79
 
72
80
  end # namespace Credentials
@@ -29,6 +29,10 @@ class Token < ::Credential
29
29
  validates :name, :format => /^[A-Za-z0-9\_\-]+$/, :presence => true,
30
30
  :uniqueness => true
31
31
 
32
+ # Tokens can expire. This is a good idea most of the time, because token
33
+ # codes are supposed to be used quickly.
34
+ include Authpwn::Expires
35
+
32
36
  # Authenticates a user using a secret token code.
33
37
  #
34
38
  # The token will be spent on successful authentication. One-time tokens are
@@ -67,6 +71,10 @@ class Token < ::Credential
67
71
  # Returns the authenticated User instance, or a symbol indicating the reason
68
72
  # why the (potentially valid) token code was rejected.
69
73
  def authenticate
74
+ if expired?
75
+ destroy
76
+ return :invalid
77
+ end
70
78
  if bounce = user.auth_bounce_reason(self)
71
79
  return bounce
72
80
  end
@@ -12,6 +12,9 @@ class EmailVerification < OneTime
12
12
  alias_attribute :email, :key
13
13
  validates :email, :presence => true
14
14
 
15
+ # Decent compromise between convenience and security.
16
+ self.expires_after = 3.days
17
+
15
18
  # Creates a token with a random code that verifies the given e-mail address.
16
19
  def self.random_for(email_credential)
17
20
  super email_credential.user, email_credential.email, self
@@ -1,8 +1,11 @@
1
1
  # :namespace
2
2
  module Tokens
3
-
3
+
4
4
  # Lets the user to change their password without knowing the old one.
5
5
  class PasswordReset < OneTime
6
+ # Decent compromise between convenience and security.
7
+ self.expires_after = 3.days
8
+
6
9
  # Blanks the user's old password, so the new password form won't ask for it.
7
10
  #
8
11
  # Returns the token instance.
@@ -14,7 +17,7 @@ class PasswordReset < OneTime
14
17
  super
15
18
  end
16
19
  end
17
-
20
+
18
21
  # The credential that is removed by this token.
19
22
  #
20
23
  # This method might return nil if a user initiates password recovery multiple
@@ -0,0 +1,54 @@
1
+ # :namespace
2
+ module Tokens
3
+
4
+ class SessionUid < Credentials::Token
5
+ # The session UID.
6
+ alias_attribute :suid, :name
7
+
8
+ # The IP address and User-Agent string of the browser using this session.
9
+ store :key, :accessors => [:browser_ip, :browser_ua]
10
+
11
+ # The User-Agent header of the browser that received this suid.
12
+ validates :browser_ua, :presence => true
13
+
14
+ # The IP of the computer that received this suid.
15
+ validates :browser_ip, :presence => true
16
+
17
+ # Decent compromise between convenience and security.
18
+ self.expires_after = 14.days
19
+
20
+ # Creates a new session UID token for a user.
21
+ #
22
+ # @param [User] user the user authenticated using this session
23
+ # @param [String] browser_ip the IP of the session
24
+ # @param [String] browser_ua the User-Agent of the browser used for this
25
+ # session
26
+ def self.random_for(user, browser_ip, browser_ua)
27
+ browser_ua = browser_ua[0, 1536] if browser_ua.length > 1536
28
+ key = { :browser_ip => browser_ip, :browser_ua => browser_ua }
29
+ super user, key, self
30
+ end
31
+
32
+ # Refresh precision for the updated_at timestamp, in seconds.
33
+ #
34
+ # When a session UID is used to authenticate a user, its updated_at time is
35
+ # refreshed if it differs from the current time by this much.
36
+ class_attribute :updates_after, :instance_writer => false
37
+ self.updates_after = 1.hour
38
+
39
+ # Updates the time associated with the session.
40
+ def spend
41
+ self.touch if Time.now - updated_at >= updates_after
42
+ end
43
+
44
+ # Garbage-collects database records of expired sessions.
45
+ #
46
+ # This method should be called periodically to keep the size of the session
47
+ # table under control.
48
+ def self.remove_expired
49
+ self.where('updated_at < ?', Time.now - expires_after).delete_all
50
+ self
51
+ end
52
+ end # class Tokens::SessionUid
53
+
54
+ end # namespace Tokens
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "authpwn_rails"
8
- s.version = "0.12.0"
8
+ s.version = "0.12.1"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Victor Costan"]
12
- s.date = "2012-09-24"
12
+ s.date = "2012-10-05"
13
13
  s.description = "Works with Facebook."
14
14
  s.email = "victor@costan.us"
15
15
  s.extra_rdoc_files = [
@@ -34,6 +34,7 @@ Gem::Specification.new do |s|
34
34
  "app/models/tokens/email_verification.rb",
35
35
  "app/models/tokens/one_time.rb",
36
36
  "app/models/tokens/password_reset.rb",
37
+ "app/models/tokens/session_uid.rb",
37
38
  "authpwn_rails.gemspec",
38
39
  "legacy/migrate_011_to_012.rb",
39
40
  "legacy/migrate_09_to_010.rb",
@@ -41,12 +42,14 @@ Gem::Specification.new do |s|
41
42
  "lib/authpwn_rails/credential_model.rb",
42
43
  "lib/authpwn_rails/current_user.rb",
43
44
  "lib/authpwn_rails/engine.rb",
45
+ "lib/authpwn_rails/expires.rb",
44
46
  "lib/authpwn_rails/facebook_session.rb",
45
47
  "lib/authpwn_rails/generators/all_generator.rb",
46
48
  "lib/authpwn_rails/generators/templates/001_create_users.rb",
47
49
  "lib/authpwn_rails/generators/templates/003_create_credentials.rb",
48
50
  "lib/authpwn_rails/generators/templates/credential.rb",
49
51
  "lib/authpwn_rails/generators/templates/credentials.yml",
52
+ "lib/authpwn_rails/generators/templates/initializer.rb",
50
53
  "lib/authpwn_rails/generators/templates/session/forbidden.html.erb",
51
54
  "lib/authpwn_rails/generators/templates/session/home.html.erb",
52
55
  "lib/authpwn_rails/generators/templates/session/new.html.erb",
@@ -79,6 +82,7 @@ Gem::Specification.new do |s|
79
82
  "test/credentials/one_time_token_credential_test.rb",
80
83
  "test/credentials/password_credential_test.rb",
81
84
  "test/credentials/password_reset_token_test.rb",
85
+ "test/credentials/session_uid_token_test.rb",
82
86
  "test/credentials/token_crendential_test.rb",
83
87
  "test/facebook_controller_test.rb",
84
88
  "test/fixtures/bare_session/forbidden.html.erb",
@@ -94,9 +98,11 @@ Gem::Specification.new do |s|
94
98
  "test/helpers/routes.rb",
95
99
  "test/helpers/view_helpers.rb",
96
100
  "test/http_basic_controller_test.rb",
101
+ "test/initializer_test.rb",
97
102
  "test/routes_test.rb",
98
103
  "test/session_controller_api_test.rb",
99
104
  "test/session_mailer_api_test.rb",
105
+ "test/test_extensions_test.rb",
100
106
  "test/test_helper.rb",
101
107
  "test/user_extensions/email_field_test.rb",
102
108
  "test/user_extensions/facebook_fields_test.rb",
data/lib/authpwn_rails.rb CHANGED
@@ -3,8 +3,10 @@ require 'active_support'
3
3
  # :nodoc: namespace
4
4
  module Authpwn
5
5
  extend ActiveSupport::Autoload
6
-
6
+
7
7
  autoload :CredentialModel, 'authpwn_rails/credential_model.rb'
8
+ autoload :CurrentUser, 'authpwn_rails/current_user.rb'
9
+ autoload :Expires, 'authpwn_rails/expires.rb'
8
10
  autoload :SessionController, 'authpwn_rails/session_controller.rb'
9
11
  autoload :SessionMailer, 'authpwn_rails/session_mailer.rb'
10
12
  autoload :UserModel, 'authpwn_rails/user_model.rb'
@@ -17,7 +19,6 @@ module Authpwn
17
19
  end
18
20
  end
19
21
 
20
- require 'authpwn_rails/current_user.rb'
21
22
  require 'authpwn_rails/facebook_session.rb'
22
23
  require 'authpwn_rails/http_basic.rb'
23
24
  require 'authpwn_rails/routes.rb'
@@ -3,16 +3,7 @@ module Authpwn
3
3
 
4
4
  # The unofficial Rails convention for tracking the authenticated user.
5
5
  module CurrentUser
6
- attr_reader :current_user
7
-
8
- def current_user=(user)
9
- @current_user = user
10
- if user
11
- session[:user_exuid] = user.to_param
12
- else
13
- session.delete :user_exuid
14
- end
15
- end
6
+ attr_accessor :current_user
16
7
  end # module Authpwn::CurrentUser
17
8
 
18
9
  end # namespace Authpwn
@@ -8,11 +8,11 @@ class Engine < Rails::Engine
8
8
  generators do
9
9
  require 'authpwn_rails/generators/all_generator.rb'
10
10
  end
11
-
11
+
12
12
  initializer 'authpwn.rspec.extensions' do
13
13
  begin
14
14
  require 'rspec'
15
-
15
+
16
16
  RSpec.configure do |c|
17
17
  c.include Authpwn::TestExtensions
18
18
  c.include Authpwn::ControllerTestExtensions
@@ -0,0 +1,23 @@
1
+ # :nodoc: namespace
2
+ module Authpwn
3
+
4
+ # Common code for credentials that expire.
5
+ module Expires
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ # Number of seconds after which a credential becomes unusable.
10
+ #
11
+ # Users can reset this timer by updating their credentials, e.g. changing
12
+ # their password.
13
+ class_attribute :expires_after, :instance_writer => false
14
+ end
15
+
16
+ # True if this password is too old and should not be used for authentication.
17
+ def expired?
18
+ return false unless expires_after
19
+ updated_at < Time.now - expires_after
20
+ end
21
+ end # module Authpwn::Expires
22
+
23
+ end # namespace Authpwn
@@ -11,7 +11,7 @@ class AllGenerator < Rails::Generators::Base
11
11
  File.join('db', 'migrate', '20100725000001_create_users.rb')
12
12
  copy_file 'users.yml', File.join('test', 'fixtures', 'users.yml')
13
13
  end
14
-
14
+
15
15
  def create_credential_model
16
16
  copy_file 'credential.rb', File.join('app', 'models', 'credential.rb')
17
17
  copy_file '003_create_credentials.rb',
@@ -19,17 +19,17 @@ class AllGenerator < Rails::Generators::Base
19
19
  copy_file 'credentials.yml',
20
20
  File.join('test', 'fixtures', 'credentials.yml')
21
21
  end
22
-
22
+
23
23
  def create_session_controller
24
24
  copy_file 'session_controller.rb',
25
- File.join('app', 'controllers', 'session_controller.rb')
25
+ File.join('app', 'controllers', 'session_controller.rb')
26
26
  copy_file File.join('session_controller_test.rb'),
27
27
  File.join('test', 'functional', 'session_controller_test.rb')
28
28
 
29
29
  route "authpwn_session"
30
30
  route "root :to => 'session#show'"
31
31
  end
32
-
32
+
33
33
  def create_session_views
34
34
  copy_file File.join('session', 'forbidden.html.erb'),
35
35
  File.join('app', 'views', 'session', 'forbidden.html.erb')
@@ -58,6 +58,11 @@ class AllGenerator < Rails::Generators::Base
58
58
  File.join('app', 'views', 'session_mailer',
59
59
  'reset_password_email.text.erb')
60
60
  end
61
+
62
+ def create_initializer
63
+ copy_file 'initializer.rb',
64
+ File.join('config', 'initializers', 'authpwn.rb')
65
+ end
61
66
  end # class Authpwn::AllGenerator
62
67
 
63
68
  end # namespace Authpwn
@@ -9,5 +9,5 @@ end
9
9
  module Credentials
10
10
 
11
11
  # Add your custom Credential types here.
12
-
12
+
13
13
  end
@@ -53,3 +53,19 @@ jane_password_token:
53
53
  user: jane
54
54
  type: Tokens::PasswordReset
55
55
  name: nbMLTKN18tYy9plBAbsrwT6zdE2jZqoKPk6Ze4lHMSQ
56
+
57
+ john_session_token:
58
+ user: john
59
+ type: Tokens::SessionUid
60
+ name: iyHvfTnYoF1f1jL9Vnb55hnXobf2Ld6HxIW-PXya6dw
61
+ key: <%= { :browser_ip => '18.241.1.121',
62
+ :browser_ua => 'Mozilla/5.0 (X11; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1'
63
+ }.to_yaml.inspect %>
64
+
65
+ jane_session_token:
66
+ user: jane
67
+ type: Tokens::SessionUid
68
+ name: sNIfh6UavUSceL0TpubJ-DnZRuxPSTAddoHBb-twEIg
69
+ key: <%= { :browser_ip => '18.70.0.160',
70
+ :browser_ua => 'Mozilla/5.0 (X11; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1'
71
+ }.to_yaml.inspect %>
@@ -0,0 +1,18 @@
1
+ # Tweak the password expiration interval, or comment out the line to disable
2
+ # password expiration altogether.
3
+ #
4
+ # NOTE: when a user's password expires, he will need to use the password reset
5
+ # flow, which relies on e-mail delivery. If your application doesn't implement
6
+ # password reset, or doesn't have working e-mail delivery, disable password
7
+ # expiration.
8
+ Credentials::Password.expires_after = 1.year
9
+
10
+ # These codes are sent in plaintext in e-mails, be somewhat aggressive.
11
+ Tokens::EmailVerification.expires_after = 3.days
12
+ Tokens::PasswordReset.expires_after = 3.days
13
+
14
+ # Users are identified by cookies whose codes are looked up in the database.
15
+ Tokens::SessionUid.expires_after = 14.days
16
+ # This knob is a compromise between accurate session expiration and write
17
+ # workload on the database. Keep it below 1% of expires_after.
18
+ Tokens::SessionUid.updates_after = 1.hour
@@ -7,7 +7,7 @@
7
7
  You should inform the user that they are logged in as
8
8
  <%= current_user.exuid %> and suggest them to
9
9
  <%= link_to 'Log out', session_path, :method => :delete %> and log in as a
10
- different user.
10
+ different user.
11
11
  </p>
12
12
  <% else %>
13
13
  <p>
@@ -1,5 +1,5 @@
1
1
  <p>
2
- This view gets displayed when the user is logged in. Right now,
2
+ This view gets displayed when the user is logged in. Right now,
3
3
  user <%= current_user.exuid %> is logged in. You should allow the user to
4
4
  <%= link_to 'Log out', session_path, :method => :delete %>.
5
5
  </p>