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,1571 @@
1
+ require_relative "cryptutil"
2
+ require_relative "util"
3
+ require_relative "dh"
4
+ require_relative "store/nonce"
5
+ require_relative "trustroot"
6
+ require_relative "association"
7
+ require_relative "message"
8
+
9
+ require "time"
10
+
11
+ module OpenID
12
+ module Server
13
+ HTTP_OK = 200
14
+ HTTP_REDIRECT = 302
15
+ HTTP_ERROR = 400
16
+
17
+ BROWSER_REQUEST_MODES = %w[checkid_setup checkid_immediate]
18
+
19
+ ENCODE_KVFORM = ["kvform"].freeze
20
+ ENCODE_URL = ["URL/redirect"].freeze
21
+ ENCODE_HTML_FORM = ["HTML form"].freeze
22
+
23
+ UNUSED = nil
24
+
25
+ class OpenIDRequest
26
+ attr_accessor :message, :mode
27
+
28
+ # I represent an incoming OpenID request.
29
+ #
30
+ # Attributes:
31
+ # mode:: The "openid.mode" of this request
32
+ def initialize
33
+ @mode = nil
34
+ @message = nil
35
+ end
36
+
37
+ def namespace
38
+ raise "Request has no message" if @message.nil?
39
+
40
+ @message.get_openid_namespace
41
+ end
42
+ end
43
+
44
+ # A request to verify the validity of a previous response.
45
+ #
46
+ # See OpenID Specs, Verifying Directly with the OpenID Provider
47
+ # <http://openid.net/specs/openid-authentication-2_0-12.html#verifying_signatures>
48
+ class CheckAuthRequest < OpenIDRequest
49
+ # The association handle the response was signed with.
50
+ attr_accessor :assoc_handle
51
+
52
+ # The message with the signature which wants checking.
53
+ attr_accessor :signed
54
+
55
+ # An association handle the client is asking about the validity
56
+ # of. May be nil.
57
+ attr_accessor :invalidate_handle
58
+
59
+ attr_accessor :sig
60
+
61
+ # Construct me.
62
+ #
63
+ # These parameters are assigned directly as class attributes.
64
+ #
65
+ # Parameters:
66
+ # assoc_handle:: the association handle for this request
67
+ # signed:: The signed message
68
+ # invalidate_handle:: An association handle that the relying
69
+ # party is checking to see if it is invalid
70
+ def initialize(assoc_handle, signed, invalidate_handle = nil)
71
+ super()
72
+
73
+ @mode = "check_authentication"
74
+ @required_fields = %w[identity return_to response_nonce].freeze
75
+
76
+ @sig = nil
77
+ @assoc_handle = assoc_handle
78
+ @signed = signed
79
+ @invalidate_handle = invalidate_handle
80
+ end
81
+
82
+ # Construct me from an OpenID::Message.
83
+ def self.from_message(message, _op_endpoint = UNUSED)
84
+ assoc_handle = message.get_arg(OPENID_NS, "assoc_handle")
85
+ invalidate_handle = message.get_arg(OPENID_NS, "invalidate_handle")
86
+
87
+ signed = message.copy
88
+ # openid.mode is currently check_authentication because
89
+ # that's the mode of this request. But the signature
90
+ # was made on something with a different openid.mode.
91
+ # http://article.gmane.org/gmane.comp.web.openid.general/537
92
+ signed.set_arg(OPENID_NS, "mode", "id_res") if signed.has_key?(OPENID_NS, "mode")
93
+
94
+ obj = new(assoc_handle, signed, invalidate_handle)
95
+ obj.message = message
96
+ obj.sig = message.get_arg(OPENID_NS, "sig")
97
+
98
+ if !obj.assoc_handle or
99
+ !obj.sig
100
+ msg = format(
101
+ "%s request missing required parameter from message %s",
102
+ obj.mode,
103
+ message,
104
+ )
105
+ raise ProtocolError.new(message, msg)
106
+ end
107
+
108
+ obj
109
+ end
110
+
111
+ # Respond to this request.
112
+ #
113
+ # Given a Signatory, I can check the validity of the signature
114
+ # and the invalidate_handle. I return a response with an
115
+ # is_valid (and, if appropriate invalidate_handle) field.
116
+ def answer(signatory)
117
+ is_valid = signatory.verify(@assoc_handle, @signed)
118
+ # Now invalidate that assoc_handle so it this checkAuth
119
+ # message cannot be replayed.
120
+ signatory.invalidate(@assoc_handle, true)
121
+ response = OpenIDResponse.new(self)
122
+ valid_str = is_valid ? "true" : "false"
123
+ response.fields.set_arg(OPENID_NS, "is_valid", valid_str)
124
+
125
+ if @invalidate_handle
126
+ assoc = signatory.get_association(@invalidate_handle, false)
127
+ unless assoc
128
+ response.fields.set_arg(
129
+ OPENID_NS, "invalidate_handle", @invalidate_handle
130
+ )
131
+ end
132
+ end
133
+
134
+ response
135
+ end
136
+
137
+ def to_s
138
+ ih = nil
139
+
140
+ ih = if @invalidate_handle
141
+ format(" invalidate? %s", @invalidate_handle)
142
+ else
143
+ ""
144
+ end
145
+
146
+ format(
147
+ "<%s handle: %s sig: %s: signed: %s%s>",
148
+ self.class,
149
+ @assoc_handle,
150
+ @sig,
151
+ @signed,
152
+ ih,
153
+ )
154
+ end
155
+ end
156
+
157
+ class BaseServerSession
158
+ attr_reader :session_type
159
+
160
+ def initialize(session_type, allowed_assoc_types)
161
+ @session_type = session_type
162
+ @allowed_assoc_types = allowed_assoc_types.dup.freeze
163
+ end
164
+
165
+ def allowed_assoc_type?(typ)
166
+ @allowed_assoc_types.member?(typ)
167
+ end
168
+ end
169
+
170
+ # An object that knows how to handle association requests with
171
+ # no session type.
172
+ #
173
+ # See OpenID Specs, Section 8: Establishing Associations
174
+ # <http://openid.net/specs/openid-authentication-2_0-12.html#associations>
175
+ class PlainTextServerSession < BaseServerSession
176
+ # The session_type for this association session. There is no
177
+ # type defined for plain-text in the OpenID specification, so we
178
+ # use 'no-encryption'.
179
+ attr_reader :session_type
180
+
181
+ def initialize
182
+ super("no-encryption", %w[HMAC-SHA1 HMAC-SHA256])
183
+ end
184
+
185
+ def self.from_message(_unused_request)
186
+ new
187
+ end
188
+
189
+ def answer(secret)
190
+ {"mac_key" => Util.to_base64(secret)}
191
+ end
192
+ end
193
+
194
+ # An object that knows how to handle association requests with the
195
+ # Diffie-Hellman session type.
196
+ #
197
+ # See OpenID Specs, Section 8: Establishing Associations
198
+ # <http://openid.net/specs/openid-authentication-2_0-12.html#associations>
199
+ class DiffieHellmanSHA1ServerSession < BaseServerSession
200
+ # The Diffie-Hellman algorithm values for this request
201
+ attr_accessor :dh
202
+
203
+ # The public key sent by the consumer in the associate request
204
+ attr_accessor :consumer_pubkey
205
+
206
+ # The session_type for this association session.
207
+ attr_reader :session_type
208
+
209
+ def initialize(dh, consumer_pubkey)
210
+ super("DH-SHA1", ["HMAC-SHA1"])
211
+
212
+ @hash_func = CryptUtil.method(:sha1)
213
+ @dh = dh
214
+ @consumer_pubkey = consumer_pubkey
215
+ end
216
+
217
+ # Construct me from OpenID Message
218
+ #
219
+ # Raises ProtocolError when parameters required to establish the
220
+ # session are missing.
221
+ def self.from_message(message)
222
+ dh_modulus = message.get_arg(OPENID_NS, "dh_modulus")
223
+ dh_gen = message.get_arg(OPENID_NS, "dh_gen")
224
+ if (!dh_modulus and dh_gen) or
225
+ (!dh_gen and dh_modulus)
226
+
227
+ missing = if !dh_modulus
228
+ "modulus"
229
+ else
230
+ "generator"
231
+ end
232
+
233
+ raise ProtocolError.new(
234
+ message,
235
+ format(
236
+ "If non-default modulus or generator is " +
237
+ "supplied, both must be supplied. Missing %s",
238
+ missing,
239
+ ),
240
+ )
241
+ end
242
+
243
+ if dh_modulus or dh_gen
244
+ dh_modulus = CryptUtil.base64_to_num(dh_modulus)
245
+ dh_gen = CryptUtil.base64_to_num(dh_gen)
246
+ dh = DiffieHellman.new(dh_modulus, dh_gen)
247
+ else
248
+ dh = DiffieHellman.from_defaults
249
+ end
250
+
251
+ consumer_pubkey = message.get_arg(OPENID_NS, "dh_consumer_public")
252
+ unless consumer_pubkey
253
+ raise ProtocolError.new(
254
+ message,
255
+ format(
256
+ "Public key for DH-SHA1 session " +
257
+ "not found in message %s",
258
+ message,
259
+ ),
260
+ )
261
+ end
262
+
263
+ consumer_pubkey = CryptUtil.base64_to_num(consumer_pubkey)
264
+
265
+ new(dh, consumer_pubkey)
266
+ end
267
+
268
+ def answer(secret)
269
+ mac_key = @dh.xor_secret(
270
+ @hash_func,
271
+ @consumer_pubkey,
272
+ secret,
273
+ )
274
+ {
275
+ "dh_server_public" => CryptUtil.num_to_base64(@dh.public),
276
+ "enc_mac_key" => Util.to_base64(mac_key),
277
+ }
278
+ end
279
+ end
280
+
281
+ class DiffieHellmanSHA256ServerSession < DiffieHellmanSHA1ServerSession
282
+ def initialize(*args)
283
+ super
284
+ @session_type = "DH-SHA256"
285
+ @hash_func = CryptUtil.method(:sha256)
286
+ @allowed_assoc_types = ["HMAC-SHA256"].freeze
287
+ end
288
+ end
289
+
290
+ # A request to establish an association.
291
+ #
292
+ # See OpenID Specs, Section 8: Establishing Associations
293
+ # <http://openid.net/specs/openid-authentication-2_0-12.html#associations>
294
+ class AssociateRequest < OpenIDRequest
295
+ # An object that knows how to handle association requests of a
296
+ # certain type.
297
+ attr_accessor :session
298
+
299
+ # The type of association. Supported values include HMAC-SHA256
300
+ # and HMAC-SHA1
301
+ attr_accessor :assoc_type
302
+
303
+ @@session_classes = {
304
+ "no-encryption" => PlainTextServerSession,
305
+ "DH-SHA1" => DiffieHellmanSHA1ServerSession,
306
+ "DH-SHA256" => DiffieHellmanSHA256ServerSession,
307
+ }
308
+
309
+ # Construct me.
310
+ #
311
+ # The session is assigned directly as a class attribute. See my
312
+ # class documentation for its description.
313
+ def initialize(session, assoc_type)
314
+ super()
315
+ @session = session
316
+ @assoc_type = assoc_type
317
+
318
+ @mode = "associate"
319
+ end
320
+
321
+ # Construct me from an OpenID Message.
322
+ def self.from_message(message, _op_endpoint = UNUSED)
323
+ if message.is_openid1
324
+ session_type = message.get_arg(OPENID_NS, "session_type")
325
+ if session_type == "no-encryption"
326
+ Util.log("Received OpenID 1 request with a no-encryption " +
327
+ "association session type. Continuing anyway.")
328
+ elsif !session_type
329
+ session_type = "no-encryption"
330
+ end
331
+ else
332
+ session_type = message.get_arg(OPENID2_NS, "session_type")
333
+ unless session_type
334
+ raise ProtocolError.new(
335
+ message,
336
+ "session_type missing from request",
337
+ )
338
+ end
339
+ end
340
+
341
+ session_class = @@session_classes[session_type]
342
+
343
+ unless session_class
344
+ raise ProtocolError.new(
345
+ message,
346
+ format("Unknown session type %s", session_type),
347
+ )
348
+ end
349
+
350
+ begin
351
+ session = session_class.from_message(message)
352
+ rescue ArgumentError => e
353
+ # XXX
354
+ raise ProtocolError.new(
355
+ message,
356
+ format(
357
+ "Error parsing %s session: %s",
358
+ session_type,
359
+ e,
360
+ ),
361
+ )
362
+ end
363
+
364
+ assoc_type = message.get_arg(OPENID_NS, "assoc_type", "HMAC-SHA1")
365
+ unless session.allowed_assoc_type?(assoc_type)
366
+ msg = format(
367
+ "Session type %s does not support association type %s",
368
+ session_type,
369
+ assoc_type,
370
+ )
371
+ raise ProtocolError.new(message, msg)
372
+ end
373
+
374
+ obj = new(session, assoc_type)
375
+ obj.message = message
376
+ obj
377
+ end
378
+
379
+ # Respond to this request with an association.
380
+ #
381
+ # assoc:: The association to send back.
382
+ #
383
+ # Returns a response with the association information, encrypted
384
+ # to the consumer's public key if appropriate.
385
+ def answer(assoc)
386
+ response = OpenIDResponse.new(self)
387
+ response.fields.update_args(OPENID_NS, {
388
+ "expires_in" => format("%d", assoc.expires_in),
389
+ "assoc_type" => @assoc_type,
390
+ "assoc_handle" => assoc.handle,
391
+ })
392
+ response.fields.update_args(
393
+ OPENID_NS,
394
+ @session.answer(assoc.secret),
395
+ )
396
+ unless @session.session_type == "no-encryption" and
397
+ @message.is_openid1
398
+ response.fields.set_arg(
399
+ OPENID_NS, "session_type", @session.session_type
400
+ )
401
+ end
402
+
403
+ response
404
+ end
405
+
406
+ # Respond to this request indicating that the association type
407
+ # or association session type is not supported.
408
+ def answer_unsupported(message, preferred_association_type = nil,
409
+ preferred_session_type = nil)
410
+ raise ProtocolError.new(@message) if @message.is_openid1
411
+
412
+ response = OpenIDResponse.new(self)
413
+ response.fields.set_arg(OPENID_NS, "error_code", "unsupported-type")
414
+ response.fields.set_arg(OPENID_NS, "error", message)
415
+
416
+ if preferred_association_type
417
+ response.fields.set_arg(
418
+ OPENID_NS, "assoc_type", preferred_association_type
419
+ )
420
+ end
421
+
422
+ if preferred_session_type
423
+ response.fields.set_arg(
424
+ OPENID_NS, "session_type", preferred_session_type
425
+ )
426
+ end
427
+
428
+ response
429
+ end
430
+ end
431
+
432
+ # A request to confirm the identity of a user.
433
+ #
434
+ # This class handles requests for openid modes
435
+ # +checkid_immediate+ and +checkid_setup+ .
436
+ class CheckIDRequest < OpenIDRequest
437
+ # Provided in smart mode requests, a handle for a previously
438
+ # established association. nil for dumb mode requests.
439
+ attr_accessor :assoc_handle
440
+
441
+ # Is this an immediate-mode request?
442
+ attr_accessor :immediate
443
+
444
+ # The URL to send the user agent back to to reply to this
445
+ # request.
446
+ attr_accessor :return_to
447
+
448
+ # The OP-local identifier being checked.
449
+ attr_accessor :identity
450
+
451
+ # The claimed identifier. Not present in OpenID 1.x
452
+ # messages.
453
+ attr_accessor :claimed_id
454
+
455
+ # This URL identifies the party making the request, and the user
456
+ # will use that to make her decision about what answer she
457
+ # trusts them to have. Referred to as "realm" in OpenID 2.0.
458
+ attr_accessor :trust_root
459
+
460
+ # mode:: +checkid_immediate+ or +checkid_setup+
461
+ attr_accessor :mode
462
+
463
+ attr_accessor :op_endpoint
464
+
465
+ # These parameters are assigned directly as attributes,
466
+ # see the #CheckIDRequest class documentation for their
467
+ # descriptions.
468
+ #
469
+ # Raises #MalformedReturnURL when the +return_to+ URL is not
470
+ # a URL.
471
+ def initialize(identity, return_to, op_endpoint, trust_root = nil,
472
+ immediate = false, assoc_handle = nil, claimed_id = nil)
473
+ @assoc_handle = assoc_handle
474
+ @identity = identity
475
+ @claimed_id = (claimed_id or identity)
476
+ @return_to = return_to
477
+ @trust_root = (trust_root or return_to)
478
+ @op_endpoint = op_endpoint
479
+ @message = nil
480
+
481
+ if immediate
482
+ @immediate = true
483
+ @mode = "checkid_immediate"
484
+ else
485
+ @immediate = false
486
+ @mode = "checkid_setup"
487
+ end
488
+
489
+ if @return_to and
490
+ !TrustRoot::TrustRoot.parse(@return_to)
491
+ raise MalformedReturnURL.new(nil, @return_to)
492
+ end
493
+
494
+ return if trust_root_valid
495
+
496
+ raise UntrustedReturnURL.new(nil, @return_to, @trust_root)
497
+ end
498
+
499
+ # Construct me from an OpenID message.
500
+ #
501
+ # message:: An OpenID checkid_* request Message
502
+ #
503
+ # op_endpoint:: The endpoint URL of the server that this
504
+ # message was sent to.
505
+ #
506
+ # Raises:
507
+ # ProtocolError:: When not all required parameters are present
508
+ # in the message.
509
+ #
510
+ # MalformedReturnURL:: When the +return_to+ URL is not a URL.
511
+ #
512
+ # UntrustedReturnURL:: When the +return_to+ URL is
513
+ # outside the +trust_root+.
514
+ def self.from_message(message, op_endpoint)
515
+ obj = allocate
516
+ obj.message = message
517
+ obj.op_endpoint = op_endpoint
518
+ mode = message.get_arg(OPENID_NS, "mode")
519
+ if mode == "checkid_immediate"
520
+ obj.immediate = true
521
+ obj.mode = "checkid_immediate"
522
+ else
523
+ obj.immediate = false
524
+ obj.mode = "checkid_setup"
525
+ end
526
+
527
+ obj.return_to = message.get_arg(OPENID_NS, "return_to")
528
+ if message.is_openid1 and !obj.return_to
529
+ msg = format(
530
+ "Missing required field 'return_to' from %s",
531
+ message,
532
+ )
533
+ raise ProtocolError.new(message, msg)
534
+ end
535
+
536
+ obj.identity = message.get_arg(OPENID_NS, "identity")
537
+ obj.claimed_id = message.get_arg(OPENID_NS, "claimed_id")
538
+ if message.is_openid1
539
+ unless obj.identity
540
+ s = "OpenID 1 message did not contain openid.identity"
541
+ raise ProtocolError.new(message, s)
542
+ end
543
+ elsif obj.identity and !obj.claimed_id
544
+ s = ("OpenID 2.0 message contained openid.identity but not " +
545
+ "claimed_id")
546
+ raise ProtocolError.new(message, s)
547
+ elsif obj.claimed_id and !obj.identity
548
+ s = ("OpenID 2.0 message contained openid.claimed_id but not " +
549
+ "identity")
550
+ raise ProtocolError.new(message, s)
551
+ end
552
+
553
+ # There's a case for making self.trust_root be a TrustRoot
554
+ # here. But if TrustRoot isn't currently part of the "public"
555
+ # API, I'm not sure it's worth doing.
556
+ trust_root_param = if message.is_openid1
557
+ "trust_root"
558
+ else
559
+ "realm"
560
+ end
561
+ trust_root = message.get_arg(OPENID_NS, trust_root_param)
562
+ trust_root = obj.return_to if trust_root.nil? || trust_root.empty?
563
+ obj.trust_root = trust_root
564
+
565
+ if !message.is_openid1 and !obj.return_to and !obj.trust_root
566
+ raise ProtocolError.new(message, "openid.realm required when " +
567
+ "openid.return_to absent")
568
+ end
569
+
570
+ obj.assoc_handle = message.get_arg(OPENID_NS, "assoc_handle")
571
+
572
+ # Using TrustRoot.parse here is a bit misleading, as we're not
573
+ # parsing return_to as a trust root at all. However, valid
574
+ # URLs are valid trust roots, so we can use this to get an
575
+ # idea if it is a valid URL. Not all trust roots are valid
576
+ # return_to URLs, however (particularly ones with wildcards),
577
+ # so this is still a little sketchy.
578
+ if obj.return_to and
579
+ !TrustRoot::TrustRoot.parse(obj.return_to)
580
+ raise MalformedReturnURL.new(message, obj.return_to)
581
+ end
582
+
583
+ # I first thought that checking to see if the return_to is
584
+ # within the trust_root is premature here, a
585
+ # logic-not-decoding thing. But it was argued that this is
586
+ # really part of data validation. A request with an invalid
587
+ # trust_root/return_to is broken regardless of application,
588
+ # right?
589
+ raise UntrustedReturnURL.new(message, obj.return_to, obj.trust_root) unless obj.trust_root_valid
590
+
591
+ obj
592
+ end
593
+
594
+ # Is the identifier to be selected by the IDP?
595
+ def id_select
596
+ # So IDPs don't have to import the constant
597
+ @identity == IDENTIFIER_SELECT
598
+ end
599
+
600
+ # Is my return_to under my trust_root?
601
+ def trust_root_valid
602
+ return true unless @trust_root
603
+
604
+ tr = TrustRoot::TrustRoot.parse(@trust_root)
605
+ raise MalformedTrustRoot.new(@message, @trust_root) unless tr
606
+
607
+ return tr.validate_url(@return_to) if @return_to
608
+
609
+ true
610
+ end
611
+
612
+ # Does the relying party publish the return_to URL for this
613
+ # response under the realm? It is up to the provider to set a
614
+ # policy for what kinds of realms should be allowed. This
615
+ # return_to URL verification reduces vulnerability to
616
+ # data-theft attacks based on open proxies,
617
+ # corss-site-scripting, or open redirectors.
618
+ #
619
+ # This check should only be performed after making sure that
620
+ # the return_to URL matches the realm.
621
+ #
622
+ # Raises DiscoveryFailure if the realm
623
+ # URL does not support Yadis discovery (and so does not
624
+ # support the verification process).
625
+ #
626
+ # Returns true if the realm publishes a document with the
627
+ # return_to URL listed
628
+ def return_to_verified
629
+ TrustRoot.verify_return_to(@trust_root, @return_to)
630
+ end
631
+
632
+ # Respond to this request.
633
+ #
634
+ # allow:: Allow this user to claim this identity, and allow the
635
+ # consumer to have this information?
636
+ #
637
+ # server_url:: DEPRECATED. Passing op_endpoint to the
638
+ # #Server constructor makes this optional.
639
+ #
640
+ # When an OpenID 1.x immediate mode request does
641
+ # not succeed, it gets back a URL where the request
642
+ # may be carried out in a not-so-immediate fashion.
643
+ # Pass my URL in here (the fully qualified address
644
+ # of this server's endpoint, i.e.
645
+ # <tt>http://example.com/server</tt>), and I will
646
+ # use it as a base for the URL for a new request.
647
+ #
648
+ # Optional for requests where
649
+ # #CheckIDRequest.immediate is false or +allow+ is
650
+ # true.
651
+ #
652
+ # identity:: The OP-local identifier to answer with. Only for use
653
+ # when the relying party requested identifier selection.
654
+ #
655
+ # claimed_id:: The claimed identifier to answer with,
656
+ # for use with identifier selection in the case where the
657
+ # claimed identifier and the OP-local identifier differ,
658
+ # i.e. when the claimed_id uses delegation.
659
+ #
660
+ # If +identity+ is provided but this is not,
661
+ # +claimed_id+ will default to the value of +identity+.
662
+ # When answering requests that did not ask for identifier
663
+ # selection, the response +claimed_id+ will default to
664
+ # that of the request.
665
+ #
666
+ # This parameter is new in OpenID 2.0.
667
+ #
668
+ # Returns an OpenIDResponse object containing a OpenID id_res message.
669
+ #
670
+ # Raises NoReturnToError if the return_to is missing.
671
+ #
672
+ # Version 2.0 deprecates +server_url+ and adds +claimed_id+.
673
+ def answer(allow, server_url = nil, identity = nil, claimed_id = nil)
674
+ raise NoReturnToError unless @return_to
675
+
676
+ unless server_url
677
+ if @message.is_openid2 and !@op_endpoint
678
+ # In other words, that warning I raised in
679
+ # Server.__init__? You should pay attention to it now.
680
+ raise "#{self} should be constructed with " \
681
+ "op_endpoint to respond to OpenID 2.0 " \
682
+ "messages."
683
+ end
684
+
685
+ server_url = @op_endpoint
686
+ end
687
+
688
+ mode = if allow
689
+ "id_res"
690
+ elsif @message.is_openid1
691
+ if @immediate
692
+ "id_res"
693
+ else
694
+ "cancel"
695
+ end
696
+ elsif @immediate
697
+ "setup_needed"
698
+ else
699
+ "cancel"
700
+ end
701
+
702
+ response = OpenIDResponse.new(self)
703
+
704
+ if claimed_id and @message.is_openid1
705
+ raise VersionError, "claimed_id is new in OpenID 2.0 and not " \
706
+ "available for #{@message.get_openid_namespace}"
707
+ end
708
+
709
+ claimed_id = identity if identity and !claimed_id
710
+
711
+ if allow
712
+ if @identity == IDENTIFIER_SELECT
713
+ unless identity
714
+ raise ArgumentError, "This request uses IdP-driven " \
715
+ "identifier selection.You must supply " \
716
+ "an identifier in the response."
717
+ end
718
+
719
+ response_identity = identity
720
+ response_claimed_id = claimed_id
721
+
722
+ elsif @identity
723
+ if identity and (@identity != identity)
724
+ raise ArgumentError, "Request was for identity #{@identity}, " \
725
+ "cannot reply with identity #{identity}"
726
+ end
727
+
728
+ response_identity = @identity
729
+ response_claimed_id = @claimed_id
730
+ else
731
+ if identity
732
+ raise ArgumentError, "This request specified no identity " \
733
+ "and you supplied #{identity}"
734
+ end
735
+ response_identity = nil
736
+ end
737
+
738
+ if @message.is_openid1 and !response_identity
739
+ raise ArgumentError, "Request was an OpenID 1 request, so " \
740
+ "response must include an identifier."
741
+ end
742
+
743
+ response.fields.update_args(OPENID_NS, {
744
+ "mode" => mode,
745
+ "op_endpoint" => server_url,
746
+ "return_to" => @return_to,
747
+ "response_nonce" => Nonce.mk_nonce,
748
+ })
749
+
750
+ if response_identity
751
+ response.fields.set_arg(OPENID_NS, "identity", response_identity)
752
+ if @message.is_openid2
753
+ response.fields.set_arg(
754
+ OPENID_NS,
755
+ "claimed_id",
756
+ response_claimed_id,
757
+ )
758
+ end
759
+ end
760
+ else
761
+ response.fields.set_arg(OPENID_NS, "mode", mode)
762
+ if @immediate
763
+ if @message.is_openid1 and !server_url
764
+ raise ArgumentError, "setup_url is required for allow=false " \
765
+ "in OpenID 1.x immediate mode."
766
+ end
767
+
768
+ # Make a new request just like me, but with
769
+ # immediate=false.
770
+ setup_request = self.class.new(
771
+ @identity,
772
+ @return_to,
773
+ @op_endpoint,
774
+ @trust_root,
775
+ false,
776
+ @assoc_handle,
777
+ @claimed_id,
778
+ )
779
+ setup_request.message = Message.new(@message.get_openid_namespace)
780
+ setup_url = setup_request.encode_to_url(server_url)
781
+ response.fields.set_arg(OPENID_NS, "user_setup_url", setup_url)
782
+ end
783
+ end
784
+
785
+ response
786
+ end
787
+
788
+ def encode_to_url(server_url)
789
+ # Encode this request as a URL to GET.
790
+ #
791
+ # server_url:: The URL of the OpenID server to make this
792
+ # request of.
793
+ raise NoReturnToError unless @return_to
794
+
795
+ # Imported from the alternate reality where these classes are
796
+ # used in both the client and server code, so Requests are
797
+ # Encodable too. That's right, code imported from alternate
798
+ # realities all for the love of you, id_res/user_setup_url.
799
+ q = {
800
+ "mode" => @mode,
801
+ "identity" => @identity,
802
+ "claimed_id" => @claimed_id,
803
+ "return_to" => @return_to,
804
+ }
805
+
806
+ if @trust_root
807
+ if @message.is_openid1
808
+ q["trust_root"] = @trust_root
809
+ else
810
+ q["realm"] = @trust_root
811
+ end
812
+ end
813
+
814
+ q["assoc_handle"] = @assoc_handle if @assoc_handle
815
+
816
+ response = Message.new(@message.get_openid_namespace)
817
+ response.update_args(@message.get_openid_namespace, q)
818
+ response.to_url(server_url)
819
+ end
820
+
821
+ def cancel_url
822
+ # Get the URL to cancel this request.
823
+ #
824
+ # Useful for creating a "Cancel" button on a web form so that
825
+ # operation can be carried out directly without another trip
826
+ # through the server.
827
+ #
828
+ # (Except you may want to make another trip through the
829
+ # server so that it knows that the user did make a decision.)
830
+ #
831
+ # Returns a URL as a string.
832
+ raise NoReturnToError unless @return_to
833
+
834
+ if @immediate
835
+ raise ArgumentError.new("Cancel is not an appropriate response to " +
836
+ "immediate mode requests.")
837
+ end
838
+
839
+ response = Message.new(@message.get_openid_namespace)
840
+ response.set_arg(OPENID_NS, "mode", "cancel")
841
+ response.to_url(@return_to)
842
+ end
843
+
844
+ def to_s
845
+ format(
846
+ "<%s id:%s im:%s tr:%s ah:%s>",
847
+ self.class,
848
+ @identity,
849
+ @immediate,
850
+ @trust_root,
851
+ @assoc_handle,
852
+ )
853
+ end
854
+ end
855
+
856
+ # I am a response to an OpenID request.
857
+ #
858
+ # Attributes:
859
+ # signed:: A list of the names of the fields which should be signed.
860
+ #
861
+ # Implementer's note: In a more symmetric client/server
862
+ # implementation, there would be more types of #OpenIDResponse
863
+ # object and they would have validated attributes according to
864
+ # the type of response. But as it is, Response objects in a
865
+ # server are basically write-only, their only job is to go out
866
+ # over the wire, so this is just a loose wrapper around
867
+ # #OpenIDResponse.fields.
868
+ class OpenIDResponse
869
+ # The #OpenIDRequest I respond to.
870
+ attr_accessor :request
871
+
872
+ # An #OpenID::Message with the data to be returned.
873
+ # Keys are parameter names with no
874
+ # leading openid. e.g. identity and mac_key
875
+ # never openid.identity.
876
+ attr_accessor :fields
877
+
878
+ def initialize(request)
879
+ # Make a response to an OpenIDRequest.
880
+ @request = request
881
+ @fields = Message.new(request.namespace)
882
+ end
883
+
884
+ def to_s
885
+ format(
886
+ "%s for %s: %s",
887
+ self.class,
888
+ @request.class,
889
+ @fields,
890
+ )
891
+ end
892
+
893
+ # form_tag_attrs is a hash of attributes to be added to the form
894
+ # tag. 'accept-charset' and 'enctype' have defaults that can be
895
+ # overridden. If a value is supplied for 'action' or 'method',
896
+ # it will be replaced.
897
+ # Returns the form markup for this response.
898
+ def to_form_markup(form_tag_attrs = nil)
899
+ @fields.to_form_markup(@request.return_to, form_tag_attrs)
900
+ end
901
+
902
+ # Wraps the form tag from to_form_markup in a complete HTML document
903
+ # that uses javascript to autosubmit the form.
904
+ def to_html(form_tag_attrs = nil)
905
+ Util.auto_submit_html(to_form_markup(form_tag_attrs))
906
+ end
907
+
908
+ def render_as_form
909
+ # Returns true if this response's encoding is
910
+ # ENCODE_HTML_FORM. Convenience method for server authors.
911
+ which_encoding == ENCODE_HTML_FORM
912
+ end
913
+
914
+ def needs_signing
915
+ # Does this response require signing?
916
+ @fields.get_arg(OPENID_NS, "mode") == "id_res"
917
+ end
918
+
919
+ # implements IEncodable
920
+
921
+ def which_encoding
922
+ # How should I be encoded?
923
+ # returns one of ENCODE_URL or ENCODE_KVFORM.
924
+ return ENCODE_KVFORM unless BROWSER_REQUEST_MODES.member?(@request.mode)
925
+
926
+ if @fields.is_openid2 and
927
+ encode_to_url.length > OPENID1_URL_LIMIT
928
+ ENCODE_HTML_FORM
929
+ else
930
+ ENCODE_URL
931
+ end
932
+ end
933
+
934
+ def encode_to_url
935
+ # Encode a response as a URL for the user agent to GET.
936
+ # You will generally use this URL with a HTTP redirect.
937
+ @fields.to_url(@request.return_to)
938
+ end
939
+
940
+ def add_extension(extension_response)
941
+ # Add an extension response to this response message.
942
+ #
943
+ # extension_response:: An object that implements the
944
+ # #OpenID::Extension interface for adding arguments to an OpenID
945
+ # message.
946
+ extension_response.to_message(@fields)
947
+ end
948
+
949
+ def encode_to_kvform
950
+ # Encode a response in key-value colon/newline format.
951
+ #
952
+ # This is a machine-readable format used to respond to
953
+ # messages which came directly from the consumer and not
954
+ # through the user agent.
955
+ #
956
+ # see: OpenID Specs,
957
+ # <a href="http://openid.net/specs.bml#keyvalue">Key-Value Colon/Newline format</a>
958
+ @fields.to_kvform
959
+ end
960
+
961
+ def copy
962
+ Marshal.load(Marshal.dump(self))
963
+ end
964
+ end
965
+
966
+ # I am a response to an OpenID request in terms a web server
967
+ # understands.
968
+ #
969
+ # I generally come from an #Encoder, either directly or from
970
+ # #Server.encodeResponse.
971
+ class WebResponse
972
+ # The HTTP code of this response as an integer.
973
+ attr_accessor :code
974
+
975
+ # #Hash of headers to include in this response.
976
+ attr_accessor :headers
977
+
978
+ # The body of this response.
979
+ attr_accessor :body
980
+
981
+ def initialize(code = HTTP_OK, headers = nil, body = "")
982
+ # Construct me.
983
+ #
984
+ # These parameters are assigned directly as class attributes,
985
+ # see my class documentation for their
986
+ # descriptions.
987
+ @code = code
988
+ @headers = headers || {}
989
+ @body = body
990
+ end
991
+ end
992
+
993
+ # I sign things.
994
+ #
995
+ # I also check signatures.
996
+ #
997
+ # All my state is encapsulated in a store, which means I'm not
998
+ # generally pickleable but I am easy to reconstruct.
999
+ class Signatory
1000
+ # The number of seconds a secret remains valid. Defaults to 14 days.
1001
+ attr_accessor :secret_lifetime
1002
+
1003
+ # keys have a bogus server URL in them because the filestore
1004
+ # really does expect that key to be a URL. This seems a little
1005
+ # silly for the server store, since I expect there to be only
1006
+ # one server URL.
1007
+ @@_normal_key = "http://localhost/|normal"
1008
+ @@_dumb_key = "http://localhost/|dumb"
1009
+
1010
+ def self._normal_key
1011
+ @@_normal_key
1012
+ end
1013
+
1014
+ def self._dumb_key
1015
+ @@_dumb_key
1016
+ end
1017
+
1018
+ attr_accessor :store
1019
+
1020
+ # Create a new Signatory. store is The back-end where my
1021
+ # associations are stored.
1022
+ def initialize(store)
1023
+ Util.truthy_assert(store)
1024
+ @store = store
1025
+ @secret_lifetime = 14 * 24 * 60 * 60
1026
+ end
1027
+
1028
+ # Verify that the signature for some data is valid.
1029
+ def verify(assoc_handle, message)
1030
+ assoc = get_association(assoc_handle, true)
1031
+ unless assoc
1032
+ Util.log(format(
1033
+ "failed to get assoc with handle %s to verify " +
1034
+ "message %s",
1035
+ assoc_handle,
1036
+ message,
1037
+ ))
1038
+ return false
1039
+ end
1040
+
1041
+ begin
1042
+ valid = assoc.check_message_signature(message)
1043
+ rescue StandardError => e
1044
+ Util.log(format(
1045
+ "Error in verifying %s with %s: %s",
1046
+ message,
1047
+ assoc,
1048
+ e,
1049
+ ))
1050
+ return false
1051
+ end
1052
+
1053
+ valid
1054
+ end
1055
+
1056
+ # Sign a response.
1057
+ #
1058
+ # I take an OpenIDResponse, create a signature for everything in
1059
+ # its signed list, and return a new copy of the response object
1060
+ # with that signature included.
1061
+ def sign(response)
1062
+ signed_response = response.copy
1063
+ assoc_handle = response.request.assoc_handle
1064
+ if assoc_handle
1065
+ # normal mode disabling expiration check because even if the
1066
+ # association is expired, we still need to know some
1067
+ # properties of the association so that we may preserve
1068
+ # those properties when creating the fallback association.
1069
+ assoc = get_association(assoc_handle, false, false)
1070
+
1071
+ if !assoc or assoc.expires_in <= 0
1072
+ # fall back to dumb mode
1073
+ signed_response.fields.set_arg(
1074
+ OPENID_NS, "invalidate_handle", assoc_handle
1075
+ )
1076
+ assoc_type = assoc ? assoc.assoc_type : "HMAC-SHA1"
1077
+ if assoc and assoc.expires_in <= 0
1078
+ # now do the clean-up that the disabled checkExpiration
1079
+ # code didn't get to do.
1080
+ invalidate(assoc_handle, false)
1081
+ end
1082
+ assoc = create_association(true, assoc_type)
1083
+ end
1084
+ else
1085
+ # dumb mode.
1086
+ assoc = create_association(true)
1087
+ end
1088
+
1089
+ begin
1090
+ signed_response.fields = assoc.sign_message(signed_response.fields)
1091
+ rescue KVFormError => e
1092
+ raise EncodingError, e
1093
+ end
1094
+ signed_response
1095
+ end
1096
+
1097
+ # Make a new association.
1098
+ def create_association(dumb = true, assoc_type = "HMAC-SHA1")
1099
+ secret = CryptUtil.random_string(OpenID.get_secret_size(assoc_type))
1100
+ uniq = Util.to_base64(CryptUtil.random_string(4))
1101
+ handle = format("{%s}{%x}{%s}", assoc_type, Time.now.to_i, uniq)
1102
+
1103
+ assoc = Association.from_expires_in(
1104
+ secret_lifetime, handle, secret, assoc_type
1105
+ )
1106
+
1107
+ key = if dumb
1108
+ @@_dumb_key
1109
+ else
1110
+ @@_normal_key
1111
+ end
1112
+
1113
+ @store.store_association(key, assoc)
1114
+ assoc
1115
+ end
1116
+
1117
+ # Get the association with the specified handle.
1118
+ def get_association(assoc_handle, dumb, check_expiration = true)
1119
+ # Hmm. We've created an interface that deals almost entirely
1120
+ # with assoc_handles. The only place outside the Signatory
1121
+ # that uses this (and thus the only place that ever sees
1122
+ # Association objects) is when creating a response to an
1123
+ # association request, as it must have the association's
1124
+ # secret.
1125
+
1126
+ raise ArgumentError.new("assoc_handle must not be None") unless assoc_handle
1127
+
1128
+ key = if dumb
1129
+ @@_dumb_key
1130
+ else
1131
+ @@_normal_key
1132
+ end
1133
+
1134
+ assoc = @store.get_association(key, assoc_handle)
1135
+ if assoc and assoc.expires_in <= 0
1136
+ Util.log(format(
1137
+ "requested %sdumb key %s is expired (by %s seconds)",
1138
+ (!dumb) ? "not-" : "",
1139
+ assoc_handle,
1140
+ assoc.expires_in,
1141
+ ))
1142
+ if check_expiration
1143
+ @store.remove_association(key, assoc_handle)
1144
+ assoc = nil
1145
+ end
1146
+ end
1147
+
1148
+ assoc
1149
+ end
1150
+
1151
+ # Invalidates the association with the given handle.
1152
+ def invalidate(assoc_handle, dumb)
1153
+ key = if dumb
1154
+ @@_dumb_key
1155
+ else
1156
+ @@_normal_key
1157
+ end
1158
+
1159
+ @store.remove_association(key, assoc_handle)
1160
+ end
1161
+ end
1162
+
1163
+ # I encode responses in to WebResponses.
1164
+ #
1165
+ # If you don't like WebResponses, you can do
1166
+ # your own handling of OpenIDResponses with
1167
+ # OpenIDResponse.whichEncoding,
1168
+ # OpenIDResponse.encodeToURL, and
1169
+ # OpenIDResponse.encodeToKVForm.
1170
+ class Encoder
1171
+ @@responseFactory = WebResponse
1172
+
1173
+ # Encode a response to a WebResponse.
1174
+ #
1175
+ # Raises EncodingError when I can't figure out how to encode
1176
+ # this message.
1177
+ def encode(response)
1178
+ encode_as = response.which_encoding
1179
+ if encode_as == ENCODE_KVFORM
1180
+ wr = @@responseFactory.new(
1181
+ HTTP_OK,
1182
+ nil,
1183
+ response.encode_to_kvform,
1184
+ )
1185
+ wr.code = HTTP_ERROR if response.is_a?(Exception)
1186
+ elsif encode_as == ENCODE_URL
1187
+ location = response.encode_to_url
1188
+ wr = @@responseFactory.new(
1189
+ HTTP_REDIRECT,
1190
+ {"location" => location},
1191
+ )
1192
+ elsif encode_as == ENCODE_HTML_FORM
1193
+ wr = @@responseFactory.new(
1194
+ HTTP_OK,
1195
+ nil,
1196
+ response.to_form_markup,
1197
+ )
1198
+ else
1199
+ # Can't encode this to a protocol message. You should
1200
+ # probably render it to HTML and show it to the user.
1201
+ raise EncodingError.new(response)
1202
+ end
1203
+
1204
+ wr
1205
+ end
1206
+ end
1207
+
1208
+ # I encode responses in to WebResponses, signing
1209
+ # them when required.
1210
+ class SigningEncoder < Encoder
1211
+ attr_accessor :signatory
1212
+
1213
+ # Create a SigningEncoder given a Signatory
1214
+ def initialize(signatory)
1215
+ @signatory = signatory
1216
+ end
1217
+
1218
+ # Encode a response to a WebResponse, signing it first if
1219
+ # appropriate.
1220
+ #
1221
+ # Raises EncodingError when I can't figure out how to encode this
1222
+ # message.
1223
+ #
1224
+ # Raises AlreadySigned when this response is already signed.
1225
+ def encode(response)
1226
+ # the is_a? is a bit of a kludge... it means there isn't
1227
+ # really an adapter to make the interfaces quite match.
1228
+ if !response.is_a?(Exception) and response.needs_signing
1229
+ unless @signatory
1230
+ raise ArgumentError.new(
1231
+ format(
1232
+ "Must have a store to sign this request: %s",
1233
+ response,
1234
+ ),
1235
+ response,
1236
+ )
1237
+ end
1238
+
1239
+ raise AlreadySigned.new(response) if response.fields.has_key?(OPENID_NS, "sig")
1240
+
1241
+ response = @signatory.sign(response)
1242
+ end
1243
+
1244
+ super
1245
+ end
1246
+ end
1247
+
1248
+ # I decode an incoming web request in to a OpenIDRequest.
1249
+ class Decoder
1250
+ @@handlers = {
1251
+ "checkid_setup" => CheckIDRequest.method(:from_message),
1252
+ "checkid_immediate" => CheckIDRequest.method(:from_message),
1253
+ "check_authentication" => CheckAuthRequest.method(:from_message),
1254
+ "associate" => AssociateRequest.method(:from_message),
1255
+ }
1256
+
1257
+ attr_accessor :server
1258
+
1259
+ # Construct a Decoder. The server is necessary because some
1260
+ # replies reference their server.
1261
+ def initialize(server)
1262
+ @server = server
1263
+ end
1264
+
1265
+ # I transform query parameters into an OpenIDRequest.
1266
+ #
1267
+ # If the query does not seem to be an OpenID request at all, I
1268
+ # return nil.
1269
+ #
1270
+ # Raises ProtocolError when the query does not seem to be a valid
1271
+ # OpenID request.
1272
+ def decode(query)
1273
+ return if query.nil? or query.empty?
1274
+
1275
+ begin
1276
+ message = Message.from_post_args(query)
1277
+ rescue InvalidOpenIDNamespace => e
1278
+ query = query.dup
1279
+ query["openid.ns"] = OPENID2_NS
1280
+ message = Message.from_post_args(query)
1281
+ raise ProtocolError.new(message, e.to_s)
1282
+ end
1283
+
1284
+ mode = message.get_arg(OPENID_NS, "mode")
1285
+ unless mode
1286
+ msg = format("No mode value in message %s", message)
1287
+ raise ProtocolError.new(message, msg)
1288
+ end
1289
+
1290
+ handler = @@handlers.fetch(mode, method(:default_decoder))
1291
+ handler.call(message, @server.op_endpoint)
1292
+ end
1293
+
1294
+ # Called to decode queries when no handler for that mode is
1295
+ # found.
1296
+ #
1297
+ # This implementation always raises ProtocolError.
1298
+ def default_decoder(message, _server)
1299
+ mode = message.get_arg(OPENID_NS, "mode")
1300
+ msg = format("Unrecognized OpenID mode %s", mode)
1301
+ raise ProtocolError.new(message, msg)
1302
+ end
1303
+ end
1304
+
1305
+ # I handle requests for an OpenID server.
1306
+ #
1307
+ # Some types of requests (those which are not checkid requests)
1308
+ # may be handed to my handleRequest method, and I will take care
1309
+ # of it and return a response.
1310
+ #
1311
+ # For your convenience, I also provide an interface to
1312
+ # Decoder.decode and SigningEncoder.encode through my methods
1313
+ # decodeRequest and encodeResponse.
1314
+ #
1315
+ # All my state is encapsulated in an store, which means I'm not
1316
+ # generally pickleable but I am easy to reconstruct.
1317
+ class Server
1318
+ @@signatoryClass = Signatory
1319
+ @@encoderClass = SigningEncoder
1320
+ @@decoderClass = Decoder
1321
+
1322
+ # The back-end where my associations and nonces are stored.
1323
+ attr_accessor :store
1324
+
1325
+ # I'm using this for associate requests and to sign things.
1326
+ attr_accessor :signatory
1327
+
1328
+ # I'm using this to encode things.
1329
+ attr_accessor :encoder
1330
+
1331
+ # I'm using this to decode things.
1332
+ attr_accessor :decoder
1333
+
1334
+ # I use this instance of OpenID::AssociationNegotiator to
1335
+ # determine which kinds of associations I can make and how.
1336
+ attr_accessor :negotiator
1337
+
1338
+ # My URL.
1339
+ attr_accessor :op_endpoint
1340
+
1341
+ # op_endpoint is new in library version 2.0.
1342
+ def initialize(store, op_endpoint)
1343
+ @store = store
1344
+ @signatory = @@signatoryClass.new(@store)
1345
+ @encoder = @@encoderClass.new(@signatory)
1346
+ @decoder = @@decoderClass.new(self)
1347
+ @negotiator = DefaultNegotiator.copy
1348
+ @op_endpoint = op_endpoint
1349
+ end
1350
+
1351
+ # Handle a request.
1352
+ #
1353
+ # Give me a request, I will give you a response. Unless it's a
1354
+ # type of request I cannot handle myself, in which case I will
1355
+ # raise RuntimeError. In that case, you can handle it yourself,
1356
+ # or add a method to me for handling that request type.
1357
+ def handle_request(request)
1358
+ begin
1359
+ handler = method("openid_" + request.mode)
1360
+ rescue NameError
1361
+ raise format(
1362
+ "%s has no handler for a request of mode %s.",
1363
+ self,
1364
+ request.mode,
1365
+ ).to_s
1366
+ end
1367
+
1368
+ handler.call(request)
1369
+ end
1370
+
1371
+ # Handle and respond to check_authentication requests.
1372
+ def openid_check_authentication(request)
1373
+ request.answer(@signatory)
1374
+ end
1375
+
1376
+ # Handle and respond to associate requests.
1377
+ def openid_associate(request)
1378
+ assoc_type = request.assoc_type
1379
+ session_type = request.session.session_type
1380
+ if @negotiator.allowed?(assoc_type, session_type)
1381
+ assoc = @signatory.create_association(
1382
+ false,
1383
+ assoc_type,
1384
+ )
1385
+ request.answer(assoc)
1386
+ else
1387
+ message = format(
1388
+ "Association type %s is not supported with " +
1389
+ "session type %s",
1390
+ assoc_type,
1391
+ session_type,
1392
+ )
1393
+ preferred_assoc_type, preferred_session_type = @negotiator.get_allowed_type
1394
+ request.answer_unsupported(
1395
+ message,
1396
+ preferred_assoc_type,
1397
+ preferred_session_type,
1398
+ )
1399
+ end
1400
+ end
1401
+
1402
+ # Transform query parameters into an OpenIDRequest.
1403
+ # query should contain the query parameters as a Hash with
1404
+ # each key mapping to one value.
1405
+ #
1406
+ # If the query does not seem to be an OpenID request at all, I
1407
+ # return nil.
1408
+ def decode_request(query)
1409
+ @decoder.decode(query)
1410
+ end
1411
+
1412
+ # Encode a response to a WebResponse, signing it first if
1413
+ # appropriate.
1414
+ #
1415
+ # Raises EncodingError when I can't figure out how to encode this
1416
+ # message.
1417
+ #
1418
+ # Raises AlreadySigned When this response is already signed.
1419
+ def encode_response(response)
1420
+ @encoder.encode(response)
1421
+ end
1422
+ end
1423
+
1424
+ # A message did not conform to the OpenID protocol.
1425
+ class ProtocolError < Exception
1426
+ # The query that is failing to be a valid OpenID request.
1427
+ attr_accessor :openid_message
1428
+ attr_accessor :reference, :contact
1429
+
1430
+ # text:: A message about the encountered error.
1431
+ def initialize(message, text = nil, reference = nil, contact = nil)
1432
+ @openid_message = message
1433
+ @reference = reference
1434
+ @contact = contact
1435
+ Util.truthy_assert(!message.is_a?(String))
1436
+ super(text)
1437
+ end
1438
+
1439
+ # Get the return_to argument from the request, if any.
1440
+ def get_return_to
1441
+ return if @openid_message.nil?
1442
+
1443
+ @openid_message.get_arg(OPENID_NS, "return_to")
1444
+ end
1445
+
1446
+ # Did this request have a return_to parameter?
1447
+ def has_return_to
1448
+ !get_return_to.nil?
1449
+ end
1450
+
1451
+ # Generate a Message object for sending to the relying party,
1452
+ # after encoding.
1453
+ def to_message
1454
+ namespace = @openid_message.get_openid_namespace
1455
+ reply = Message.new(namespace)
1456
+ reply.set_arg(OPENID_NS, "mode", "error")
1457
+ reply.set_arg(OPENID_NS, "error", to_s)
1458
+
1459
+ reply.set_arg(OPENID_NS, "contact", @contact.to_s) if @contact
1460
+
1461
+ reply.set_arg(OPENID_NS, "reference", @reference.to_s) if @reference
1462
+
1463
+ reply
1464
+ end
1465
+
1466
+ # implements IEncodable
1467
+
1468
+ def encode_to_url
1469
+ to_message.to_url(get_return_to)
1470
+ end
1471
+
1472
+ def encode_to_kvform
1473
+ to_message.to_kvform
1474
+ end
1475
+
1476
+ def to_form_markup
1477
+ to_message.to_form_markup(get_return_to)
1478
+ end
1479
+
1480
+ def to_html
1481
+ Util.auto_submit_html(to_form_markup)
1482
+ end
1483
+
1484
+ # How should I be encoded?
1485
+ #
1486
+ # Returns one of ENCODE_URL, ENCODE_KVFORM, or None. If None,
1487
+ # I cannot be encoded as a protocol message and should be
1488
+ # displayed to the user.
1489
+ def which_encoding
1490
+ if has_return_to
1491
+ if @openid_message.is_openid2 and
1492
+ encode_to_url.length > OPENID1_URL_LIMIT
1493
+ return ENCODE_HTML_FORM
1494
+ else
1495
+ return ENCODE_URL
1496
+ end
1497
+ end
1498
+
1499
+ return if @openid_message.nil?
1500
+
1501
+ mode = @openid_message.get_arg(OPENID_NS, "mode")
1502
+ return ENCODE_KVFORM if mode && !BROWSER_REQUEST_MODES.member?(mode)
1503
+
1504
+ # If your request was so broken that you didn't manage to
1505
+ # include an openid.mode, I'm not going to worry too much
1506
+ # about returning you something you can't parse.
1507
+ nil
1508
+ end
1509
+ end
1510
+
1511
+ # Raised when an operation was attempted that is not compatible
1512
+ # with the protocol version being used.
1513
+ class VersionError < Exception
1514
+ end
1515
+
1516
+ # Raised when a response to a request cannot be generated
1517
+ # because the request contains no return_to URL.
1518
+ class NoReturnToError < Exception
1519
+ end
1520
+
1521
+ # Could not encode this as a protocol message.
1522
+ #
1523
+ # You should probably render it and show it to the user.
1524
+ class EncodingError < Exception
1525
+ # The response that failed to encode.
1526
+ attr_reader :response
1527
+
1528
+ def initialize(response)
1529
+ super
1530
+ @response = response
1531
+ end
1532
+ end
1533
+
1534
+ # This response is already signed.
1535
+ class AlreadySigned < EncodingError
1536
+ end
1537
+
1538
+ # A return_to is outside the trust_root.
1539
+ class UntrustedReturnURL < ProtocolError
1540
+ attr_reader :return_to, :trust_root
1541
+
1542
+ def initialize(message, return_to, trust_root)
1543
+ super(message)
1544
+ @return_to = return_to
1545
+ @trust_root = trust_root
1546
+ end
1547
+
1548
+ def to_s
1549
+ format(
1550
+ "return_to %s not under trust_root %s",
1551
+ @return_to,
1552
+ @trust_root,
1553
+ )
1554
+ end
1555
+ end
1556
+
1557
+ # The return_to URL doesn't look like a valid URL.
1558
+ class MalformedReturnURL < ProtocolError
1559
+ attr_reader :return_to
1560
+
1561
+ def initialize(openid_message, return_to)
1562
+ @return_to = return_to
1563
+ super(openid_message)
1564
+ end
1565
+ end
1566
+
1567
+ # The trust root is not well-formed.
1568
+ class MalformedTrustRoot < ProtocolError
1569
+ end
1570
+ end
1571
+ end