nov-ruby-openid 2.1.9

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