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,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