ruby-openid2 3.0.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 (59) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +136 -0
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/CONTRIBUTING.md +54 -0
  6. data/LICENSE.txt +210 -0
  7. data/README.md +81 -0
  8. data/SECURITY.md +15 -0
  9. data/lib/hmac/hmac.rb +110 -0
  10. data/lib/hmac/sha1.rb +11 -0
  11. data/lib/hmac/sha2.rb +25 -0
  12. data/lib/openid/association.rb +246 -0
  13. data/lib/openid/consumer/associationmanager.rb +354 -0
  14. data/lib/openid/consumer/checkid_request.rb +179 -0
  15. data/lib/openid/consumer/discovery.rb +516 -0
  16. data/lib/openid/consumer/discovery_manager.rb +144 -0
  17. data/lib/openid/consumer/html_parse.rb +142 -0
  18. data/lib/openid/consumer/idres.rb +513 -0
  19. data/lib/openid/consumer/responses.rb +147 -0
  20. data/lib/openid/consumer/session.rb +36 -0
  21. data/lib/openid/consumer.rb +406 -0
  22. data/lib/openid/cryptutil.rb +112 -0
  23. data/lib/openid/dh.rb +84 -0
  24. data/lib/openid/extension.rb +38 -0
  25. data/lib/openid/extensions/ax.rb +552 -0
  26. data/lib/openid/extensions/oauth.rb +88 -0
  27. data/lib/openid/extensions/pape.rb +170 -0
  28. data/lib/openid/extensions/sreg.rb +268 -0
  29. data/lib/openid/extensions/ui.rb +49 -0
  30. data/lib/openid/fetchers.rb +277 -0
  31. data/lib/openid/kvform.rb +113 -0
  32. data/lib/openid/kvpost.rb +62 -0
  33. data/lib/openid/message.rb +555 -0
  34. data/lib/openid/protocolerror.rb +7 -0
  35. data/lib/openid/server.rb +1571 -0
  36. data/lib/openid/store/filesystem.rb +260 -0
  37. data/lib/openid/store/interface.rb +73 -0
  38. data/lib/openid/store/memcache.rb +109 -0
  39. data/lib/openid/store/memory.rb +79 -0
  40. data/lib/openid/store/nonce.rb +72 -0
  41. data/lib/openid/trustroot.rb +597 -0
  42. data/lib/openid/urinorm.rb +72 -0
  43. data/lib/openid/util.rb +119 -0
  44. data/lib/openid/version.rb +5 -0
  45. data/lib/openid/yadis/accept.rb +141 -0
  46. data/lib/openid/yadis/constants.rb +16 -0
  47. data/lib/openid/yadis/discovery.rb +151 -0
  48. data/lib/openid/yadis/filters.rb +192 -0
  49. data/lib/openid/yadis/htmltokenizer.rb +290 -0
  50. data/lib/openid/yadis/parsehtml.rb +50 -0
  51. data/lib/openid/yadis/services.rb +44 -0
  52. data/lib/openid/yadis/xrds.rb +160 -0
  53. data/lib/openid/yadis/xri.rb +86 -0
  54. data/lib/openid/yadis/xrires.rb +87 -0
  55. data/lib/openid.rb +27 -0
  56. data/lib/ruby-openid.rb +1 -0
  57. data.tar.gz.sig +0 -0
  58. metadata +331 -0
  59. metadata.gz.sig +0 -0
@@ -0,0 +1,142 @@
1
+ require_relative "../yadis/htmltokenizer"
2
+
3
+ module OpenID
4
+ # Stuff to remove before we start looking for tags
5
+ REMOVED_RE = %r{
6
+ # Comments
7
+ <!--.*?-->
8
+
9
+ # CDATA blocks
10
+ | <!\[CDATA\[.*?\]\]>
11
+
12
+ # script blocks
13
+ | <script\b
14
+
15
+ # make sure script is not an XML namespace
16
+ (?!:)
17
+
18
+ [^>]*>.*?</script>
19
+
20
+ }mix
21
+
22
+ def self.openid_unescape(s)
23
+ s.gsub("&amp;", "&").gsub("&lt;", "<").gsub("&gt;", ">").gsub("&quot;", '"')
24
+ end
25
+
26
+ def self.unescape_hash(h)
27
+ newh = {}
28
+ h.map do |k, v|
29
+ newh[k] = openid_unescape(v)
30
+ end
31
+ newh
32
+ end
33
+
34
+ def self.parse_link_attrs(html)
35
+ begin
36
+ stripped = html.gsub(REMOVED_RE, "")
37
+ rescue ArgumentError
38
+ begin
39
+ stripped = html.encode("UTF-8", "binary", invalid: :replace, undef: :replace, replace: "").gsub(
40
+ REMOVED_RE, ""
41
+ )
42
+ rescue Encoding::UndefinedConversionError, Encoding::ConverterNotFoundError
43
+ # needed for a problem in JRuby where it can't handle the conversion.
44
+ # see details here: https://github.com/jruby/jruby/issues/829
45
+ stripped = html.encode("UTF-8", "ASCII", invalid: :replace, undef: :replace, replace: "").gsub(
46
+ REMOVED_RE, ""
47
+ )
48
+ end
49
+ end
50
+ parser = HTMLTokenizer.new(stripped)
51
+
52
+ links = []
53
+ # to keep track of whether or not we are in the head element
54
+ in_head = false
55
+ in_html = false
56
+ saw_head = false
57
+
58
+ begin
59
+ while el = parser.getTag(
60
+ "head",
61
+ "/head",
62
+ "link",
63
+ "body",
64
+ "/body",
65
+ "html",
66
+ "/html",
67
+ )
68
+
69
+ # we are leaving head or have reached body, so we bail
70
+ return links if ["/head", "body", "/body", "/html"].member?(el.tag_name)
71
+
72
+ # enforce html > head > link
73
+ in_html = true if el.tag_name == "html"
74
+ next unless in_html
75
+
76
+ if el.tag_name == "head"
77
+ if saw_head
78
+ return links # only allow one head
79
+ end
80
+
81
+ saw_head = true
82
+ in_head = true unless el.to_s[-2] == 47 # tag ends with a /: a short tag
83
+ end
84
+ next unless in_head
85
+
86
+ return links if el.tag_name == "html"
87
+
88
+ links << unescape_hash(el.attr_hash) if el.tag_name == "link"
89
+
90
+ end
91
+ rescue Exception # just stop parsing if there's an error
92
+ end
93
+ links
94
+ end
95
+
96
+ def self.rel_matches(rel_attr, target_rel)
97
+ # Does this target_rel appear in the rel_str?
98
+ # XXX: TESTME
99
+ rels = rel_attr.strip.split
100
+ rels.each do |rel|
101
+ rel = rel.downcase
102
+ return true if rel == target_rel
103
+ end
104
+
105
+ false
106
+ end
107
+
108
+ def self.link_has_rel(link_attrs, target_rel)
109
+ # Does this link have target_rel as a relationship?
110
+
111
+ # XXX: TESTME
112
+ rel_attr = link_attrs["rel"]
113
+ (rel_attr and rel_matches(rel_attr, target_rel))
114
+ end
115
+
116
+ def self.find_links_rel(link_attrs_list, target_rel)
117
+ # Filter the list of link attributes on whether it has target_rel
118
+ # as a relationship.
119
+
120
+ # XXX: TESTME
121
+ matches_target = ->(attrs) { link_has_rel(attrs, target_rel) }
122
+ result = []
123
+
124
+ link_attrs_list.each do |item|
125
+ result << item if matches_target.call(item)
126
+ end
127
+
128
+ result
129
+ end
130
+
131
+ def self.find_first_href(link_attrs_list, target_rel)
132
+ # Return the value of the href attribute for the first link tag in
133
+ # the list that has target_rel as a relationship.
134
+
135
+ # XXX: TESTME
136
+ matches = find_links_rel(link_attrs_list, target_rel)
137
+ return if !matches or matches.empty?
138
+
139
+ first = matches[0]
140
+ first["href"]
141
+ end
142
+ end
@@ -0,0 +1,513 @@
1
+ require_relative "../message"
2
+ require_relative "../protocolerror"
3
+ require_relative "../kvpost"
4
+ require_relative "../urinorm"
5
+ require_relative "discovery"
6
+
7
+ module OpenID
8
+ class TypeURIMismatch < ProtocolError
9
+ attr_reader :type_uri, :endpoint
10
+
11
+ def initialize(type_uri, endpoint)
12
+ @type_uri = type_uri
13
+ @endpoint = endpoint
14
+ end
15
+ end
16
+
17
+ class Consumer
18
+ @openid1_return_to_nonce_name = "rp_nonce"
19
+ @openid1_return_to_claimed_id_name = "openid1_claimed_id"
20
+
21
+ # Set the name of the query parameter that this library will use
22
+ # to thread a nonce through an OpenID 1 transaction. It will be
23
+ # appended to the return_to URL.
24
+ class << self
25
+ attr_accessor :openid1_return_to_nonce_name
26
+ end
27
+
28
+ # Set the name of the query parameter that this library will use
29
+ # to thread the requested URL through an OpenID 1 transaction (for
30
+ # use when verifying discovered information). It will be appended
31
+ # to the return_to URL.
32
+ class << self
33
+ attr_accessor :openid1_return_to_claimed_id_name
34
+ end
35
+
36
+ # Handles an openid.mode=id_res response. This object is
37
+ # instantiated and used by the Consumer.
38
+ class IdResHandler
39
+ attr_reader :endpoint, :message
40
+
41
+ def initialize(message, current_url, store = nil, endpoint = nil)
42
+ @store = store # Fer the nonce and invalidate_handle
43
+ @message = message
44
+ @endpoint = endpoint
45
+ @current_url = current_url
46
+ @signed_list = nil
47
+
48
+ # Start the verification process
49
+ id_res
50
+ end
51
+
52
+ def signed_fields
53
+ signed_list.map { |x| "openid." + x }
54
+ end
55
+
56
+ protected
57
+
58
+ # This method will raise ProtocolError unless the request is a
59
+ # valid id_res response. Once it has been verified, the methods
60
+ # 'endpoint', 'message', and 'signed_fields' contain the
61
+ # verified information.
62
+ def id_res
63
+ check_for_fields
64
+ check_signature
65
+ check_nonce
66
+ verify_return_to
67
+ verify_discovery_results
68
+ end
69
+
70
+ def server_url
71
+ @endpoint.nil? ? nil : @endpoint.server_url
72
+ end
73
+
74
+ def openid_namespace
75
+ @message.get_openid_namespace
76
+ end
77
+
78
+ def fetch(field, default = NO_DEFAULT)
79
+ @message.get_arg(OPENID_NS, field, default)
80
+ end
81
+
82
+ def signed_list
83
+ if @signed_list.nil?
84
+ signed_list_str = fetch("signed", nil)
85
+ raise ProtocolError, "Response missing signed list" if signed_list_str.nil?
86
+
87
+ @signed_list = signed_list_str.split(",", -1)
88
+ end
89
+ @signed_list
90
+ end
91
+
92
+ def check_for_fields
93
+ # XXX: if a field is missing, we should not have to explicitly
94
+ # check that it's present, just make sure that the fields are
95
+ # actually being used by the rest of the code in
96
+ # tests. Although, which fields are signed does need to be
97
+ # checked somewhere.
98
+ basic_fields = %w[return_to assoc_handle sig signed]
99
+ basic_sig_fields = %w[return_to identity]
100
+
101
+ case openid_namespace
102
+ when OPENID2_NS
103
+ require_fields = basic_fields + ["op_endpoint"]
104
+ require_sigs = basic_sig_fields +
105
+ %w[response_nonce claimed_id assoc_handle op_endpoint]
106
+ when OPENID1_NS, OPENID11_NS
107
+ require_fields = basic_fields + ["identity"]
108
+ require_sigs = basic_sig_fields
109
+ else
110
+ raise "check_for_fields doesn't know about " \
111
+ "namespace #{openid_namespace.inspect}"
112
+ end
113
+
114
+ require_fields.each do |field|
115
+ raise ProtocolError, "Missing required field #{field}" unless @message.has_key?(OPENID_NS, field)
116
+ end
117
+
118
+ require_sigs.each do |field|
119
+ # Field is present and not in signed list
120
+ if @message.has_key?(OPENID_NS, field) && !signed_list.member?(field)
121
+ raise ProtocolError, "#{field.inspect} not signed"
122
+ end
123
+ end
124
+ end
125
+
126
+ def verify_return_to
127
+ begin
128
+ msg_return_to = URI.parse(URINorm.urinorm(fetch("return_to")))
129
+ rescue URI::InvalidURIError
130
+ raise ProtocolError, ("return_to is not a valid URI")
131
+ end
132
+
133
+ verify_return_to_args(msg_return_to)
134
+ return if @current_url.nil?
135
+
136
+ verify_return_to_base(msg_return_to)
137
+ end
138
+
139
+ def verify_return_to_args(msg_return_to)
140
+ return_to_parsed_query = {}
141
+ unless msg_return_to.query.nil?
142
+ CGI.parse(msg_return_to.query).each_pair do |k, vs|
143
+ return_to_parsed_query[k] = vs[0]
144
+ end
145
+ end
146
+ query = @message.to_post_args
147
+ return_to_parsed_query.each_pair do |rt_key, rt_val|
148
+ msg_val = query[rt_key]
149
+ if msg_val.nil? && !rt_val.nil?
150
+ raise ProtocolError, "Message missing return_to argument '#{rt_key}'"
151
+ elsif msg_val != rt_val
152
+ raise ProtocolError, "Parameter '#{rt_key}' value " \
153
+ "#{msg_val.inspect} does not match " \
154
+ "return_to's value #{rt_val.inspect}"
155
+ end
156
+ end
157
+ @message.get_args(BARE_NS).each_pair do |bare_key, bare_val|
158
+ rt_val = return_to_parsed_query[bare_key]
159
+ unless return_to_parsed_query.has_key?(bare_key)
160
+ # This may be caused by your web framework throwing extra
161
+ # entries in to your parameters hash that were not GET or
162
+ # POST parameters. For example, Rails has been known to
163
+ # add "controller" and "action" keys; another server adds
164
+ # at least a "format" key.
165
+ raise ProtocolError, "Unexpected parameter (not on return_to): " \
166
+ "'#{bare_key}'=#{rt_val.inspect})"
167
+ end
168
+ next unless rt_val != bare_val
169
+
170
+ raise ProtocolError, "Parameter '#{bare_key}' value " \
171
+ "#{bare_val.inspect} does not match " \
172
+ "return_to's value #{rt_val.inspect}"
173
+ end
174
+ end
175
+
176
+ def verify_return_to_base(msg_return_to)
177
+ begin
178
+ app_parsed = URI.parse(URINorm.urinorm(@current_url))
179
+ rescue URI::InvalidURIError
180
+ raise ProtocolError, "current_url is not a valid URI: #{@current_url}"
181
+ end
182
+
183
+ %i[scheme host port path].each do |meth|
184
+ raise ProtocolError, "return_to #{meth} does not match" if msg_return_to.send(meth) != app_parsed.send(meth)
185
+ end
186
+ end
187
+
188
+ # Raises ProtocolError if the signature is bad
189
+ def check_signature
190
+ # ----------------------------------------------------------------------
191
+ # The server url must be defined within the endpoint instance for the
192
+ # OpenID2 namespace in order for the signature check to complete
193
+ # successfully.
194
+ #
195
+ # This fix corrects issue #125 - Unable to complete OpenID login
196
+ # with ruby-openid 2.9.0/2.9.1
197
+ # ---------------------------------------------------------------------
198
+ set_endpoint_flag = false
199
+ if @endpoint.nil? && openid_namespace == OPENID2_NS
200
+ @endpoint = OpenIDServiceEndpoint.new
201
+ @endpoint.server_url = fetch("op_endpoint")
202
+ set_endpoint_flag = true
203
+ end
204
+
205
+ assoc = if @store.nil?
206
+ nil
207
+ else
208
+ @store.get_association(server_url, fetch("assoc_handle"))
209
+ end
210
+
211
+ if assoc.nil?
212
+ check_auth
213
+ elsif assoc.expires_in <= 0
214
+ raise ProtocolError, "Association with #{server_url} expired"
215
+ # XXX: It might be a good idea sometimes to re-start the
216
+ # authentication with a new association. Doing it
217
+ # automatically opens the possibility for
218
+ # denial-of-service by a server that just returns expired
219
+ # associations (or really short-lived associations)
220
+ elsif !assoc.check_message_signature(@message)
221
+ raise ProtocolError, "Bad signature in response from #{server_url}"
222
+ end
223
+ @endpoint = nil if set_endpoint_flag # Clear endpoint if we defined it.
224
+ end
225
+
226
+ def check_auth
227
+ Util.log("Using 'check_authentication' with #{server_url}")
228
+ begin
229
+ request = create_check_auth_request
230
+ rescue Message::KeyNotFound => e
231
+ raise ProtocolError, "Could not generate 'check_authentication' " \
232
+ "request: #{e.message}"
233
+ end
234
+
235
+ response = OpenID.make_kv_post(request, server_url)
236
+
237
+ process_check_auth_response(response)
238
+ end
239
+
240
+ def create_check_auth_request
241
+ signed_list = @message.get_arg(OPENID_NS, "signed", NO_DEFAULT).split(",")
242
+
243
+ # check that we got all the signed arguments
244
+ signed_list.each do |k|
245
+ @message.get_aliased_arg(k, NO_DEFAULT)
246
+ end
247
+
248
+ ca_message = @message.copy
249
+ ca_message.set_arg(OPENID_NS, "mode", "check_authentication")
250
+
251
+ ca_message
252
+ end
253
+
254
+ # Process the response message from a check_authentication
255
+ # request, invalidating associations if requested.
256
+ def process_check_auth_response(response)
257
+ is_valid = response.get_arg(OPENID_NS, "is_valid", "false")
258
+
259
+ invalidate_handle = response.get_arg(OPENID_NS, "invalidate_handle")
260
+ unless invalidate_handle.nil?
261
+ Util.log("Received 'invalidate_handle' from server #{server_url}")
262
+ if @store.nil?
263
+ Util.log('Unexpectedly got "invalidate_handle" without a store!')
264
+ else
265
+ @store.remove_association(server_url, invalidate_handle)
266
+ end
267
+ end
268
+
269
+ return unless is_valid != "true"
270
+
271
+ raise ProtocolError, "Server #{server_url} responds that the " \
272
+ "'check_authentication' call is not valid"
273
+ end
274
+
275
+ def check_nonce
276
+ case openid_namespace
277
+ when OPENID1_NS, OPENID11_NS
278
+ nonce =
279
+ @message.get_arg(BARE_NS, Consumer.openid1_return_to_nonce_name)
280
+
281
+ # We generated the nonce, so it uses the empty string as the
282
+ # server URL
283
+ server_url = ""
284
+ when OPENID2_NS
285
+ nonce = @message.get_arg(OPENID2_NS, "response_nonce")
286
+ server_url = self.server_url
287
+ else
288
+ raise StandardError, "Not reached"
289
+ end
290
+
291
+ raise ProtocolError, "Nonce missing from response" if nonce.nil?
292
+
293
+ begin
294
+ time, extra = Nonce.split_nonce(nonce)
295
+ rescue ArgumentError
296
+ raise ProtocolError, "Malformed nonce: #{nonce.inspect}"
297
+ end
298
+
299
+ return unless !@store.nil? && !@store.use_nonce(server_url, time, extra)
300
+
301
+ raise ProtocolError, "Nonce already used or out of range: " \
302
+ "#{nonce.inspect}"
303
+ end
304
+
305
+ def verify_discovery_results
306
+ case openid_namespace
307
+ when OPENID1_NS, OPENID11_NS
308
+ verify_discovery_results_openid1
309
+ when OPENID2_NS
310
+ verify_discovery_results_openid2
311
+ else
312
+ raise StandardError, "Not reached: #{openid_namespace}"
313
+ end
314
+ rescue Message::KeyNotFound => e
315
+ raise ProtocolError, "Missing required field: #{e.message}"
316
+ end
317
+
318
+ def verify_discovery_results_openid2
319
+ to_match = OpenIDServiceEndpoint.new
320
+ to_match.type_uris = [OPENID_2_0_TYPE]
321
+ to_match.claimed_id = fetch("claimed_id", nil)
322
+ to_match.local_id = fetch("identity", nil)
323
+ to_match.server_url = fetch("op_endpoint")
324
+
325
+ if to_match.claimed_id.nil? && !to_match.local_id.nil?
326
+ raise ProtocolError, "openid.identity is present without " \
327
+ "openid.claimed_id"
328
+ elsif !to_match.claimed_id.nil? && to_match.local_id.nil?
329
+ raise ProtocolError, "openid.claimed_id is present without " \
330
+ "openid.identity"
331
+
332
+ # This is a response without identifiers, so there's really no
333
+ # checking that we can do, so return an endpoint that's for
334
+ # the specified `openid.op_endpoint'
335
+ elsif to_match.claimed_id.nil?
336
+ @endpoint =
337
+ OpenIDServiceEndpoint.from_op_endpoint_url(to_match.server_url)
338
+ return
339
+ end
340
+
341
+ if @endpoint.nil?
342
+ Util.log("No pre-discovered information supplied")
343
+ discover_and_verify(to_match.claimed_id, [to_match])
344
+ else
345
+ begin
346
+ verify_discovery_single(@endpoint, to_match)
347
+ rescue ProtocolError => e
348
+ Util.log("Error attempting to use stored discovery " \
349
+ "information: #{e.message}")
350
+ Util.log("Attempting discovery to verify endpoint")
351
+ discover_and_verify(to_match.claimed_id, [to_match])
352
+ end
353
+ end
354
+
355
+ return unless @endpoint.claimed_id != to_match.claimed_id
356
+
357
+ @endpoint = @endpoint.dup
358
+ @endpoint.claimed_id = to_match.claimed_id
359
+ end
360
+
361
+ def verify_discovery_results_openid1
362
+ claimed_id =
363
+ @message.get_arg(BARE_NS, Consumer.openid1_return_to_claimed_id_name)
364
+
365
+ if claimed_id.nil?
366
+ if @endpoint.nil?
367
+ raise ProtocolError, "When using OpenID 1, the claimed ID must " \
368
+ "be supplied, either by passing it through " \
369
+ "as a return_to parameter or by using a " \
370
+ "session, and supplied to the IdResHandler " \
371
+ "when it is constructed."
372
+ else
373
+ claimed_id = @endpoint.claimed_id
374
+ end
375
+ end
376
+
377
+ to_match = OpenIDServiceEndpoint.new
378
+ to_match.type_uris = [OPENID_1_1_TYPE]
379
+ to_match.local_id = fetch("identity")
380
+ # Restore delegate information from the initiation phase
381
+ to_match.claimed_id = claimed_id
382
+
383
+ to_match_1_0 = to_match.dup
384
+ to_match_1_0.type_uris = [OPENID_1_0_TYPE]
385
+
386
+ unless @endpoint.nil?
387
+ begin
388
+ begin
389
+ verify_discovery_single(@endpoint, to_match)
390
+ rescue TypeURIMismatch
391
+ verify_discovery_single(@endpoint, to_match_1_0)
392
+ end
393
+ rescue ProtocolError => e
394
+ Util.log("Error attempting to use stored discovery information: " +
395
+ e.message)
396
+ Util.log("Attempting discovery to verify endpoint")
397
+ else
398
+ return @endpoint
399
+ end
400
+ end
401
+
402
+ # Either no endpoint was supplied or OpenID 1.x verification
403
+ # of the information that's in the message failed on that
404
+ # endpoint.
405
+ discover_and_verify(to_match.claimed_id, [to_match, to_match_1_0])
406
+ end
407
+
408
+ # Given an endpoint object created from the information in an
409
+ # OpenID response, perform discovery and verify the discovery
410
+ # results, returning the matching endpoint that is the result of
411
+ # doing that discovery.
412
+ def discover_and_verify(claimed_id, to_match_endpoints)
413
+ Util.log("Performing discovery on #{claimed_id}")
414
+ _, services = OpenID.discover(claimed_id)
415
+ if services.length == 0
416
+ # XXX: this might want to be something other than
417
+ # ProtocolError. In Python, it's DiscoveryFailure
418
+ raise ProtocolError, "No OpenID information found at " \
419
+ "#{claimed_id}"
420
+ end
421
+ verify_discovered_services(claimed_id, services, to_match_endpoints)
422
+ end
423
+
424
+ def verify_discovered_services(claimed_id, services, to_match_endpoints)
425
+ # Search the services resulting from discovery to find one
426
+ # that matches the information from the assertion
427
+ failure_messages = []
428
+ for endpoint in services
429
+ for to_match_endpoint in to_match_endpoints
430
+ begin
431
+ verify_discovery_single(endpoint, to_match_endpoint)
432
+ rescue ProtocolError => e
433
+ failure_messages << e.message
434
+ else
435
+ # It matches, so discover verification has
436
+ # succeeded. Return this endpoint.
437
+ @endpoint = endpoint
438
+ return
439
+ end
440
+ end
441
+ end
442
+
443
+ Util.log("Discovery verification failure for #{claimed_id}")
444
+ failure_messages.each do |failure_message|
445
+ Util.log(" * Endpoint mismatch: " + failure_message)
446
+ end
447
+
448
+ # XXX: is DiscoveryFailure in Python OpenID
449
+ raise ProtocolError, "No matching endpoint found after " \
450
+ "discovering #{claimed_id}"
451
+ end
452
+
453
+ def verify_discovery_single(endpoint, to_match)
454
+ # Every type URI that's in the to_match endpoint has to be
455
+ # present in the discovered endpoint.
456
+ for type_uri in to_match.type_uris
457
+ raise TypeURIMismatch.new(type_uri, endpoint) unless endpoint.uses_extension(type_uri)
458
+ end
459
+
460
+ # Fragments do not influence discovery, so we can't compare a
461
+ # claimed identifier with a fragment to discovered information.
462
+ defragged_claimed_id =
463
+ case Yadis::XRI.identifier_scheme(to_match.claimed_id)
464
+ when :xri
465
+ to_match.claimed_id
466
+ when :uri
467
+ begin
468
+ parsed = URI.parse(to_match.claimed_id)
469
+ rescue URI::InvalidURIError
470
+ to_match.claimed_id
471
+ else
472
+ parsed.fragment = nil
473
+ parsed.to_s
474
+ end
475
+ else
476
+ raise StandardError, "Not reached"
477
+ end
478
+
479
+ if defragged_claimed_id != endpoint.claimed_id
480
+ raise ProtocolError, "Claimed ID does not match (different " \
481
+ "subjects!), Expected " \
482
+ "#{defragged_claimed_id}, got " \
483
+ "#{endpoint.claimed_id}"
484
+ end
485
+
486
+ if to_match.get_local_id != endpoint.get_local_id
487
+ raise ProtocolError, "local_id mismatch. Expected " \
488
+ "#{to_match.get_local_id}, got " \
489
+ "#{endpoint.get_local_id}"
490
+ end
491
+
492
+ # If the server URL is nil, this must be an OpenID 1
493
+ # response, because op_endpoint is a required parameter in
494
+ # OpenID 2. In that case, we don't actually care what the
495
+ # discovered server_url is, because signature checking or
496
+ # check_auth should take care of that check for us.
497
+ if to_match.server_url.nil?
498
+ if to_match.preferred_namespace != OPENID1_NS
499
+ raise StandardError,
500
+ "The code calling this must ensure that OpenID 2 " \
501
+ "responses have a non-none `openid.op_endpoint' and " \
502
+ "that it is set as the `server_url' attribute of the " \
503
+ "`to_match' endpoint."
504
+ end
505
+ elsif to_match.server_url != endpoint.server_url
506
+ raise ProtocolError, "OP Endpoint mismatch. Expected" \
507
+ "#{to_match.server_url}, got " \
508
+ "#{endpoint.server_url}"
509
+ end
510
+ end
511
+ end
512
+ end
513
+ end