authpwn_rails 0.9.6 → 0.10.0

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 (47) hide show
  1. data/.travis.yml +6 -0
  2. data/Gemfile +3 -2
  3. data/Gemfile.lock +38 -36
  4. data/README.rdoc +6 -11
  5. data/VERSION +1 -1
  6. data/authpwn_rails.gemspec +30 -22
  7. data/lib/authpwn_rails.rb +2 -2
  8. data/lib/authpwn_rails/credential_model.rb +38 -0
  9. data/lib/authpwn_rails/credentials.rb +10 -0
  10. data/lib/authpwn_rails/credentials/email.rb +30 -0
  11. data/lib/authpwn_rails/credentials/facebook.rb +77 -0
  12. data/lib/authpwn_rails/credentials/password.rb +63 -0
  13. data/lib/authpwn_rails/engine.rb +5 -7
  14. data/lib/authpwn_rails/facebook_session.rb +5 -5
  15. data/lib/authpwn_rails/generators/{session_generator.rb → all_generator.rb} +28 -9
  16. data/lib/authpwn_rails/generators/templates/001_create_users.rb +3 -11
  17. data/lib/authpwn_rails/generators/templates/002_create_credentials.rb +19 -0
  18. data/lib/authpwn_rails/generators/templates/credential.rb +16 -0
  19. data/lib/authpwn_rails/generators/templates/credentials.yml +34 -0
  20. data/lib/authpwn_rails/generators/templates/session/forbidden.html.erb +2 -2
  21. data/lib/authpwn_rails/generators/templates/session/home.html.erb +1 -1
  22. data/lib/authpwn_rails/generators/templates/session/new.html.erb +6 -6
  23. data/lib/authpwn_rails/generators/templates/session_controller.rb +1 -1
  24. data/lib/authpwn_rails/generators/templates/session_controller_test.rb +2 -2
  25. data/lib/authpwn_rails/generators/templates/user.rb +2 -2
  26. data/lib/authpwn_rails/generators/templates/users.yml +5 -8
  27. data/lib/authpwn_rails/session.rb +7 -7
  28. data/lib/authpwn_rails/session_controller.rb +15 -13
  29. data/lib/authpwn_rails/test_extensions.rb +6 -6
  30. data/lib/authpwn_rails/user_model.rb +23 -92
  31. data/test/email_credential_test.rb +50 -0
  32. data/test/facebook_controller_test.rb +7 -2
  33. data/test/facebook_credential_test.rb +74 -0
  34. data/test/helpers/db_setup.rb +4 -4
  35. data/test/helpers/fbgraph.rb +6 -2
  36. data/test/password_credential_test.rb +67 -0
  37. data/test/session_controller_api_test.rb +12 -12
  38. data/test/test_helper.rb +1 -0
  39. data/test/user_test.rb +11 -100
  40. metadata +41 -25
  41. data/lib/authpwn_rails/facebook_token_model.rb +0 -66
  42. data/lib/authpwn_rails/generators/facebook_generator.rb +0 -18
  43. data/lib/authpwn_rails/generators/templates/002_create_facebook_tokens.rb +0 -15
  44. data/lib/authpwn_rails/generators/templates/facebook_token.rb +0 -6
  45. data/lib/authpwn_rails/generators/templates/facebook_tokens.yml +0 -10
  46. data/lib/authpwn_rails/generators/users_generator.rb +0 -16
  47. data/test/facebook_token_test.rb +0 -28
@@ -1,24 +1,24 @@
1
1
  # :nodoc: namespace
2
- module AuthpwnRails
2
+ module Authpwn
3
3
 
4
4
  # Included in test cases.
5
5
  module TestExtensions
6
6
  # Sets the authenticated user in the test session.
7
7
  def set_session_current_user(user)
8
- request.session[:current_user_pid] = user ? user.to_param : nil
8
+ request.session[:user_exuid] = user ? user.to_param : nil
9
9
  end
10
10
 
11
11
  # The authenticated user in the test session.
12
12
  def session_current_user
13
- return nil unless user_param = request.session[:current_user_pid]
13
+ return nil unless user_param = request.session[:user_exuid]
14
14
  User.find_by_param user_param
15
15
  end
16
- end # module AuthpwnRails::TestExtensions
16
+ end # module Authpwn::TestExtensions
17
17
 
18
- end # namespace AuthpwnRails
18
+ end # namespace Authpwn
19
19
 
20
20
 
21
21
  # :nodoc: extend Test::Unit
22
22
  class ActionController::TestCase
23
- include AuthpwnRails::TestExtensions
23
+ include Authpwn::TestExtensions
24
24
  end
@@ -2,119 +2,50 @@ require 'active_model'
2
2
  require 'active_support'
3
3
 
4
4
  # :nodoc: namespace
5
- module AuthpwnRails
5
+ module Authpwn
6
6
 
7
7
  # Included by the model class that represents users.
8
8
  #
9
- # Right now, some parts of the codebase assume the model will be named User.
9
+ # Parts of the codebase assume the model will be named User.
10
10
  module UserModel
11
11
  extend ActiveSupport::Concern
12
-
12
+
13
13
  included do
14
- # E-mail address identifying the user account.
15
- validates :email, :format => /^[A-Za-z0-9.+_]+@[^@]*\.(\w+)$/,
16
- :presence => true, :length => 1..128, :uniqueness => true
17
-
18
- # Hash of e-mail address of the user account.
19
- validates :email_hash, :length => 64..64, :allow_nil => false
20
-
21
- # Random string preventing dictionary attacks on the password database.
22
- validates :password_salt, :length => { :in => 1..16, :allow_nil => true }
14
+ # Externally-visible user ID.
15
+ #
16
+ # This is decoupled from "id" column to avoid leaking information about
17
+ # the application's usage.
18
+ validates :exuid, :presence => true, :length => 1..32, :uniqueness => true
23
19
 
24
- # SHA-256 of (salt + password).
25
- validates :password_hash, :length => { :in => 64..64, :allow_nil => true }
26
-
27
- # Virtual attribute: the user's password.
28
- attr_reader :password
29
- validates :password, :confirmation => true
30
-
31
- # Virtual attribute: confirmation for the user's password.
32
- attr_accessor :password_confirmation
33
- validates_confirmation_of :password
20
+ # Credentials used to authenticate the user.
21
+ has_many :credentials, :dependent => :destroy, :inverse_of => :user
34
22
 
35
- # Facebook token.
36
- has_one :facebook_token, :dependent => :destroy, :inverse_of => :user
23
+ before_validation :set_default_exuid, :on => :create
37
24
  end
38
25
 
39
- # Class methods on models that include AuthpwnRails::UserModel.
26
+ # Class methods on models that include Authpwn::UserModel.
40
27
  module ClassMethods
41
28
  # Queries the database using the value returned by User#to_param.
42
29
  #
43
30
  # Returns nil if no matching User exists.
44
31
  def find_by_param(param)
45
- where(:email_hash => param).first
46
- end
47
-
48
- # The authenticated user or nil.
49
- def find_by_email_and_password(email, password)
50
- user = where(:email => email).first
51
- (user && user.password_matches?(password)) ? user : nil
52
- end
53
-
54
- # Computes a password hash from a raw password and a salt.
55
- def hash_password(password, salt)
56
- Digest::SHA2.hexdigest(password + salt)
57
- end
58
-
59
- # Generates a random salt value.
60
- def random_salt
61
- [(0...12).map { |i| 1 + rand(255) }.pack('C*')].pack('m').strip
62
- end
63
-
64
- # Fills out a new user's information based on a Facebook access token.
65
- def create_with_facebook_token(token)
66
- self.create! :email => "#{token.external_uid}@graph.facebook.com"
32
+ where(:exuid => param).first
67
33
  end
68
-
69
- # The user that owns a given Facebook OAuth2 token.
70
- #
71
- # A new user will be created if the token doesn't belong to any user. This
72
- # is the case for a new visitor.
73
- def for_facebook_token(access_token)
74
- FacebookToken.for(access_token).user
75
- end
76
- end # module AuthpwnRails::UserModel::ClassMethods
34
+ end # module Authpwn::UserModel::ClassMethods
77
35
 
78
- # Included in models that include AuthpwnRails::UserModel.
36
+ # Included in models that include Authpwn::UserModel.
79
37
  module InstanceMethods
80
- # Resets the virtual password attributes.
81
- def reset_password
82
- @password = @password_confirmation = nil
83
- end
84
-
85
- # Compares the given password against the user's stored password.
86
- #
87
- # Returns +true+ for a match, +false+ otherwise.
88
- def password_matches?(passwd)
89
- password_hash == self.class.hash_password(passwd, password_salt)
90
- end
91
-
92
- # Password virtual attribute.
93
- def password=(new_password)
94
- @password = new_password
95
- self.password_salt = self.class.random_salt
96
- self.password_hash = new_password &&
97
- self.class.hash_password(new_password, password_salt)
98
- end
99
-
100
38
  # Use e-mails instead of exposing ActiveRecord IDs.
101
39
  def to_param
102
- email_hash
103
- end
104
-
105
- # :nodoc: overwrites
106
- def email=(new_email)
107
- super
108
- self.email_hash = new_email && Digest::SHA2.hexdigest(new_email)
40
+ exuid
109
41
  end
110
42
 
111
- # Do not expose password and ActiveRecord IDs in JSON representation.
112
- def as_json(options = {})
113
- options ||= {}
114
- super(options.merge(:except => [:password_salt, :password_hash, :id]))
43
+ # :nodoc: sets exuid to a (hopefully) unique value before validations occur.
44
+ def set_default_exuid
45
+ self.exuid ||= (Time.now.to_f * 1_000_000).to_i
115
46
  end
116
- end # module AuthpwnRails::UserModel::InstanceMethods
117
-
118
- end # namespace AuthpwnRails::UserModel
47
+ end # module Authpwn::UserModel::InstanceMethods
48
+
49
+ end # namespace Authpwn::UserModel
119
50
 
120
- end # namespace AuthpwnRails
51
+ end # namespace Authpwn
@@ -0,0 +1,50 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ class EmailCredentialTest < ActiveSupport::TestCase
4
+ def setup
5
+ @credential = Credentials::Email.new :email => 'dvdjohn@mit.edu',
6
+ :user => users(:bill)
7
+ end
8
+
9
+ test 'setup' do
10
+ assert @credential.valid?
11
+ end
12
+
13
+ test 'verified required' do
14
+ @credential.verified = ''
15
+ assert !@credential.valid?
16
+ end
17
+
18
+ test 'user presence' do
19
+ @credential.user = nil
20
+ assert !@credential.valid?
21
+ end
22
+
23
+ test 'email presence' do
24
+ @credential.email = nil
25
+ assert !@credential.valid?
26
+ end
27
+
28
+ test 'email length' do
29
+ @credential.email = 'abcde' * 25 + '@mit.edu'
30
+ assert !@credential.valid?, 'Overly long email'
31
+ end
32
+
33
+ test 'email format' do
34
+ ['cos tan@gmail.com', 'costan@x@mit.edu'].each do |email|
35
+ @credential.email = email
36
+ assert !@credential.valid?, "Bad email format - #{email}"
37
+ end
38
+ end
39
+
40
+ test 'email uniqueness' do
41
+ @credential.email = credentials(:john_email).email
42
+ assert !@credential.valid?
43
+ end
44
+
45
+ test 'User#email_credential' do
46
+ assert_equal credentials(:john_email), users(:john).email_credential
47
+ assert_equal credentials(:jane_email), users(:jane).email_credential
48
+ assert_nil users(:bill).email_credential
49
+ end
50
+ end
@@ -28,7 +28,10 @@ class FacebookControllerTest < ActionController::TestCase
28
28
  end
29
29
 
30
30
  test "facebook token for existing user" do
31
- set_session_current_facebook_token facebook_tokens(:john).access_token
31
+ flexmock(Credentials::Facebook).should_receive(:uid_from_token).
32
+ with(credentials(:john_facebook).key).
33
+ and_return(credentials(:john_facebook).facebook_uid)
34
+ set_session_current_facebook_token credentials(:john_facebook).key
32
35
  get :show, {}
33
36
  assert_response :success
34
37
  assert_equal @user, assigns(:current_user)
@@ -36,8 +39,10 @@ class FacebookControllerTest < ActionController::TestCase
36
39
 
37
40
  test "new facebook token" do
38
41
  set_session_current_facebook_token @new_token
42
+ flexmock(Credentials::Facebook).should_receive(:uid_from_token).
43
+ with(@new_token).and_return('12345678')
39
44
  get :show, {}
40
45
  assert_response :success
41
- assert !(@user == assigns(:current_user))
46
+ assert_not_equal @user, assigns(:current_user)
42
47
  end
43
48
  end
@@ -0,0 +1,74 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ class FacebookCredentialTest < ActiveSupport::TestCase
4
+ def setup
5
+ @code = 'AAAEj8jKX2a8BAA4kNheRhOs6SlECVcZCE9o5pPKMytOjjoiNAoZBGZAwuL4KrrxXWesfJRhzDZCJiqrcQG3UdjRRNtyMJQMZD'
6
+ @credential = Credentials::Facebook.new :facebook_uid => '1181310542',
7
+ :key => 'AAAEj8jKX2a8BAOBMZCjxBe4dw7cRoD1JVxUgZAtB6ozJlR4Viazh6OAYcHB5kZAtUwgjpDy7a54ZA1DObLmBT9X99CLWYOj5Stqx8bHwnE7EzyBS1WxY',
8
+ :user => users(:bill)
9
+ end
10
+
11
+ test 'setup' do
12
+ assert @credential.valid?
13
+ end
14
+
15
+ test 'key required' do
16
+ @credential.key = nil
17
+ assert !@credential.valid?
18
+ end
19
+
20
+ test 'user presence' do
21
+ @credential.user = nil
22
+ assert !@credential.valid?
23
+ end
24
+
25
+ test 'user uniqueness' do
26
+ @credential.user = users(:john)
27
+ assert !@credential.valid?
28
+ end
29
+
30
+ test 'facebook_uid uniqueness' do
31
+ @credential.facebook_uid = credentials(:jane_facebook).facebook_uid
32
+ assert !@credential.valid?
33
+ end
34
+
35
+ test "uid_from_token" do
36
+ assert_equal '1011950666', Credentials::Facebook.uid_from_token(@code)
37
+ end
38
+
39
+ test "for with existing access token" do
40
+ flexmock(Credentials::Facebook).should_receive(:uid_from_token).with(@code).
41
+ and_return(credentials(:jane_facebook).facebook_uid)
42
+
43
+ assert_equal credentials(:jane_facebook), Credentials::Facebook.for(@code),
44
+ 'Wrong token'
45
+ assert_equal @code, credentials(:jane_facebook).reload.key,
46
+ 'Token not refreshed'
47
+ end
48
+
49
+ test "for with new access token" do
50
+ credential = nil
51
+ flexmock(Credentials::Facebook).should_receive(:uid_from_token).
52
+ with(@credential.key).and_return('123456789')
53
+ assert_difference 'Credentials::Facebook.count', 1 do
54
+ credential = Credentials::Facebook.for @credential.key
55
+ end
56
+ assert_equal '123456789', credential.facebook_uid
57
+ assert_equal @credential.key, credential.key
58
+ assert !credential.new_record?, 'New credential not saved'
59
+ assert !credential.user.new_record?, "New credential's user not saved"
60
+ end
61
+
62
+ test 'User#facebook_credential' do
63
+ user = users(:john)
64
+ assert_equal credentials(:john_facebook), user.facebook_credential
65
+ end
66
+
67
+ test 'User#for_facebook_token' do
68
+ flexmock(Credentials::Facebook).should_receive(:uid_from_token).
69
+ with(credentials(:john_facebook).key).
70
+ and_return(credentials(:john_facebook).facebook_uid)
71
+ assert_equal users(:john),
72
+ User.for_facebook_token(credentials(:john_facebook).key)
73
+ end
74
+ end
@@ -4,12 +4,12 @@ ActiveRecord::Base.configurations = true
4
4
 
5
5
  ActiveRecord::Migration.verbose = false
6
6
  require 'authpwn_rails/generators/templates/001_create_users.rb'
7
- CreateUsers.up
8
- require 'authpwn_rails/generators/templates/002_create_facebook_tokens.rb'
9
- CreateFacebookTokens.up
7
+ CreateUsers.migrate :up
8
+ require 'authpwn_rails/generators/templates/002_create_credentials.rb'
9
+ CreateCredentials.migrate :up
10
10
 
11
- require 'authpwn_rails/generators/templates/facebook_token.rb'
12
11
  require 'authpwn_rails/generators/templates/user.rb'
12
+ require 'authpwn_rails/generators/templates/credential.rb'
13
13
 
14
14
  # :nodoc: open TestCase to setup fixtures
15
15
  class ActiveSupport::TestCase
@@ -1,6 +1,10 @@
1
1
  # :nodoc: stub FBGraphRails.config because it depends on Rails.root
2
2
  module FBGraphRails
3
3
  def self.config
4
- { 'id' => '12345', 'secret' => 'awesome', 'scope' => []}
4
+ {
5
+ 'id' => '320998114580911',
6
+ 'secret' => '7ded389d3c226e1f5d363b2df695be2f',
7
+ 'scope' => []
8
+ }
5
9
  end
6
- end
10
+ end
@@ -0,0 +1,67 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ class PasswordCredentialTest < ActiveSupport::TestCase
4
+ def setup
5
+ @credential = Credentials::Password.new :password => 'awesome',
6
+ :password_confirmation => 'awesome', :user => users(:bill)
7
+ end
8
+
9
+ test 'setup' do
10
+ assert @credential.valid?
11
+ end
12
+
13
+ test 'key not required' do
14
+ @credential.key = nil
15
+ assert @credential.valid?
16
+ end
17
+
18
+ test 'user presence' do
19
+ @credential.user = nil
20
+ assert !@credential.valid?
21
+ end
22
+
23
+ test 'user uniqueness' do
24
+ @credential.user = users(:john)
25
+ assert !@credential.valid?
26
+ end
27
+
28
+ test 'password confirmation' do
29
+ @credential.password_confirmation = 'not awesome'
30
+ assert !@credential.valid?
31
+ end
32
+
33
+ test 'password required' do
34
+ @credential.password = @credential.password_confirmation = nil
35
+ assert !@credential.valid?
36
+ end
37
+
38
+ test 'authenticate' do
39
+ assert_equal true, @credential.authenticate('awesome')
40
+ assert_equal false, @credential.authenticate('not awesome'),
41
+ 'Bogus password'
42
+ assert_equal false, @credential.authenticate('password'),
43
+ "Another user's password"
44
+ end
45
+
46
+ test 'authenticate_email' do
47
+ assert_equal users(:john),
48
+ Credentials::Password.authenticate_email('john@gmail.com', 'password')
49
+ assert_equal nil,
50
+ Credentials::Password.authenticate_email('john@gmail.com', 'pa55w0rd'),
51
+ "Jane's password on John's account"
52
+ assert_equal users(:jane),
53
+ Credentials::Password.authenticate_email('jane@gmail.com', 'pa55w0rd')
54
+ assert_equal nil,
55
+ Credentials::Password.authenticate_email('jane@gmail.com', 'password'),
56
+ "John's password on Jane's account"
57
+ assert_equal nil,
58
+ Credentials::Password.authenticate_email('john@gmail.com', 'awesome'),
59
+ 'Bogus password'
60
+ end
61
+
62
+ test 'User#password_credential' do
63
+ assert_equal credentials(:john_password), users(:john).password_credential
64
+ assert_equal credentials(:jane_password), users(:jane).password_credential
65
+ assert_nil users(:bill).password_credential
66
+ end
67
+ end
@@ -11,6 +11,7 @@ class SessionControllerApiTest < ActionController::TestCase
11
11
 
12
12
  setup do
13
13
  @user = users(:john)
14
+ @email_credential = credentials(:john_email)
14
15
  end
15
16
 
16
17
  test "show renders welcome without a user" do
@@ -42,7 +43,7 @@ class SessionControllerApiTest < ActionController::TestCase
42
43
  get :show, :format => 'json'
43
44
  assert_response :ok
44
45
  data = ActiveSupport::JSON.decode response.body
45
- assert_equal @user.email, data['user']['email']
46
+ assert_equal @user.exuid, data['user']['exuid']
46
47
  assert_equal session[:_csrf_token], data['csrf']
47
48
  assert_equal @user, assigns(:user), 'home controller method not called'
48
49
  end
@@ -57,11 +58,10 @@ class SessionControllerApiTest < ActionController::TestCase
57
58
  get :new
58
59
  assert_template :new
59
60
  assert_nil assigns(:current_user), 'current_user should not be set'
60
- assert assigns(:user).new_record?, 'user instance variable should be fresh'
61
61
 
62
62
  assert_select 'form' do
63
- assert_select 'input#user_email'
64
- assert_select 'input#user_password'
63
+ assert_select 'input#email'
64
+ assert_select 'input#password'
65
65
  assert_select 'input[type=submit]'
66
66
  end
67
67
  end
@@ -77,18 +77,18 @@ class SessionControllerApiTest < ActionController::TestCase
77
77
  end
78
78
 
79
79
  test "create logs in with good account details" do
80
- post :create, :user => { :email => @user.email, :password => 'password' }
80
+ post :create, :email => @email_credential.email, :password => 'password'
81
81
  assert_redirected_to session_url
82
82
  assert_equal @user, assigns(:current_user), 'instance variable'
83
83
  assert_equal @user, session_current_user, 'session'
84
84
  end
85
85
 
86
86
  test "create by json logs in with good account details" do
87
- post :create, :user => { :email => @user.email, :password => 'password' },
87
+ post :create, :email => @email_credential.email, :password => 'password',
88
88
  :format => 'json'
89
89
  assert_response :ok
90
90
  data = ActiveSupport::JSON.decode response.body
91
- assert_equal @user.email, data['user']['email']
91
+ assert_equal @user.exuid, data['user']['exuid']
92
92
  assert_equal session[:_csrf_token], data['csrf']
93
93
  assert_equal @user, assigns(:current_user), 'instance variable'
94
94
  assert_equal @user, session_current_user, 'session'
@@ -96,13 +96,13 @@ class SessionControllerApiTest < ActionController::TestCase
96
96
 
97
97
  test "create redirects properly with good account details" do
98
98
  url = 'http://authpwn.redirect.url'
99
- post :create, :user => { :email => @user.email, :password => 'password' },
99
+ post :create, :email => @email_credential.email, :password => 'password',
100
100
  :redirect_url => url
101
101
  assert_redirected_to url
102
102
  end
103
103
 
104
104
  test "create does not log in with bad password" do
105
- post :create, :user => { :email => @user.email, :password => 'fail' }
105
+ post :create, :email => @email_credential.email, :password => 'fail'
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'
@@ -110,7 +110,7 @@ class SessionControllerApiTest < ActionController::TestCase
110
110
  end
111
111
 
112
112
  test "create by json does not log in with bad password" do
113
- post :create, :user => { :email => @user.email, :password => 'fail' },
113
+ post :create, :email => @email_credential.email, :password => 'fail',
114
114
  :format => 'json'
115
115
  assert_response :ok
116
116
  data = ActiveSupport::JSON.decode response.body
@@ -121,7 +121,7 @@ class SessionControllerApiTest < ActionController::TestCase
121
121
 
122
122
  test "create maintains redirect_url for bad logins" do
123
123
  url = 'http://authpwn.redirect.url'
124
- post :create, :user => { :email => @user.email, :password => 'fail' },
124
+ post :create, :email => @email_credential.email, :password => 'fail',
125
125
  :redirect_url => url
126
126
  assert_redirected_to new_session_url
127
127
  assert_not_nil flash[:notice]
@@ -129,7 +129,7 @@ class SessionControllerApiTest < ActionController::TestCase
129
129
  end
130
130
 
131
131
  test "create does not log in with bad e-mail" do
132
- post :create, :user => { :email => 'nobody@gmail.com', :password => 'no' }
132
+ post :create, :email => 'nobody@gmail.com', :password => 'no'
133
133
  assert_redirected_to new_session_url
134
134
  assert_nil assigns(:current_user), 'instance variable'
135
135
  assert_nil session_current_user, 'session'