ruby-openid2 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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