authpwn_rails 0.10.3 → 0.10.4

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.
data/Gemfile.lock CHANGED
@@ -60,13 +60,13 @@ GEM
60
60
  bundler (~> 1.0)
61
61
  git (>= 1.2.5)
62
62
  rake
63
- json (1.6.1)
63
+ json (1.6.2)
64
64
  mail (2.3.0)
65
65
  i18n (>= 0.4.0)
66
66
  mime-types (~> 1.16)
67
67
  treetop (~> 1.4.8)
68
68
  mime-types (1.17.2)
69
- multi_json (1.0.3)
69
+ multi_json (1.0.4)
70
70
  multipart-post (1.1.4)
71
71
  oauth2 (0.5.1)
72
72
  faraday (~> 0.7.4)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.10.3
1
+ 0.10.4
@@ -10,13 +10,41 @@ class Email < ::Credential
10
10
  :message => 'This e-mail address is already claimed by an account' }
11
11
 
12
12
  # '1' if the user proved ownership of the e-mail address.
13
- alias_attribute :verified, :key
14
- validates :verified, :presence => true
13
+ validates :key, :presence => true, :inclusion => { :in => ['0', '1'] }
15
14
 
16
15
  before_validation :set_verified_to_false, :on => :create
17
16
  # :nodoc: by default, e-mail addresses are not verified
18
17
  def set_verified_to_false
19
- self.verified ||= '0' if self.key.nil?
18
+ self.key ||= '0' if self.key.nil?
19
+ end
20
+
21
+ # True if the e-mail has been verified via a token URL.
22
+ def verified?
23
+ key == '1'
24
+ end
25
+
26
+ # True if the e-mail has been verified via a token URL.
27
+ def verified=(new_verified_value)
28
+ self.key = new_verified_value ? '1' : '0'
29
+ new_verified_value ? true : false
30
+ end
31
+
32
+ # Locates the user that owns an e-mail address, for authentication purposes.
33
+ #
34
+ # Presenting the correct e-mail is almost never sufficient for authentication
35
+ # purposes. This method will most likely used to kick off an authentication
36
+ # process, such as in Password#authenticate_email.
37
+ #
38
+ # Returns the authenticated User instance, or a symbol indicating the reason
39
+ # why the (potentially valid) password was rejected.
40
+ def self.authenticate(email)
41
+ # This method is likely to be used to kick off a complex authentication
42
+ # process, so it makes sense to pre-fetch the user's other credentials.
43
+ credential = Credentials::Email.where(:name => email).
44
+ includes(:user => :credentials).first
45
+ return :invalid unless credential
46
+ user = credential.user
47
+ user.auth_bounce_reason(credential) || user
20
48
  end
21
49
 
22
50
  # Forms can only change the e-mail in the credential.
@@ -14,14 +14,23 @@ class Password < ::Credential
14
14
  # A user can have a single password
15
15
  validates :user_id, :uniqueness => true
16
16
 
17
- # Compares the given password against the user's stored password.
17
+ # Compares a plain-text password against the password hash in this credential.
18
18
  #
19
19
  # Returns +true+ for a match, +false+ otherwise.
20
- def authenticate(password)
20
+ def check_password(password)
21
21
  return false unless key
22
22
  key == self.class.hash_password(password, key.split('|', 2).first)
23
23
  end
24
24
 
25
+ # Compares a plain-text password against the password hash in this credential.
26
+ #
27
+ # Returns the authenticated User instance, or a symbol indicating the reason
28
+ # why the (potentially valid) password was rejected.
29
+ def authenticate(password)
30
+ return :invalid unless check_password(password)
31
+ user.auth_bounce_reason(self) || user
32
+ end
33
+
25
34
  # Password virtual attribute.
26
35
  def password=(new_password)
27
36
  @password = new_password
@@ -34,14 +43,16 @@ class Password < ::Credential
34
43
  @password = @password_confirmation = nil
35
44
  end
36
45
 
37
- # The authenticated user or nil.
46
+ # Authenticates a user given an e-mail / password pair.
47
+ #
48
+ # Returns the authenticated User instance, or a symbol indicating the reason
49
+ # why the (potentially valid) credential was rejected.
38
50
  def self.authenticate_email(email, password)
39
- email_cred = Credentials::Email.where(:name => email).
40
- includes(:user => :credentials).first
41
- return nil unless email_cred
42
- credential = email_cred.user.credentials.
43
- find { |c| c.kind_of? Credentials::Password }
44
- credential.authenticate(password) ? email_cred.user : nil
51
+ user = Credentials::Email.authenticate email
52
+ return user if user.is_a? Symbol
53
+
54
+ credential = user.credentials.find { |c| c.kind_of? Credentials::Password }
55
+ credential ? credential.authenticate(password) : :invalid
45
56
  end
46
57
 
47
58
  # Computes a password hash from a raw password and a salt.
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "authpwn_rails"
8
- s.version = "0.10.3"
8
+ s.version = "0.10.4"
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 = "2011-11-25"
12
+ s.date = "2011-11-30"
13
13
  s.description = "Works with Facebook."
14
14
  s.email = "victor@costan.us"
15
15
  s.extra_rdoc_files = [
@@ -79,4 +79,5 @@ class DropFacebookTokens < ActiveRecord::Migration
79
79
  drop_table :facebook_tokens
80
80
  end
81
81
  end
82
+ DropFacebookTokens.migrate :up
82
83
  reload!
@@ -15,6 +15,7 @@ class Engine < Rails::Engine
15
15
 
16
16
  RSpec.configure do |c|
17
17
  c.include Authpwn::TestExtensions
18
+ c.include Authpwn::ControllerTestExtensions
18
19
  end
19
20
  rescue LoadError
20
21
  # No RSpec, no extensions.
@@ -16,6 +16,16 @@ class SessionController < ApplicationController
16
16
  end
17
17
  private :home
18
18
 
19
+ # The notification text displayed when a session authentication fails.
20
+ def bounce_notice_text(reason)
21
+ case reason
22
+ when :invalid
23
+ 'Invalid e-mail or password'
24
+ when :blocked
25
+ 'Account blocked. Please verify your e-mail address'
26
+ end
27
+ end
28
+
19
29
  # You shouldn't extend the session controller, so you can benefit from future
20
30
  # features, like Facebook / Twitter / OpenID integration. But, if you must,
21
31
  # you can do it here.
@@ -54,8 +54,8 @@ module SessionController
54
54
  def create
55
55
  @redirect_url = params[:redirect_url] || session_url
56
56
  @email = params[:email]
57
- self.current_user = Credentials::Password.authenticate_email(@email,
58
- params[:password])
57
+ auth = Credentials::Password.authenticate_email @email, params[:password]
58
+ self.current_user = auth unless auth.kind_of? Symbol
59
59
 
60
60
  respond_to do |format|
61
61
  if current_user
@@ -69,12 +69,12 @@ module SessionController
69
69
  :csrf => form_authenticity_token }
70
70
  end
71
71
  else
72
- notice = 'Invalid e-mail or password'
72
+ notice = bounce_notice_text auth
73
73
  format.html do
74
74
  redirect_to new_session_url, :flash => { :notice => notice,
75
75
  :auth_redirect_url => @redirect_url }
76
76
  end
77
- format.json { render :json => { :error => notice} }
77
+ format.json { render :json => { :error => auth, :text => notice } }
78
78
  end
79
79
  end
80
80
  end
@@ -97,6 +97,16 @@ module SessionController
97
97
  def welcome
98
98
  end
99
99
  private :welcome
100
+
101
+ # Hook for customizing the bounce notification text.
102
+ def bounce_notice_text(reason)
103
+ case reason
104
+ when :invalid
105
+ 'Invalid e-mail or password'
106
+ when :blocked
107
+ 'Account blocked. Please verify your e-mail address'
108
+ end
109
+ end
100
110
  end # module Authpwn::SessionController::InstanceMethods
101
111
 
102
112
  end # module Authpwn::SessionController
@@ -1,8 +1,39 @@
1
1
  # :nodoc: namespace
2
2
  module Authpwn
3
3
 
4
- # Included in test cases.
4
+ # Included in all test cases.
5
5
  module TestExtensions
6
+ # Stubs User#auth_bounce_reason to block a given credential.
7
+ #
8
+ # The default implementation of User#auth_bounce_reason always returns nil.
9
+ # Your application's implementation might differ. Either way, the method is
10
+ # replaced for the duration of the block, such that it returns :block if
11
+ # the credential matches the given argument, and nil otherwise.
12
+ def with_blocked_credential(blocked_credential, reason = :blocked, &block)
13
+ # Stub a method in all User instances for this test only.
14
+ # flexmock.new_instances doesn't work because ActiveRecord doesn't use new
15
+ # to instantiate records.
16
+ ::User.class_eval do
17
+ alias_method :_auth_bounce_reason_wbc_stub, :auth_bounce_reason
18
+ define_method :auth_bounce_reason do |credential|
19
+ credential == blocked_credential ? reason : nil
20
+ end
21
+ end
22
+
23
+ begin
24
+ yield
25
+ ensure
26
+ ::User.class_eval do
27
+ undef_method :auth_bounce_reason
28
+ alias_method :auth_bounce_reason, :_auth_bounce_reason_wbc_stub
29
+ undef_method :_auth_bounce_reason_wbc_stub
30
+ end
31
+ end
32
+ end
33
+ end # module Authpwn::TestExtensions
34
+
35
+ # Included in controller test cases.
36
+ module ControllerTestExtensions
6
37
  # Sets the authenticated user in the test session.
7
38
  def set_session_current_user(user)
8
39
  request.session[:user_exuid] = user ? user.to_param : nil
@@ -13,12 +44,16 @@ module TestExtensions
13
44
  return nil unless user_param = request.session[:user_exuid]
14
45
  User.find_by_param user_param
15
46
  end
16
- end # module Authpwn::TestExtensions
47
+ end # module Authpwn::ControllerTestExtensions
17
48
 
18
49
  end # namespace Authpwn
19
50
 
51
+ # :nodoc: extend Test::Unit
52
+ class ActiveSupport::TestCase
53
+ include Authpwn::TestExtensions
54
+ end
20
55
 
21
56
  # :nodoc: extend Test::Unit
22
57
  class ActionController::TestCase
23
- include Authpwn::TestExtensions
58
+ include Authpwn::ControllerTestExtensions
24
59
  end
@@ -42,6 +42,14 @@ module UserModel
42
42
 
43
43
  # Included in models that include Authpwn::UserModel.
44
44
  module InstanceMethods
45
+ # Checks if a credential is acceptable for authenticating a user.
46
+ #
47
+ # Returns nil if the credential is acceptable, or a String containing a
48
+ # user-visible reason why the credential is not acceptable.
49
+ def auth_bounce_reason(crdential)
50
+ nil
51
+ end
52
+
45
53
  # Use e-mails instead of exposing ActiveRecord IDs.
46
54
  def to_param
47
55
  exuid
@@ -10,11 +10,28 @@ class EmailCredentialTest < ActiveSupport::TestCase
10
10
  assert @credential.valid?
11
11
  end
12
12
 
13
- test 'verified required' do
14
- @credential.verified = ''
13
+ test 'key required' do
14
+ @credential.key = ''
15
+ assert !@credential.valid?
16
+ end
17
+
18
+ test 'key cannot be some random string' do
19
+ @credential.key = 'xoxo'
15
20
  assert !@credential.valid?
16
21
  end
17
22
 
23
+ test 'verified set to true' do
24
+ @credential.verified = true
25
+ assert_equal '1', @credential.key, 'key'
26
+ assert_equal true, @credential.verified?, 'verified?'
27
+ end
28
+
29
+ test 'verified set to false' do
30
+ @credential.verified = false
31
+ assert_equal '0', @credential.key, 'key'
32
+ assert_equal false, @credential.verified?, 'verified?'
33
+ end
34
+
18
35
  test 'user presence' do
19
36
  @credential.user = nil
20
37
  assert !@credential.valid?
@@ -41,4 +58,19 @@ class EmailCredentialTest < ActiveSupport::TestCase
41
58
  @credential.email = credentials(:john_email).email
42
59
  assert !@credential.valid?
43
60
  end
61
+
62
+ test 'authenticate' do
63
+ assert_equal users(:john), Credentials::Email.authenticate('john@gmail.com')
64
+ assert_equal users(:jane), Credentials::Email.authenticate('jane@gmail.com')
65
+ assert_equal :invalid, Credentials::Email.authenticate('bill@gmail.com')
66
+ end
67
+
68
+ test 'authenticate calls User#auth_bounce_reason' do
69
+ with_blocked_credential credentials(:john_email), :reason do
70
+ assert_equal :reason, Credentials::Email.authenticate('john@gmail.com')
71
+ assert_equal users(:jane),
72
+ Credentials::Email.authenticate('jane@gmail.com')
73
+ assert_equal :invalid, Credentials::Email.authenticate('bill@gmail.com')
74
+ end
75
+ end
44
76
  end
@@ -36,27 +36,45 @@ class PasswordCredentialTest < ActiveSupport::TestCase
36
36
  assert !@credential.valid?
37
37
  end
38
38
 
39
- test 'authenticate' do
40
- assert_equal true, @credential.authenticate('awesome')
41
- assert_equal false, @credential.authenticate('not awesome'),
39
+ test 'check_password' do
40
+ assert_equal true, @credential.check_password('awesome')
41
+ assert_equal false, @credential.check_password('not awesome'),
42
42
  'Bogus password'
43
- assert_equal false, @credential.authenticate('password'),
43
+ assert_equal false, @credential.check_password('password'),
44
44
  "Another user's password"
45
45
  end
46
+
47
+ test 'authenticate' do
48
+ assert_equal users(:bill), @credential.authenticate('awesome')
49
+ assert_equal :invalid, @credential.authenticate('not awesome')
50
+ end
51
+
52
+ test 'authenticate calls User#auth_bounce_reason' do
53
+ user = @credential.user
54
+ flexmock(user).should_receive(:auth_bounce_reason).and_return(:reason)
55
+ assert_equal :reason, @credential.authenticate('awesome')
56
+ assert_equal :invalid, @credential.authenticate('not awesome')
57
+ end
46
58
 
47
59
  test 'authenticate_email' do
48
60
  assert_equal users(:john),
49
61
  Credentials::Password.authenticate_email('john@gmail.com', 'password')
50
- assert_equal nil,
62
+ assert_equal :invalid,
51
63
  Credentials::Password.authenticate_email('john@gmail.com', 'pa55w0rd'),
52
64
  "Jane's password on John's account"
53
65
  assert_equal users(:jane),
54
66
  Credentials::Password.authenticate_email('jane@gmail.com', 'pa55w0rd')
55
- assert_equal nil,
67
+ assert_equal :invalid,
56
68
  Credentials::Password.authenticate_email('jane@gmail.com', 'password'),
57
69
  "John's password on Jane's account"
58
- assert_equal nil,
70
+ assert_equal :invalid,
59
71
  Credentials::Password.authenticate_email('john@gmail.com', 'awesome'),
60
72
  'Bogus password'
73
+ assert_equal :invalid,
74
+ Credentials::Password.authenticate_email('bill@gmail.com', 'pa55w0rd'),
75
+ 'Password authentication on account without password credential'
76
+ assert_equal :invalid,
77
+ Credentials::Password.authenticate_email('none@gmail.com', 'pa55w0rd'),
78
+ 'Bogus e-mail'
61
79
  end
62
80
  end
@@ -106,7 +106,17 @@ class SessionControllerApiTest < ActionController::TestCase
106
106
  assert_redirected_to new_session_url
107
107
  assert_nil assigns(:current_user), 'instance variable'
108
108
  assert_nil session_current_user, 'session'
109
- assert_not_nil flash[:notice]
109
+ assert_match(/Invalid/, flash[:notice])
110
+ end
111
+
112
+ test "create does not log in blocked accounts" do
113
+ with_blocked_credential @email_credential do
114
+ post :create, :email => @email_credential.email, :password => 'password'
115
+ end
116
+ assert_redirected_to new_session_url
117
+ assert_nil assigns(:current_user), 'instance variable'
118
+ assert_nil session_current_user, 'session'
119
+ assert_match(/ blocked/, flash[:notice])
110
120
  end
111
121
 
112
122
  test "create by json does not log in with bad password" do
@@ -114,11 +124,25 @@ class SessionControllerApiTest < ActionController::TestCase
114
124
  :format => 'json'
115
125
  assert_response :ok
116
126
  data = ActiveSupport::JSON.decode response.body
117
- assert_match(/invalid/i , data['error'])
127
+ assert_equal 'invalid', data['error']
128
+ assert_match(/invalid/i , data['text'])
118
129
  assert_nil assigns(:current_user), 'instance variable'
119
130
  assert_nil session_current_user, 'session'
120
131
  end
121
132
 
133
+ test "create by json does not log in blocked accounts" do
134
+ with_blocked_credential @email_credential do
135
+ post :create, :email => @email_credential.email, :password => 'password',
136
+ :format => 'json'
137
+ end
138
+ assert_response :ok
139
+ data = ActiveSupport::JSON.decode response.body
140
+ assert_equal 'blocked', data['error']
141
+ assert_match(/blocked/i , data['text'])
142
+ assert_nil assigns(:current_user), 'instance variable'
143
+ assert_nil session_current_user, 'session'
144
+ end
145
+
122
146
  test "create maintains redirect_url for bad logins" do
123
147
  url = 'http://authpwn.redirect.url'
124
148
  post :create, :email => @email_credential.email, :password => 'fail',
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: authpwn_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.3
4
+ version: 0.10.4
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-11-25 00:00:00.000000000Z
12
+ date: 2011-11-30 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: fbgraph_rails
16
- requirement: &24789920 !ruby/object:Gem::Requirement
16
+ requirement: &14255560 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 0.2.2
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *24789920
24
+ version_requirements: *14255560
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rails
27
- requirement: &24770680 !ruby/object:Gem::Requirement
27
+ requirement: &14254720 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: 3.1.3
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *24770680
35
+ version_requirements: *14254720
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: bundler
38
- requirement: &24769460 !ruby/object:Gem::Requirement
38
+ requirement: &14253940 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ~>
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: 1.0.0
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *24769460
46
+ version_requirements: *14253940
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: flexmock
49
- requirement: &24767220 !ruby/object:Gem::Requirement
49
+ requirement: &14252840 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ~>
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: 0.9.0
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *24767220
57
+ version_requirements: *14252840
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: jeweler
60
- requirement: &24766540 !ruby/object:Gem::Requirement
60
+ requirement: &14251980 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ~>
@@ -65,10 +65,10 @@ dependencies:
65
65
  version: 1.6.0
66
66
  type: :development
67
67
  prerelease: false
68
- version_requirements: *24766540
68
+ version_requirements: *14251980
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rcov
71
- requirement: &24765740 !ruby/object:Gem::Requirement
71
+ requirement: &14248280 !ruby/object:Gem::Requirement
72
72
  none: false
73
73
  requirements:
74
74
  - - ! '>='
@@ -76,10 +76,10 @@ dependencies:
76
76
  version: '0'
77
77
  type: :development
78
78
  prerelease: false
79
- version_requirements: *24765740
79
+ version_requirements: *14248280
80
80
  - !ruby/object:Gem::Dependency
81
81
  name: sqlite3
82
- requirement: &24765080 !ruby/object:Gem::Requirement
82
+ requirement: &14247460 !ruby/object:Gem::Requirement
83
83
  none: false
84
84
  requirements:
85
85
  - - ! '>='
@@ -87,7 +87,7 @@ dependencies:
87
87
  version: 1.3.3
88
88
  type: :development
89
89
  prerelease: false
90
- version_requirements: *24765080
90
+ version_requirements: *14247460
91
91
  description: Works with Facebook.
92
92
  email: victor@costan.us
93
93
  executables: []
@@ -167,7 +167,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
167
167
  version: '0'
168
168
  segments:
169
169
  - 0
170
- hash: -300984823103934206
170
+ hash: -3462448452980002093
171
171
  required_rubygems_version: !ruby/object:Gem::Requirement
172
172
  none: false
173
173
  requirements: