authpwn_rails 0.10.5 → 0.10.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/Gemfile +8 -8
  2. data/Gemfile.lock +1 -1
  3. data/VERSION +1 -1
  4. data/app/models/credentials/email.rb +12 -4
  5. data/app/models/credentials/token.rb +106 -0
  6. data/app/models/tokens/email_verification.rb +42 -0
  7. data/app/models/tokens/one_time.rb +16 -0
  8. data/app/models/tokens/password_reset.rb +27 -0
  9. data/authpwn_rails.gemspec +36 -11
  10. data/lib/authpwn_rails.rb +2 -0
  11. data/lib/authpwn_rails/generators/all_generator.rb +20 -2
  12. data/lib/authpwn_rails/generators/templates/credentials.yml +21 -0
  13. data/lib/authpwn_rails/generators/templates/session/new.html.erb +10 -5
  14. data/lib/authpwn_rails/generators/templates/session/password_change.html.erb +37 -0
  15. data/lib/authpwn_rails/generators/templates/session_controller.rb +20 -2
  16. data/lib/authpwn_rails/generators/templates/session_controller_test.rb +71 -0
  17. data/lib/authpwn_rails/generators/templates/session_mailer.rb +26 -0
  18. data/lib/authpwn_rails/generators/templates/session_mailer/email_verification_email.html.erb +23 -0
  19. data/lib/authpwn_rails/generators/templates/session_mailer/email_verification_email.text.erb +11 -0
  20. data/lib/authpwn_rails/generators/templates/session_mailer/reset_password_email.html.erb +23 -0
  21. data/lib/authpwn_rails/generators/templates/session_mailer/reset_password_email.text.erb +11 -0
  22. data/lib/authpwn_rails/generators/templates/session_mailer_test.rb +37 -0
  23. data/lib/authpwn_rails/routes.rb +50 -0
  24. data/lib/authpwn_rails/session_controller.rb +129 -0
  25. data/lib/authpwn_rails/session_mailer.rb +66 -0
  26. data/test/{email_credential_test.rb → credentials/email_credential_test.rb} +1 -1
  27. data/test/credentials/email_verification_token_test.rb +78 -0
  28. data/test/{facebook_credential_test.rb → credentials/facebook_credential_test.rb} +1 -1
  29. data/test/credentials/one_time_token_credential_test.rb +84 -0
  30. data/test/{password_credential_test.rb → credentials/password_credential_test.rb} +1 -1
  31. data/test/credentials/password_reset_token_test.rb +72 -0
  32. data/test/credentials/token_crendential_test.rb +102 -0
  33. data/test/fixtures/bare_session/forbidden.html.erb +20 -0
  34. data/test/fixtures/bare_session/home.html.erb +5 -0
  35. data/test/fixtures/bare_session/new.html.erb +32 -0
  36. data/test/fixtures/bare_session/password_change.html.erb +30 -0
  37. data/test/fixtures/bare_session/welcome.html.erb +5 -0
  38. data/test/helpers/action_mailer.rb +8 -0
  39. data/test/helpers/routes.rb +8 -2
  40. data/test/routes_test.rb +31 -0
  41. data/test/session_controller_api_test.rb +310 -15
  42. data/test/session_mailer_api_test.rb +67 -0
  43. data/test/test_helper.rb +3 -1
  44. data/test/{email_field_test.rb → user_extensions/email_field_test.rb} +1 -1
  45. data/test/{facebook_fields_test.rb → user_extensions/facebook_fields_test.rb} +1 -1
  46. data/test/{password_field_test.rb → user_extensions/password_field_test.rb} +1 -1
  47. metadata +49 -24
data/Gemfile CHANGED
@@ -1,11 +1,11 @@
1
- source "http://rubygems.org"
2
- gem "fbgraph_rails", ">= 0.2.2"
3
- gem "rails", ">= 3.2.0.rc2"
1
+ source 'http://rubygems.org'
2
+ gem 'fbgraph_rails', '>= 0.2.2'
3
+ gem 'rails', '>= 3.2.0.rc2'
4
4
 
5
5
  group :development do
6
- gem "bundler", "~> 1.0.0"
7
- gem "flexmock", "~> 0.9.0"
8
- gem "jeweler", "~> 1.6.0"
9
- gem "rcov", ">= 0", :platform => :mri
10
- gem "sqlite3", ">= 1.3.3"
6
+ gem 'bundler', '~> 1.0.0'
7
+ gem 'flexmock', '~> 0.9.0'
8
+ gem 'jeweler', '~> 1.6.0'
9
+ gem 'rcov', '>= 0', :platform => :mri
10
+ gem 'sqlite3', '>= 1.3.5'
11
11
  end
data/Gemfile.lock CHANGED
@@ -122,4 +122,4 @@ DEPENDENCIES
122
122
  jeweler (~> 1.6.0)
123
123
  rails (>= 3.2.0.rc2)
124
124
  rcov
125
- sqlite3 (>= 1.3.3)
125
+ sqlite3 (>= 1.3.5)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.10.5
1
+ 0.10.6
@@ -38,17 +38,25 @@ class Email < ::Credential
38
38
  # Returns the authenticated User instance, or a symbol indicating the reason
39
39
  # why the (potentially valid) password was rejected.
40
40
  def self.authenticate(email)
41
+ credential = with email
42
+ return :invalid unless credential
43
+ user = credential.user
44
+ user.auth_bounce_reason(credential) || user
45
+ end
46
+
47
+ # Locates the credential holding an e-mail address.
48
+ #
49
+ # Returns the User matching the given e-mail, or nil if the e-mail is not
50
+ # associated with any user.
51
+ def self.with(email)
41
52
  # This method is likely to be used to kick off a complex authentication
42
53
  # process, so it makes sense to pre-fetch the user's other credentials.
43
54
  credential = Credentials::Email.where(:name => email).
44
55
  includes(:user => :credentials).first
45
- return :invalid unless credential
46
- user = credential.user
47
- user.auth_bounce_reason(credential) || user
48
56
  end
49
57
 
50
58
  # Forms can only change the e-mail in the credential.
51
59
  attr_accessible :email
52
- end # class Credentials::Email
60
+ end # class Credentials::Email
53
61
 
54
62
  end # namespace Credentials
@@ -0,0 +1,106 @@
1
+ require 'securerandom'
2
+
3
+ # :namespace
4
+ module Credentials
5
+
6
+ # Associates a secret token code with the account.
7
+ #
8
+ # Subclasses of this class are in the tokens namespace.
9
+ class Token < ::Credential
10
+ # The secret token code.
11
+ alias_attribute :code, :name
12
+ # Token names are random, so we can expect they'll be unique across the entire
13
+ # namespace. We need this check to enforce name uniqueness across different
14
+ # token types.
15
+ validates :name, :format => /^[A-Za-z0-9\_\-]+$/, :presence => true,
16
+ :uniqueness => true
17
+
18
+ # Authenticates a user using a secret token code.
19
+ #
20
+ # The token will be spent on successful authentication. One-time tokens are
21
+ # deleted when spent.
22
+ #
23
+ # Returns the authenticated User instance, or a symbol indicating the reason
24
+ # why the (potentially valid) token code was rejected.
25
+ def self.authenticate(code)
26
+ credential = self.with_code code
27
+ credential ? credential.authenticate : :invalid
28
+ end
29
+
30
+ # The token matching a secret code.
31
+ def self.with_code(code)
32
+ # NOTE 1: The where query must be performed off the root type, otherwise
33
+ # Rails will try to guess the right values for the 'type' column,
34
+ # and will sometimes get them wrong.
35
+ # NOTE 2: After using this method, it's likely that the user's other tokens
36
+ # (e.g., email or Facebook OAuth token) will be required, so we
37
+ # pre-fetch them.
38
+ credential = Credential.where(:name => code).
39
+ includes(:user => :credentials).first
40
+
41
+ if credential.is_a? Credentials::Token
42
+ credential
43
+ else
44
+ nil
45
+ end
46
+ end
47
+
48
+ # Authenticates a user using this token.
49
+ #
50
+ # The token will be spent on successful authentication. One-time tokens are
51
+ # deleted when spent.
52
+ #
53
+ # Returns the authenticated User instance, or a symbol indicating the reason
54
+ # why the (potentially valid) token code was rejected.
55
+ def authenticate
56
+ if bounce = user.auth_bounce_reason(self)
57
+ return bounce
58
+ end
59
+ spend
60
+ user
61
+ end
62
+
63
+ # Updates the token's state to reflect that it was used for authentication.
64
+ #
65
+ # Tokens may become invalid after they are spent.
66
+ #
67
+ # Returns the token instance.
68
+ def spend
69
+ self
70
+ end
71
+
72
+ # Creates a new random token for a user.
73
+ #
74
+ # Args:
75
+ # user:: the User who will be authenticated by the token
76
+ # key:: optional data associated with the token
77
+ # klass:: class that will be instantiated (should be a subclass of Token)
78
+ #
79
+ # Returns a newly created and saved token with a random code.
80
+ def self.random_for(user, key = nil, klass = nil)
81
+ klass ||= self
82
+ if key.nil?
83
+ token = self.new(:code => random_code)
84
+ else
85
+ token = self.new(:code => random_code, :key => key)
86
+ end
87
+ user.credentials << token
88
+ token.save!
89
+ token
90
+ end
91
+
92
+ # Generates a random token code.
93
+ def self.random_code
94
+ SecureRandom.urlsafe_base64(32)
95
+ end
96
+
97
+ # Use codes instead of exposing ActiveRecord IDs.
98
+ def to_param
99
+ code
100
+ end
101
+ class <<self
102
+ alias_method :find_by_param, :with_code
103
+ end
104
+ end # class Credentials::Token
105
+
106
+ end # namespace Credentials
@@ -0,0 +1,42 @@
1
+ # :namespace
2
+ module Tokens
3
+
4
+ # A token that verifies the user's ownership of their e-mail address.
5
+ class EmailVerification < OneTime
6
+ # The e-mail address verified by this token.
7
+ #
8
+ # Note that it's useful to keep track of the exact e-mail address that the
9
+ # token vouches for, even if an application only allows a single e-mail per
10
+ # user. Otherwise, a user might be able to change their e-mail address and
11
+ # then use the token to verify the ownership of the wrong address.
12
+ alias_attribute :email, :key
13
+ validates :email, :presence => true
14
+
15
+ # Creates a token with a random code that verifies the given e-mail address.
16
+ def self.random_for(email_credential)
17
+ super email_credential.user, email_credential.email, self
18
+ end
19
+
20
+ # Marks the e-mail associated with the token as verified.
21
+ #
22
+ # Returns the token instance.
23
+ def spend
24
+ self.transaction do
25
+ if credential = email_credential
26
+ credential.verified = true
27
+ credential.save!
28
+ end
29
+ super
30
+ end
31
+ end
32
+
33
+ # The credential whose ownership is verified by this token.
34
+ #
35
+ # This method might return nil if a user is trying to take advantage of a race
36
+ # condition and changes her e-mail address before using the token.
37
+ def email_credential
38
+ user.credentials.find { |c| c.name == email }
39
+ end
40
+ end # class Tokens::EmailVerification
41
+
42
+ end # namespace Tokens
@@ -0,0 +1,16 @@
1
+ # :namespace
2
+ module Tokens
3
+
4
+ # One-time tokens can only be used once to authenticate an account.
5
+ class OneTime < Credentials::Token
6
+ # Updates the token's state to reflect that it was used for authentication.
7
+ #
8
+ # One-time tokens become invalid after they are spent.
9
+ #
10
+ # Returns the token instance.
11
+ def spend
12
+ destroy
13
+ end
14
+ end # class Tokens::OneTime
15
+
16
+ end # namespace Tokens
@@ -0,0 +1,27 @@
1
+ # :namespace
2
+ module Tokens
3
+
4
+ # Lets the user to change their password without knowing the old one.
5
+ class PasswordReset < OneTime
6
+ # Blanks the user's old password, so the new password form won't ask for it.
7
+ #
8
+ # Returns the token instance.
9
+ def spend
10
+ self.transaction do
11
+ if credential = password_credential
12
+ credential.destroy
13
+ end
14
+ super
15
+ end
16
+ end
17
+
18
+ # The credential that is removed by this token.
19
+ #
20
+ # This method might return nil if a user initiates password recovery multiple
21
+ # times.
22
+ def password_credential
23
+ user.credentials.find { |c| c.is_a? Credentials::Password }
24
+ end
25
+ end # class Tokens::PasswordReset
26
+
27
+ 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.10.5"
8
+ s.version = "0.10.6"
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-01-07"
12
+ s.date = "2012-01-11"
13
13
  s.description = "Works with Facebook."
14
14
  s.email = "victor@costan.us"
15
15
  s.extra_rdoc_files = [
@@ -30,6 +30,10 @@ Gem::Specification.new do |s|
30
30
  "app/models/credentials/email.rb",
31
31
  "app/models/credentials/facebook.rb",
32
32
  "app/models/credentials/password.rb",
33
+ "app/models/credentials/token.rb",
34
+ "app/models/tokens/email_verification.rb",
35
+ "app/models/tokens/one_time.rb",
36
+ "app/models/tokens/password_reset.rb",
33
37
  "authpwn_rails.gemspec",
34
38
  "legacy/migrate_09_to_010.rb",
35
39
  "lib/authpwn_rails.rb",
@@ -44,34 +48,55 @@ Gem::Specification.new do |s|
44
48
  "lib/authpwn_rails/generators/templates/session/forbidden.html.erb",
45
49
  "lib/authpwn_rails/generators/templates/session/home.html.erb",
46
50
  "lib/authpwn_rails/generators/templates/session/new.html.erb",
51
+ "lib/authpwn_rails/generators/templates/session/password_change.html.erb",
47
52
  "lib/authpwn_rails/generators/templates/session/welcome.html.erb",
48
53
  "lib/authpwn_rails/generators/templates/session_controller.rb",
49
54
  "lib/authpwn_rails/generators/templates/session_controller_test.rb",
55
+ "lib/authpwn_rails/generators/templates/session_mailer.rb",
56
+ "lib/authpwn_rails/generators/templates/session_mailer/email_verification_email.html.erb",
57
+ "lib/authpwn_rails/generators/templates/session_mailer/email_verification_email.text.erb",
58
+ "lib/authpwn_rails/generators/templates/session_mailer/reset_password_email.html.erb",
59
+ "lib/authpwn_rails/generators/templates/session_mailer/reset_password_email.text.erb",
60
+ "lib/authpwn_rails/generators/templates/session_mailer_test.rb",
50
61
  "lib/authpwn_rails/generators/templates/user.rb",
51
62
  "lib/authpwn_rails/generators/templates/users.yml",
63
+ "lib/authpwn_rails/routes.rb",
52
64
  "lib/authpwn_rails/session.rb",
53
65
  "lib/authpwn_rails/session_controller.rb",
66
+ "lib/authpwn_rails/session_mailer.rb",
54
67
  "lib/authpwn_rails/test_extensions.rb",
55
68
  "lib/authpwn_rails/user_extensions/email_field.rb",
56
69
  "lib/authpwn_rails/user_extensions/facebook_fields.rb",
57
70
  "lib/authpwn_rails/user_extensions/password_field.rb",
58
71
  "lib/authpwn_rails/user_model.rb",
59
72
  "test/cookie_controller_test.rb",
60
- "test/email_credential_test.rb",
61
- "test/email_field_test.rb",
73
+ "test/credentials/email_credential_test.rb",
74
+ "test/credentials/email_verification_token_test.rb",
75
+ "test/credentials/facebook_credential_test.rb",
76
+ "test/credentials/one_time_token_credential_test.rb",
77
+ "test/credentials/password_credential_test.rb",
78
+ "test/credentials/password_reset_token_test.rb",
79
+ "test/credentials/token_crendential_test.rb",
62
80
  "test/facebook_controller_test.rb",
63
- "test/facebook_credential_test.rb",
64
- "test/facebook_fields_test.rb",
81
+ "test/fixtures/bare_session/forbidden.html.erb",
82
+ "test/fixtures/bare_session/home.html.erb",
83
+ "test/fixtures/bare_session/new.html.erb",
84
+ "test/fixtures/bare_session/password_change.html.erb",
85
+ "test/fixtures/bare_session/welcome.html.erb",
86
+ "test/helpers/action_mailer.rb",
65
87
  "test/helpers/application_controller.rb",
66
88
  "test/helpers/autoload_path.rb",
67
89
  "test/helpers/db_setup.rb",
68
90
  "test/helpers/fbgraph.rb",
69
91
  "test/helpers/routes.rb",
70
92
  "test/helpers/view_helpers.rb",
71
- "test/password_credential_test.rb",
72
- "test/password_field_test.rb",
93
+ "test/routes_test.rb",
73
94
  "test/session_controller_api_test.rb",
95
+ "test/session_mailer_api_test.rb",
74
96
  "test/test_helper.rb",
97
+ "test/user_extensions/email_field_test.rb",
98
+ "test/user_extensions/facebook_fields_test.rb",
99
+ "test/user_extensions/password_field_test.rb",
75
100
  "test/user_test.rb"
76
101
  ]
77
102
  s.homepage = "http://github.com/pwnall/authpwn_rails"
@@ -90,7 +115,7 @@ Gem::Specification.new do |s|
90
115
  s.add_development_dependency(%q<flexmock>, ["~> 0.9.0"])
91
116
  s.add_development_dependency(%q<jeweler>, ["~> 1.6.0"])
92
117
  s.add_development_dependency(%q<rcov>, [">= 0"])
93
- s.add_development_dependency(%q<sqlite3>, [">= 1.3.3"])
118
+ s.add_development_dependency(%q<sqlite3>, [">= 1.3.5"])
94
119
  else
95
120
  s.add_dependency(%q<fbgraph_rails>, [">= 0.2.2"])
96
121
  s.add_dependency(%q<rails>, [">= 3.2.0.rc2"])
@@ -98,7 +123,7 @@ Gem::Specification.new do |s|
98
123
  s.add_dependency(%q<flexmock>, ["~> 0.9.0"])
99
124
  s.add_dependency(%q<jeweler>, ["~> 1.6.0"])
100
125
  s.add_dependency(%q<rcov>, [">= 0"])
101
- s.add_dependency(%q<sqlite3>, [">= 1.3.3"])
126
+ s.add_dependency(%q<sqlite3>, [">= 1.3.5"])
102
127
  end
103
128
  else
104
129
  s.add_dependency(%q<fbgraph_rails>, [">= 0.2.2"])
@@ -107,7 +132,7 @@ Gem::Specification.new do |s|
107
132
  s.add_dependency(%q<flexmock>, ["~> 0.9.0"])
108
133
  s.add_dependency(%q<jeweler>, ["~> 1.6.0"])
109
134
  s.add_dependency(%q<rcov>, [">= 0"])
110
- s.add_dependency(%q<sqlite3>, [">= 1.3.3"])
135
+ s.add_dependency(%q<sqlite3>, [">= 1.3.5"])
111
136
  end
112
137
  end
113
138
 
data/lib/authpwn_rails.rb CHANGED
@@ -6,6 +6,7 @@ module Authpwn
6
6
 
7
7
  autoload :CredentialModel, 'authpwn_rails/credential_model.rb'
8
8
  autoload :SessionController, 'authpwn_rails/session_controller.rb'
9
+ autoload :SessionMailer, 'authpwn_rails/session_mailer.rb'
9
10
  autoload :UserModel, 'authpwn_rails/user_model.rb'
10
11
 
11
12
  # Contains extensions to the User model.
@@ -17,6 +18,7 @@ module Authpwn
17
18
  end
18
19
 
19
20
  require 'authpwn_rails/facebook_session.rb'
21
+ require 'authpwn_rails/routes.rb'
20
22
  require 'authpwn_rails/session.rb'
21
23
  require 'authpwn_rails/test_extensions.rb'
22
24
 
@@ -26,7 +26,7 @@ class AllGenerator < Rails::Generators::Base
26
26
  copy_file File.join('session_controller_test.rb'),
27
27
  File.join('test', 'functional', 'session_controller_test.rb')
28
28
 
29
- route "resource :session, :controller => 'session'"
29
+ route "authpwn_session"
30
30
  route "root :to => 'session#show'"
31
31
  end
32
32
 
@@ -37,9 +37,27 @@ class AllGenerator < Rails::Generators::Base
37
37
  File.join('app', 'views', 'session', 'home.html.erb')
38
38
  copy_file File.join('session', 'new.html.erb'),
39
39
  File.join('app', 'views', 'session', 'new.html.erb')
40
+ copy_file File.join('session', 'password_change.html.erb'),
41
+ File.join('app', 'views', 'session', 'password_change.html.erb')
40
42
  copy_file File.join('session', 'welcome.html.erb'),
41
43
  File.join('app', 'views', 'session', 'welcome.html.erb')
42
- end
44
+ end
45
+
46
+ def create_session_mailer
47
+ copy_file 'session_mailer.rb',
48
+ File.join('app', 'mailers', 'session_mailer.rb')
49
+ copy_file File.join('session_mailer_test.rb'),
50
+ File.join('test', 'functional', 'session_mailer_test.rb')
51
+ end
52
+
53
+ def create_session_mailer_views
54
+ copy_file File.join('session_mailer', 'reset_password_email.html.erb'),
55
+ File.join('app', 'views', 'session_mailer',
56
+ 'reset_password_email.html.erb')
57
+ copy_file File.join('session_mailer', 'reset_password_email.text.erb'),
58
+ File.join('app', 'views', 'session_mailer',
59
+ 'reset_password_email.text.erb')
60
+ end
43
61
  end # class Authpwn::AllGenerator
44
62
 
45
63
  end # namespace Authpwn
@@ -32,3 +32,24 @@ john_facebook:
32
32
  type: Credentials::Facebook
33
33
  name: 702659
34
34
  key: 702659|ffffffffffffffffffffffff-702659|ZZZZZZZZZZZZZZZZZZZZZZZZZZZZ
35
+
36
+ jane_token:
37
+ user: jane
38
+ type: Credentials::Token
39
+ name: "6TXe1vv7BgOw0BkJ1hzUKO6G08fLk4sVfJ3wPDZHS-c"
40
+
41
+ john_token:
42
+ user: john
43
+ type: Tokens::OneTime
44
+ name: YZ-Fo8HX6_NyU6lVZXYi6cMDLV5eAgt35UTF5l8bD6A
45
+
46
+ john_email_token:
47
+ user: john
48
+ type: Tokens::EmailVerification
49
+ name: bDSU4tzfjuob79e3R0ykLcOGTBBYvuBWWJ9V06tQrCE
50
+ key: john@gmail.com
51
+
52
+ jane_password_token:
53
+ user: jane
54
+ type: Tokens::PasswordReset
55
+ name: nbMLTKN18tYy9plBAbsrwT6zdE2jZqoKPk6Ze4lHMSQ