authpwn_rails 0.12.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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>