C0 code coverage information
Generated on Fri Jul 11 15:55:30 -0700 2008 with rcov 0.7.0
Code reported as executed by Ruby looks like this...
and this: this line is also marked as covered.
Lines considered as run by rcov, but not reported by Ruby, look like this,
and this: these lines were inferred by rcov (using simple heuristics).
Finally, here's a line marked as not executed.
1 require "openid/message"
2 require "openid/protocolerror"
3 require "openid/kvpost"
4 require "openid/consumer/discovery"
5 require "openid/urinorm"
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 def self.openid1_return_to_nonce_name=(query_arg_name)
25 @openid1_return_to_nonce_name = query_arg_name
26 end
27
28 # See openid1_return_to_nonce_name= documentation
29 def self.openid1_return_to_nonce_name
30 @openid1_return_to_nonce_name
31 end
32
33 # Set the name of the query parameter that this library will use
34 # to thread the requested URL through an OpenID 1 transaction (for
35 # use when verifying discovered information). It will be appended
36 # to the return_to URL.
37 def self.openid1_return_to_claimed_id_name=(query_arg_name)
38 @openid1_return_to_claimed_id_name = query_arg_name
39 end
40
41 # See openid1_return_to_claimed_id_name=
42 def self.openid1_return_to_claimed_id_name
43 @openid1_return_to_claimed_id_name
44 end
45
46 # Handles an openid.mode=id_res response. This object is
47 # instantiated and used by the Consumer.
48 class IdResHandler
49 attr_reader :endpoint, :message
50
51 def initialize(message, current_url, store=nil, endpoint=nil)
52 @store = store # Fer the nonce and invalidate_handle
53 @message = message
54 @endpoint = endpoint
55 @current_url = current_url
56 @signed_list = nil
57
58 # Start the verification process
59 id_res
60 end
61
62 def signed_fields
63 signed_list.map {|x| 'openid.' + x}
64 end
65
66 protected
67
68 # This method will raise ProtocolError unless the request is a
69 # valid id_res response. Once it has been verified, the methods
70 # 'endpoint', 'message', and 'signed_fields' contain the
71 # verified information.
72 def id_res
73 check_for_fields
74 verify_return_to
75 verify_discovery_results
76 check_signature
77 check_nonce
78 end
79
80 def server_url
81 @endpoint.nil? ? nil : @endpoint.server_url
82 end
83
84 def openid_namespace
85 @message.get_openid_namespace
86 end
87
88 def fetch(field, default=NO_DEFAULT)
89 @message.get_arg(OPENID_NS, field, default)
90 end
91
92 def signed_list
93 if @signed_list.nil?
94 signed_list_str = fetch('signed', nil)
95 if signed_list_str.nil?
96 raise ProtocolError, 'Response missing signed list'
97 end
98
99 @signed_list = signed_list_str.split(',', -1)
100 end
101 @signed_list
102 end
103
104 def check_for_fields
105 # XXX: if a field is missing, we should not have to explicitly
106 # check that it's present, just make sure that the fields are
107 # actually being used by the rest of the code in
108 # tests. Although, which fields are signed does need to be
109 # checked somewhere.
110 basic_fields = ['return_to', 'assoc_handle', 'sig', 'signed']
111 basic_sig_fields = ['return_to', 'identity']
112
113 case openid_namespace
114 when OPENID2_NS
115 require_fields = basic_fields + ['op_endpoint']
116 require_sigs = basic_sig_fields +
117 ['response_nonce', 'claimed_id', 'assoc_handle',]
118 when OPENID1_NS
119 require_fields = basic_fields + ['identity']
120 require_sigs = basic_sig_fields
121 else
122 raise RuntimeError, "check_for_fields doesn't know about "\
123 "namespace #{openid_namespace.inspect}"
124 end
125
126 require_fields.each do |field|
127 if !@message.has_key?(OPENID_NS, field)
128 raise ProtocolError, "Missing required field #{field}"
129 end
130 end
131
132 require_sigs.each do |field|
133 # Field is present and not in signed list
134 if @message.has_key?(OPENID_NS, field) && !signed_list.member?(field)
135 raise ProtocolError, "#{field.inspect} not signed"
136 end
137 end
138 end
139
140 def verify_return_to
141 begin
142 msg_return_to = URI.parse(URINorm::urinorm(fetch('return_to')))
143 rescue URI::InvalidURIError
144 raise ProtocolError, ("return_to is not a valid URI")
145 end
146
147 verify_return_to_args(msg_return_to)
148 if !@current_url.nil?
149 verify_return_to_base(msg_return_to)
150 end
151 end
152
153 def verify_return_to_args(msg_return_to)
154 return_to_parsed_query = {}
155 if !msg_return_to.query.nil?
156 CGI.parse(msg_return_to.query).each_pair do |k, vs|
157 return_to_parsed_query[k] = vs[0]
158 end
159 end
160 query = @message.to_post_args
161 return_to_parsed_query.each_pair do |rt_key, rt_val|
162 msg_val = query[rt_key]
163 if msg_val.nil?
164 raise ProtocolError, "Message missing return_to argument '#{rt_key}'"
165 elsif msg_val != rt_val
166 raise ProtocolError, ("Parameter '#{rt_key}' value "\
167 "#{msg_val.inspect} does not match "\
168 "return_to's value #{rt_val.inspect}")
169 end
170 end
171 @message.get_args(BARE_NS).each_pair do |bare_key, bare_val|
172 rt_val = return_to_parsed_query[bare_key]
173 if not return_to_parsed_query.has_key? bare_key
174 # This may be caused by your web framework throwing extra
175 # entries in to your parameters hash that were not GET or
176 # POST parameters. For example, Rails has been known to
177 # add "controller" and "action" keys; another server adds
178 # at least a "format" key.
179 raise ProtocolError, ("Unexpected parameter (not on return_to): "\
180 "'#{bare_key}'=#{rt_val.inspect})")
181 end
182 if rt_val != bare_val
183 raise ProtocolError, ("Parameter '#{bare_key}' value "\
184 "#{bare_val.inspect} does not match "\
185 "return_to's value #{rt_val.inspect}")
186 end
187 end
188 end
189
190 def verify_return_to_base(msg_return_to)
191 begin
192 app_parsed = URI.parse(URINorm::urinorm(@current_url))
193 rescue URI::InvalidURIError
194 raise ProtocolError, "current_url is not a valid URI: #{@current_url}"
195 end
196
197 [:scheme, :host, :port, :path].each do |meth|
198 if msg_return_to.send(meth) != app_parsed.send(meth)
199 raise ProtocolError, "return_to #{meth.to_s} does not match"
200 end
201 end
202 end
203
204 # Raises ProtocolError if the signature is bad
205 def check_signature
206 if @store.nil?
207 assoc = nil
208 else
209 assoc = @store.get_association(server_url, fetch('assoc_handle'))
210 end
211
212 if assoc.nil?
213 check_auth
214 else
215 if assoc.expires_in <= 0
216 # XXX: It might be a good idea sometimes to re-start the
217 # authentication with a new association. Doing it
218 # automatically opens the possibility for
219 # denial-of-service by a server that just returns expired
220 # associations (or really short-lived associations)
221 raise ProtocolError, "Association with #{server_url} expired"
222 elsif !assoc.check_message_signature(@message)
223 raise ProtocolError, "Bad signature in response from #{server_url}"
224 end
225 end
226 end
227
228 def check_auth
229 Util.log("Using 'check_authentication' with #{server_url}")
230 begin
231 request = create_check_auth_request
232 rescue Message::KeyNotFound => why
233 raise ProtocolError, "Could not generate 'check_authentication' "\
234 "request: #{why.message}"
235 end
236
237 response = OpenID.make_kv_post(request, server_url)
238
239 process_check_auth_response(response)
240 end
241
242 def create_check_auth_request
243 signed_list = @message.get_arg(OPENID_NS, 'signed', NO_DEFAULT).split(',')
244
245 # check that we got all the signed arguments
246 signed_list.each {|k|
247 @message.get_aliased_arg(k, NO_DEFAULT)
248 }
249
250 ca_message = @message.copy
251 ca_message.set_arg(OPENID_NS, 'mode', 'check_authentication')
252
253 return ca_message
254 end
255
256 # Process the response message from a check_authentication
257 # request, invalidating associations if requested.
258 def process_check_auth_response(response)
259 is_valid = response.get_arg(OPENID_NS, 'is_valid', 'false')
260
261 invalidate_handle = response.get_arg(OPENID_NS, 'invalidate_handle')
262 if !invalidate_handle.nil?
263 Util.log("Received 'invalidate_handle' from server #{server_url}")
264 if @store.nil?
265 Util.log('Unexpectedly got "invalidate_handle" without a store!')
266 else
267 @store.remove_association(server_url, invalidate_handle)
268 end
269 end
270
271 if is_valid != 'true'
272 raise ProtocolError, ("Server #{server_url} responds that the "\
273 "'check_authentication' call is not valid")
274 end
275 end
276
277 def check_nonce
278 case openid_namespace
279 when OPENID1_NS
280 nonce =
281 @message.get_arg(BARE_NS, Consumer.openid1_return_to_nonce_name)
282
283 # We generated the nonce, so it uses the empty string as the
284 # server URL
285 server_url = ''
286 when OPENID2_NS
287 nonce = @message.get_arg(OPENID2_NS, 'response_nonce')
288 server_url = self.server_url
289 else
290 raise StandardError, 'Not reached'
291 end
292
293 if nonce.nil?
294 raise ProtocolError, 'Nonce missing from response'
295 end
296
297 begin
298 time, extra = Nonce.split_nonce(nonce)
299 rescue ArgumentError => why
300 raise ProtocolError, "Malformed nonce: #{nonce.inspect}"
301 end
302
303 if !@store.nil? && !@store.use_nonce(server_url, time, extra)
304 raise ProtocolError, ("Nonce already used or out of range: "\
305 "#{nonce.inspect}")
306 end
307 end
308
309 def verify_discovery_results
310 begin
311 case openid_namespace
312 when OPENID1_NS
313 verify_discovery_results_openid1
314 when OPENID2_NS
315 verify_discovery_results_openid2
316 else
317 raise StandardError, "Not reached: #{openid_namespace}"
318 end
319 rescue Message::KeyNotFound => why
320 raise ProtocolError, "Missing required field: #{why.message}"
321 end
322 end
323
324 def verify_discovery_results_openid2
325 to_match = OpenIDServiceEndpoint.new
326 to_match.type_uris = [OPENID_2_0_TYPE]
327 to_match.claimed_id = fetch('claimed_id', nil)
328 to_match.local_id = fetch('identity', nil)
329 to_match.server_url = fetch('op_endpoint')
330
331 if to_match.claimed_id.nil? && !to_match.local_id.nil?
332 raise ProtocolError, ('openid.identity is present without '\
333 'openid.claimed_id')
334 elsif !to_match.claimed_id.nil? && to_match.local_id.nil?
335 raise ProtocolError, ('openid.claimed_id is present without '\
336 'openid.identity')
337
338 # This is a response without identifiers, so there's really no
339 # checking that we can do, so return an endpoint that's for
340 # the specified `openid.op_endpoint'
341 elsif to_match.claimed_id.nil?
342 @endpoint =
343 OpenIDServiceEndpoint.from_op_endpoint_url(to_match.server_url)
344 return
345 end
346
347 if @endpoint.nil?
348 Util.log('No pre-discovered information supplied')
349 discover_and_verify(to_match.claimed_id, [to_match])
350 else
351 begin
352 verify_discovery_single(@endpoint, to_match)
353 rescue ProtocolError => why
354 Util.log("Error attempting to use stored discovery "\
355 "information: #{why.message}")
356 Util.log("Attempting discovery to verify endpoint")
357 discover_and_verify(to_match.claimed_id, [to_match])
358 end
359 end
360
361 if @endpoint.claimed_id != to_match.claimed_id
362 @endpoint = @endpoint.dup
363 @endpoint.claimed_id = to_match.claimed_id
364 end
365 end
366
367 def verify_discovery_results_openid1
368 claimed_id =
369 @message.get_arg(BARE_NS, Consumer.openid1_return_to_claimed_id_name)
370
371 if claimed_id.nil?
372 if @endpoint.nil?
373 raise ProtocolError, ("When using OpenID 1, the claimed ID must "\
374 "be supplied, either by passing it through "\
375 "as a return_to parameter or by using a "\
376 "session, and supplied to the IdResHandler "\
377 "when it is constructed.")
378 else
379 claimed_id = @endpoint.claimed_id
380 end
381 end
382
383 to_match = OpenIDServiceEndpoint.new
384 to_match.type_uris = [OPENID_1_1_TYPE]
385 to_match.local_id = fetch('identity')
386 # Restore delegate information from the initiation phase
387 to_match.claimed_id = claimed_id
388
389 to_match_1_0 = to_match.dup
390 to_match_1_0.type_uris = [OPENID_1_0_TYPE]
391
392 if !@endpoint.nil?
393 begin
394 begin
395 verify_discovery_single(@endpoint, to_match)
396 rescue TypeURIMismatch
397 verify_discovery_single(@endpoint, to_match_1_0)
398 end
399 rescue ProtocolError => why
400 Util.log('Error attempting to use stored discovery information: ' +
401 why.message)
402 Util.log('Attempting discovery to verify endpoint')
403 else
404 return @endpoint
405 end
406 end
407
408 # Either no endpoint was supplied or OpenID 1.x verification
409 # of the information that's in the message failed on that
410 # endpoint.
411 discover_and_verify(to_match.claimed_id, [to_match, to_match_1_0])
412 end
413
414 # Given an endpoint object created from the information in an
415 # OpenID response, perform discovery and verify the discovery
416 # results, returning the matching endpoint that is the result of
417 # doing that discovery.
418 def discover_and_verify(claimed_id, to_match_endpoints)
419 Util.log("Performing discovery on #{claimed_id}")
420 _, services = OpenID.discover(claimed_id)
421 if services.length == 0
422 # XXX: this might want to be something other than
423 # ProtocolError. In Python, it's DiscoveryFailure
424 raise ProtocolError, ("No OpenID information found at "\
425 "#{claimed_id}")
426 end
427 verify_discovered_services(claimed_id, services, to_match_endpoints)
428 end
429
430
431 def verify_discovered_services(claimed_id, services, to_match_endpoints)
432 # Search the services resulting from discovery to find one
433 # that matches the information from the assertion
434 failure_messages = []
435 for endpoint in services
436 for to_match_endpoint in to_match_endpoints
437 begin
438 verify_discovery_single(endpoint, to_match_endpoint)
439 rescue ProtocolError => why
440 failure_messages << why.message
441 else
442 # It matches, so discover verification has
443 # succeeded. Return this endpoint.
444 @endpoint = endpoint
445 return
446 end
447 end
448 end
449
450 Util.log("Discovery verification failure for #{claimed_id}")
451 failure_messages.each do |failure_message|
452 Util.log(" * Endpoint mismatch: " + failure_message)
453 end
454
455 # XXX: is DiscoveryFailure in Python OpenID
456 raise ProtocolError, ("No matching endpoint found after "\
457 "discovering #{claimed_id}")
458 end
459
460 def verify_discovery_single(endpoint, to_match)
461 # Every type URI that's in the to_match endpoint has to be
462 # present in the discovered endpoint.
463 for type_uri in to_match.type_uris
464 if !endpoint.uses_extension(type_uri)
465 raise TypeURIMismatch.new(type_uri, endpoint)
466 end
467 end
468
469 # Fragments do not influence discovery, so we can't compare a
470 # claimed identifier with a fragment to discovered information.
471 defragged_claimed_id =
472 case Yadis::XRI.identifier_scheme(endpoint.claimed_id)
473 when :xri
474 endpoint.claimed_id
475 when :uri
476 begin
477 parsed = URI.parse(endpoint.claimed_id)
478 rescue URI::InvalidURIError
479 endpoint.claimed_id
480 else
481 parsed.fragment = nil
482 parsed.to_s
483 end
484 else
485 raise StandardError, 'Not reached'
486 end
487
488 if defragged_claimed_id != endpoint.claimed_id
489 raise ProtocolError, ("Claimed ID does not match (different "\
490 "subjects!), Expected "\
491 "#{defragged_claimed_id}, got "\
492 "#{endpoint.claimed_id}")
493 end
494
495 if to_match.get_local_id != endpoint.get_local_id
496 raise ProtocolError, ("local_id mismatch. Expected "\
497 "#{to_match.get_local_id}, got "\
498 "#{endpoint.get_local_id}")
499 end
500
501 # If the server URL is nil, this must be an OpenID 1
502 # response, because op_endpoint is a required parameter in
503 # OpenID 2. In that case, we don't actually care what the
504 # discovered server_url is, because signature checking or
505 # check_auth should take care of that check for us.
506 if to_match.server_url.nil?
507 if to_match.preferred_namespace != OPENID1_NS
508 raise StandardError,
509 "The code calling this must ensure that OpenID 2 "\
510 "responses have a non-none `openid.op_endpoint' and "\
511 "that it is set as the `server_url' attribute of the "\
512 "`to_match' endpoint."
513 end
514 elsif to_match.server_url != endpoint.server_url
515 raise ProtocolError, ("OP Endpoint mismatch. Expected"\
516 "#{to_match.server_url}, got "\
517 "#{endpoint.server_url}")
518 end
519 end
520
521 end
522 end
523 end
Generated using the rcov code coverage analysis tool for Ruby version 0.7.0.