Empact-authlogic_rpx 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/.gitignore +1 -0
  2. data/CHANGELOG.rdoc +39 -0
  3. data/Empact-authlogic_rpx.gemspec +100 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.rdoc +747 -0
  6. data/Rakefile +47 -0
  7. data/VERSION +1 -0
  8. data/generators/add_authlogic_rpx_migration/USAGE +18 -0
  9. data/generators/add_authlogic_rpx_migration/add_authlogic_rpx_migration_generator.rb +44 -0
  10. data/generators/add_authlogic_rpx_migration/templates/migration_internal_mapping.rb +34 -0
  11. data/generators/add_authlogic_rpx_migration/templates/migration_no_mapping.rb +29 -0
  12. data/init.rb +1 -0
  13. data/lib/authlogic_rpx.rb +13 -0
  14. data/lib/authlogic_rpx/acts_as_authentic.rb +274 -0
  15. data/lib/authlogic_rpx/helper.rb +44 -0
  16. data/lib/authlogic_rpx/rpx_identifier.rb +5 -0
  17. data/lib/authlogic_rpx/session.rb +241 -0
  18. data/lib/authlogic_rpx/session/validation.rb +30 -0
  19. data/lib/authlogic_rpx/version.rb +51 -0
  20. data/rails/init.rb +1 -0
  21. data/test/fixtures/rpxresponses.yml +20 -0
  22. data/test/fixtures/users.yml +20 -0
  23. data/test/integration/basic_authentication_and_registration_test.rb +73 -0
  24. data/test/integration/internal_mapping/basic_authentication_and_registration_test.rb +3 -0
  25. data/test/integration/internal_mapping/settings_test.rb +10 -0
  26. data/test/integration/no_mapping/basic_authentication_and_registration_test.rb +3 -0
  27. data/test/integration/no_mapping/settings_test.rb +10 -0
  28. data/test/libs/ext_test_unit.rb +30 -0
  29. data/test/libs/mock_rpx_now.rb +34 -0
  30. data/test/libs/rails_trickery.rb +41 -0
  31. data/test/libs/rpxresponse.rb +3 -0
  32. data/test/libs/user.rb +3 -0
  33. data/test/libs/user_session.rb +3 -0
  34. data/test/test_helper.rb +85 -0
  35. data/test/test_internal_mapping_helper.rb +93 -0
  36. data/test/unit/acts_as_authentic_settings_test.rb +42 -0
  37. data/test/unit/session_settings_test.rb +38 -0
  38. data/test/unit/session_validation_test.rb +16 -0
  39. data/test/unit/verify_rpx_mock_test.rb +29 -0
  40. metadata +143 -0
@@ -0,0 +1,44 @@
1
+ module AuthlogicRpx
2
+ module Helper
3
+
4
+ # helper to insert an embedded iframe RPX login
5
+ # takes options hash:
6
+ # * <tt>app_name:</tt> name of the application (will be prepended to RPX domain and used in RPX dialogues)
7
+ # * <tt>return_url:</tt> url for the RPX callback (e.g. user_sessions_url)
8
+ # * <tt>add_rpx:</tt> if true, requests RPX callback to add to current session. Else runs normal authentication process (default)
9
+ #
10
+ # The options hash may include other options as supported by rpx_now (see http://github.com/grosser/rpx_now)
11
+ #
12
+ def rpx_embed(options = {})
13
+ app_name = options.delete( :app_name )
14
+ token_url = build_token_url!( options )
15
+ RPXNow.embed_code(app_name, token_url, options )
16
+ end
17
+
18
+ # helper to insert a link to pop-up RPX login
19
+ # takes options hash:
20
+ # * <tt>link_text:</tt> text to use in the link
21
+ # * <tt>app_name:</tt> name of the application (will be prepended to RPX domain and used in RPX dialogues)
22
+ # * <tt>return_url:</tt> url for the RPX callback (e.g. user_sessions_url)
23
+ # * <tt>add_rpx:</tt> if true, requests RPX callback to add to current session. Else runs normal authentication process (default)
24
+ # * <tt>unobtrusive:</tt> true/false; sets javascript style for link. Default: true
25
+ #
26
+ # The options hash may include other options as supported by rpx_now (see http://github.com/grosser/rpx_now)
27
+ #
28
+ def rpx_popup(options = {})
29
+ options = { :unobtrusive => true, :add_rpx => false }.merge( options )
30
+ app_name = options.delete( :app_name )
31
+ link_text = options.delete( :link_text )
32
+ token_url = build_token_url!( options )
33
+ RPXNow.popup_code( link_text, app_name, token_url, options )
34
+ end
35
+
36
+ private
37
+
38
+ def build_token_url!( options )
39
+ options.delete( :return_url ) + '?' + (
40
+ { :authenticity_token => form_authenticity_token, :add_rpx => options.delete( :add_rpx ) }.collect { |n| "#{n[0]}=#{ u(n[1]) }" if n[1] }
41
+ ).compact.join('&')
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,5 @@
1
+ class RPXIdentifier < ActiveRecord::Base
2
+ validates_presence_of :identifier
3
+ validates_uniqueness_of :identifier
4
+ validates_presence_of :user_id
5
+ end
@@ -0,0 +1,241 @@
1
+ module AuthlogicRpx
2
+ # This module is responsible for adding all of the RPX goodness to the Authlogic::Session::Base class.
3
+ module Session
4
+ # Add a simple rpx_identifier attribute and some validations for the field.
5
+ def self.included(klass)
6
+ klass.class_eval do
7
+ extend Config
8
+ include Methods
9
+ end
10
+ end
11
+
12
+ module Config
13
+
14
+ def find_by_rpx_identifier_method(value = nil)
15
+ rw_config(:find_by_rpx_identifier_method, value, :find_by_rpx_identifier)
16
+ end
17
+ alias_method :find_by_rpx_identifier_method=, :find_by_rpx_identifier_method
18
+
19
+ # Auto Register is enabled by default.
20
+ # Add this in your Session object if you need to disable auto-registration via rpx
21
+ #
22
+ def auto_register(value=true)
23
+ auto_register_value(value)
24
+ end
25
+ def auto_register_value(value=nil)
26
+ rw_config(:auto_register,value,true)
27
+ end
28
+ alias_method :auto_register=,:auto_register
29
+
30
+ # Add this in your Session object to set the RPX API key
31
+ # RPX won't work without the API key. Set it here if not already set in your app configuration.
32
+ #
33
+ def rpx_key(value=nil)
34
+ rpx_key_value(value)
35
+ end
36
+ def rpx_key_value(value=nil)
37
+ if !inheritable_attributes.include?(:rpx_key)
38
+ RPXNow.api_key = value
39
+ end
40
+ rw_config(:rpx_key,value,false)
41
+ end
42
+ alias_method :rpx_key=,:rpx_key
43
+
44
+ # Add this in your Session object to set whether RPX returns extended user info
45
+ # By default, it will not, which is enough to get username, name, email and the rpx identified
46
+ # if you want to map additional information into your user details, you can request extended
47
+ # attributes (though not all providers give them - see the RPX docs)
48
+ #
49
+ def rpx_extended_info(value=true)
50
+ rpx_extended_info_value(value)
51
+ end
52
+ def rpx_extended_info_value(value=nil)
53
+ rw_config(:rpx_extended_info,value,false)
54
+ end
55
+ alias_method :rpx_extended_info=,:rpx_extended_info
56
+
57
+ end
58
+
59
+ module Methods
60
+
61
+ def self.included(klass)
62
+ klass.class_eval do
63
+ attr_accessor :new_registration
64
+ after_persisting :add_rpx_identifier, :if => :adding_rpx_identifier?
65
+ validate :validate_by_rpx, :if => :authenticating_with_rpx?
66
+ validate :validate_user
67
+ end
68
+ end
69
+
70
+ # Determines if the authenticated user is also a new registration.
71
+ # For use in the session controller to help direct the most appropriate action to follow.
72
+ #
73
+ def new_registration?
74
+ new_registration || !new_registration.nil?
75
+ end
76
+
77
+ # True if the login was succeessful, but the logged-in user object is invalid.
78
+ # For use in the session controller to help direct the most appropriate action to follow.
79
+ #
80
+ def registration_incomplete?
81
+ valid? && attempted_record && !attempted_record.valid?
82
+ end
83
+
84
+ # Determines if the authenticated user has a complete registration (no validation errors)
85
+ # For use in the session controller to help direct the most appropriate action to follow.
86
+ #
87
+ def registration_complete?
88
+ attempted_record && attempted_record.valid?
89
+ end
90
+
91
+ private
92
+ # Tests if current request is for RPX authentication
93
+ #
94
+ def authenticating_with_rpx?
95
+ controller.params[:token] && !controller.params[:add_rpx]
96
+ end
97
+
98
+ # hook instance finder method to class
99
+ #
100
+ def find_by_rpx_identifier_method
101
+ self.class.find_by_rpx_identifier_method
102
+ end
103
+
104
+ # Tests if auto_registration is enabled (on by default)
105
+ #
106
+ def auto_register?
107
+ self.class.auto_register_value
108
+ end
109
+
110
+ # Tests if rpx_extended_info is enabled (off by default)
111
+ #
112
+ def rpx_extended_info?
113
+ self.class.rpx_extended_info_value
114
+ end
115
+
116
+ # Tests if current request is the special case of adding RPX to an existing account
117
+ #
118
+ def adding_rpx_identifier?
119
+ controller.params[:token] && controller.params[:add_rpx]
120
+ end
121
+
122
+ # Handles the special case of RPX being added to an existing account.
123
+ # At this point, a session has been established as a result of a "save" on the user model (which indirectly triggers user session validation).
124
+ # We do not directly add the RPX details to the user record here in order to avoid getting
125
+ # into a recursive dance between the session and user models.
126
+ # Rather, it uses the trick of adding the necessary RPX information to the session object,
127
+ # and the user model will pluck these values out before completing its validation step.
128
+ #
129
+ def add_rpx_identifier
130
+ data = RPXNow.user_data(controller.params[:token], :extended=> rpx_extended_info? ) {|raw| raw }
131
+ controller.session['added_rpx_data'] = data if data
132
+ end
133
+
134
+ # the main RPX magic. At this point, a session is being validated and we know RPX identifier
135
+ # has been provided. We'll callback to RPX to verify the token, and authenticate the matching
136
+ # user.
137
+ # If no user is found, and we have auto_register enabled (default) this method will also
138
+ # create the user registration stub.
139
+ #
140
+ # On return to the controller, you can test for new_registration? and registration_complete?
141
+ # to determine the most appropriate action
142
+ #
143
+ def validate_by_rpx
144
+ @rpx_data = RPXNow.user_data(
145
+ controller.params[:token],
146
+ :extended => rpx_extended_info?) { |raw| raw }
147
+
148
+ # If we don't have a valid sign-in, give-up at this point
149
+ if @rpx_data.nil? || @rpx_data['profile'].nil?
150
+ errors.add_to_base("Authentication failed. Please try again.")
151
+ return false
152
+ end
153
+
154
+ rpx_id = @rpx_data['profile']['identifier']
155
+ rpx_provider_name = @rpx_data['profile']['providerName']
156
+ if rpx_id.blank?
157
+ errors.add_to_base("Authentication failed. Please try again.")
158
+ return false
159
+ end
160
+
161
+ self.attempted_record = klass.send(find_by_rpx_identifier_method, rpx_id)
162
+
163
+ # so what do we do if we can't find an existing user matching the RPX authentication...
164
+ if !attempted_record
165
+ if auto_register?
166
+ self.attempted_record = new_rpx_user(controller.params)
167
+ map_rpx_data
168
+
169
+ # save the new user record - without session maintenance else we
170
+ # get caught in a self-referential hell, since both session and
171
+ # user objects invoke each other upon save
172
+ self.new_registration = true
173
+ self.attempted_record.add_rpx_identifier( rpx_id, rpx_provider_name)
174
+ self.attempted_record.save_without_session_maintenance
175
+ else
176
+ errors.add_to_base("We did not find any accounts with that login. Enter your details and create an account.")
177
+ return false
178
+ end
179
+ else
180
+ map_rpx_data_each_login
181
+ end
182
+
183
+ end
184
+
185
+ def validate_user
186
+ errors.add(:user, "is invalid") if attempted_record && !attempted_record.valid?
187
+ end
188
+
189
+ # map_rpx_data maps additional fields from the RPX response into the user object during auto-registration.
190
+ # Override this in your session model to change the field mapping
191
+ # See https://rpxnow.com/docs#profile_data for the definition of available attributes
192
+ #
193
+ # In this procedure, you will be writing to fields of the "self.attempted_record" object, pulling data from the @rpx_data object.
194
+ #
195
+ # WARNING: if you are using auto-registration, any fields you map should NOT have constraints enforced at the database level.
196
+ # authlogic_rpx will optimistically attempt to save the user record during registration, and
197
+ # violating a database constraint will cause the authentication/registration to fail.
198
+ #
199
+ # You can/should enforce any required validations at the model level e.g.
200
+ # validates_uniqueness_of :username, :case_sensitive => false
201
+ # This will allow the auto-registration to proceed, and the user can be given a chance to rectify the validation errors
202
+ # on your user profile page.
203
+ #
204
+ # If it is not acceptable in your application to have user records created with potential validation errors in auto-populated fields, you
205
+ # will need to override map_rpx_data and implement whatever special handling makes sense in your case. For example:
206
+ # - directly check for uniqueness and other validation requirements
207
+ # - automatically "uniquify" fields like username
208
+ # - save conflicting profile information to "pending user review" columns or a seperate table
209
+ #
210
+ def map_rpx_data
211
+ self.attempted_record.send("#{klass.login_field}=", @rpx_data['profile']['preferredUsername'] ) if attempted_record.send(klass.login_field).blank?
212
+ self.attempted_record.send("#{klass.email_field}=", @rpx_data['profile']['email'] ) if attempted_record.send(klass.email_field).blank?
213
+ end
214
+
215
+ # new_rpx_user creates a fresh user in the case of auto-registration, prior to mapping in
216
+ # the rpx data via map_rpx_data. You are passed the current controller params,
217
+ # which you can use to initialize the user.
218
+ #
219
+ # Override this in your session model to change the initialization of auto-registered users.
220
+ #
221
+ def new_rpx_user(params)
222
+ klass.new()
223
+ end
224
+
225
+ # map_rpx_data_each_login provides a hook to allow you to map RPX profile information every time the user
226
+ # logs in.
227
+ # By default, nothing is mapped.
228
+ #
229
+ # This would mainly be used to update relatively volatile information that you are maintaining in the user model (such as profile image url)
230
+ #
231
+ # In this procedure, you will be writing to fields of the "self.attempted_record" object, pulling data from the @rpx_data object.
232
+ #
233
+ #
234
+ def map_rpx_data_each_login
235
+
236
+ end
237
+
238
+ end
239
+
240
+ end
241
+ end
@@ -0,0 +1,30 @@
1
+ module AuthlogicRpx
2
+ module Session # :nodoc:
3
+ # This is different from mainline Session::Base#valid? in that it does not
4
+ # attempt to save_record on the attempted_record
5
+ #
6
+ # In a world without auto-registration the save, which seems to have been a
7
+ # performance optimization (authlogic SHA: 198521b75d26143c43), would be unlikely
8
+ # to produce show-stopper errors, but as RPX-driven auto-registration is
9
+ # far less likely to generate valid objects (for example, they can easily violate uniqueness
10
+ # constraints/validations), it is important not to save to avoid blowing up in these cases.
11
+ module Validation
12
+ def valid?
13
+ errors.clear
14
+ self.attempted_record = nil
15
+
16
+ before_validation
17
+ new_session? ? before_validation_on_create : before_validation_on_update
18
+ validate
19
+ ensure_authentication_attempted
20
+
21
+ if errors.size == 0
22
+ new_session? ? after_validation_on_create : after_validation_on_update
23
+ after_validation
24
+ end
25
+
26
+ errors.size == 0
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,51 @@
1
+ module AuthlogicRpx
2
+ # A class for describing the current version of a library. The version
3
+ # consists of three parts: the +major+ number, the +minor+ number, and the
4
+ # +tiny+ (or +patch+) number.
5
+ class Version
6
+ include Comparable
7
+
8
+ # A convenience method for instantiating a new Version instance with the
9
+ # given +major+, +minor+, and +tiny+ components.
10
+ def self.[](major, minor, tiny)
11
+ new(major, minor, tiny)
12
+ end
13
+
14
+ attr_reader :major, :minor, :tiny
15
+
16
+ # Create a new Version object with the given components.
17
+ def initialize(major, minor, tiny)
18
+ @major, @minor, @tiny = major, minor, tiny
19
+ end
20
+
21
+ # Compare this version to the given +version+ object.
22
+ def <=>(version)
23
+ to_i <=> version.to_i
24
+ end
25
+
26
+ # Converts this version object to a string, where each of the three
27
+ # version components are joined by the '.' character. E.g., 2.0.0.
28
+ def to_s
29
+ @to_s ||= [@major, @minor, @tiny].join(".")
30
+ end
31
+
32
+ # Converts this version to a canonical integer that may be compared
33
+ # against other version objects.
34
+ def to_i
35
+ @to_i ||= @major * 1_000_000 + @minor * 1_000 + @tiny
36
+ end
37
+
38
+ def to_a
39
+ [@major, @minor, @tiny]
40
+ end
41
+
42
+ MAJOR = 1
43
+ MINOR = 1
44
+ TINY = 1
45
+
46
+ # The current version as a Version instance
47
+ CURRENT = new(MAJOR, MINOR, TINY)
48
+ # The current version as a String
49
+ STRING = CURRENT.to_s
50
+ end
51
+ end
data/rails/init.rb ADDED
@@ -0,0 +1 @@
1
+ require "authlogic_rpx"
@@ -0,0 +1,20 @@
1
+ valid_rpx_auth_user_one:
2
+ identifier : http://provider.one/valid_rpx_auth_user_one
3
+ provider_name: provider.one
4
+ username: valid_rpx_auth_user_one
5
+ verified_email: valid_rpx_auth_user_one@provider.one
6
+ display_name: valid rpx auth user one
7
+
8
+ valid_rpx_auth_user_two:
9
+ identifier : http://provider.one/valid_rpx_auth_user_two
10
+ provider_name: provider.one
11
+ username: valid_rpx_auth_user_two
12
+ verified_email: valid_rpx_auth_user_two@provider.one
13
+ display_name: valid rpx auth user two
14
+
15
+ unregistered_rpx_auth_user_one:
16
+ identifier : http://provider.one/unregistered_rpx_auth_user_one
17
+ provider_name: provider.one
18
+ username: unregistered_rpx_auth_user_one
19
+ verified_email: unregistered_rpx_auth_user_one@provider.one
20
+ display_name: unregistered rpx auth user one
@@ -0,0 +1,20 @@
1
+ valid_rpx_auth_user_one:
2
+ login: valid_rpx_auth_user_one
3
+ email: valid_rpx_auth_user_one@provider.one
4
+ persistence_token: 6cde0674657a8a313ce952df979de2830309aa4c11ca65805dd00bfdc65dbcc2f5e36718660a1d2e68c1a08c276d996763985d2f06fd3d076eb7bc4d97b1e317
5
+ single_access_token: <%= Authlogic::Random.friendly_token %>
6
+ perishable_token: <%= Authlogic::Random.friendly_token %>
7
+ rpx_identifier : http://provider.one/valid_rpx_auth_user_one
8
+
9
+ valid_rpx_auth_user_two:
10
+ login: valid_rpx_auth_user_two
11
+ email: valid_rpx_auth_user_two@provider.one
12
+ persistence_token: 6cde0674657a8a313ce952df979de2830309aa4c11ca65805dd00bfdc65dbcc2f5e36718660a1d2e68c1a08c276d996763985d2f06fd3d076eb7bc4d97b1e317
13
+ single_access_token: <%= Authlogic::Random.friendly_token %>
14
+ perishable_token: <%= Authlogic::Random.friendly_token %>
15
+ rpx_identifier : http://provider.one/valid_rpx_auth_user_two
16
+
17
+ invalid_rpx_auth_user_one:
18
+ login: invalid_rpx_auth_user_one
19
+
20
+
@@ -0,0 +1,73 @@
1
+ # requires test_helper to be loaded first
2
+
3
+ class BasicAuthenticationAndRegistrationTest < ActiveSupport::TestCase
4
+
5
+ must "authenticate valid existing user" do
6
+ test_user = users(:valid_rpx_auth_user_one)
7
+ controller.params[:token] = test_user.login
8
+ session = UserSession.new
9
+ assert_true session.save, "should be a valid session"
10
+ assert_false session.new_registration?, "should not be a new registration"
11
+ assert_true session.registration_complete?, "registration should be complete"
12
+ assert_equal test_user, session.record
13
+ end
14
+
15
+ must "do not authenticate invalidate non-existent user" do
16
+ controller.params[:token] = ''
17
+ session = UserSession.new
18
+ assert_false session.save, "should not be a valid session"
19
+ end
20
+
21
+ must "mark an invalaid auto-registered user as registration_incomplete" do
22
+ # enforce Authlogic settings required for test
23
+ UserSession.auto_register true
24
+ User.account_merge_enabled false
25
+ User.account_mapping_mode :none
26
+
27
+ # get response template. set the controller token (used by RPX mock to match mock response)
28
+ test_user = rpxresponses(:unregistered_rpx_auth_user_one)
29
+ controller.params[:token] = test_user.username
30
+ # emulate constraint violation
31
+
32
+ session = UserSession.new
33
+
34
+ stub.instance_of(User).save_without_session_maintenance.with_any_args {|*args|
35
+ raise "Kaboom" unless args.empty? || args.first
36
+ }
37
+ stub.instance_of(User).valid? { false }
38
+ assert_true session.registration_incomplete?
39
+ end
40
+
41
+ must "auto-register an unregistered user" do
42
+ # enforce Authlogic settings required for test
43
+ UserSession.auto_register true
44
+ User.account_merge_enabled false
45
+ User.account_mapping_mode :none
46
+
47
+ # get response template. set the controller token (used by RPX mock to match mock response)
48
+ test_user = rpxresponses(:unregistered_rpx_auth_user_one)
49
+ controller.params[:token] = test_user.username
50
+
51
+ session = UserSession.new
52
+ assert_false session.registration_incomplete?
53
+ assert_true session.save, "should be a valid session"
54
+ assert_true session.new_registration?, "should be a new registration"
55
+ assert_true session.registration_complete?, "registration should be complete"
56
+ end
57
+
58
+ must "auto-register disabled for an unregistered user" do
59
+ # enforce Authlogic settings required for test
60
+ UserSession.auto_register false
61
+ User.account_merge_enabled false
62
+ User.account_mapping_mode :none
63
+
64
+ # get response template. set the controller token (used by RPX mock to match mock response)
65
+ test_user = rpxresponses(:unregistered_rpx_auth_user_one)
66
+ controller.params[:token] = test_user.username
67
+
68
+ session = UserSession.new
69
+ assert_false session.registration_incomplete?
70
+ assert_false session.save, "should not be a valid session"
71
+ end
72
+
73
+ end