entp-ruby-openid 2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. data/CHANGELOG +215 -0
  2. data/INSTALL +47 -0
  3. data/LICENSE +210 -0
  4. data/NOTICE +2 -0
  5. data/README +85 -0
  6. data/UPGRADE +127 -0
  7. data/admin/runtests.rb +45 -0
  8. data/examples/README +32 -0
  9. data/examples/active_record_openid_store/README +58 -0
  10. data/examples/active_record_openid_store/XXX_add_open_id_store_to_db.rb +24 -0
  11. data/examples/active_record_openid_store/XXX_upgrade_open_id_store.rb +26 -0
  12. data/examples/active_record_openid_store/init.rb +8 -0
  13. data/examples/active_record_openid_store/lib/association.rb +10 -0
  14. data/examples/active_record_openid_store/lib/nonce.rb +3 -0
  15. data/examples/active_record_openid_store/lib/open_id_setting.rb +4 -0
  16. data/examples/active_record_openid_store/lib/openid_ar_store.rb +57 -0
  17. data/examples/active_record_openid_store/test/store_test.rb +212 -0
  18. data/examples/discover +49 -0
  19. data/examples/rails_openid/README +153 -0
  20. data/examples/rails_openid/Rakefile +10 -0
  21. data/examples/rails_openid/app/controllers/application.rb +4 -0
  22. data/examples/rails_openid/app/controllers/consumer_controller.rb +125 -0
  23. data/examples/rails_openid/app/controllers/login_controller.rb +45 -0
  24. data/examples/rails_openid/app/controllers/server_controller.rb +265 -0
  25. data/examples/rails_openid/app/helpers/application_helper.rb +3 -0
  26. data/examples/rails_openid/app/helpers/login_helper.rb +2 -0
  27. data/examples/rails_openid/app/helpers/server_helper.rb +9 -0
  28. data/examples/rails_openid/app/views/consumer/index.rhtml +81 -0
  29. data/examples/rails_openid/app/views/layouts/server.rhtml +68 -0
  30. data/examples/rails_openid/app/views/login/index.rhtml +56 -0
  31. data/examples/rails_openid/app/views/server/decide.rhtml +26 -0
  32. data/examples/rails_openid/config/boot.rb +19 -0
  33. data/examples/rails_openid/config/database.yml +74 -0
  34. data/examples/rails_openid/config/environment.rb +54 -0
  35. data/examples/rails_openid/config/environments/development.rb +19 -0
  36. data/examples/rails_openid/config/environments/production.rb +19 -0
  37. data/examples/rails_openid/config/environments/test.rb +19 -0
  38. data/examples/rails_openid/config/routes.rb +24 -0
  39. data/examples/rails_openid/doc/README_FOR_APP +2 -0
  40. data/examples/rails_openid/public/404.html +8 -0
  41. data/examples/rails_openid/public/500.html +8 -0
  42. data/examples/rails_openid/public/dispatch.cgi +12 -0
  43. data/examples/rails_openid/public/dispatch.fcgi +26 -0
  44. data/examples/rails_openid/public/dispatch.rb +12 -0
  45. data/examples/rails_openid/public/favicon.ico +0 -0
  46. data/examples/rails_openid/public/images/openid_login_bg.gif +0 -0
  47. data/examples/rails_openid/public/javascripts/controls.js +750 -0
  48. data/examples/rails_openid/public/javascripts/dragdrop.js +584 -0
  49. data/examples/rails_openid/public/javascripts/effects.js +854 -0
  50. data/examples/rails_openid/public/javascripts/prototype.js +1785 -0
  51. data/examples/rails_openid/public/robots.txt +1 -0
  52. data/examples/rails_openid/script/about +3 -0
  53. data/examples/rails_openid/script/breakpointer +3 -0
  54. data/examples/rails_openid/script/console +3 -0
  55. data/examples/rails_openid/script/destroy +3 -0
  56. data/examples/rails_openid/script/generate +3 -0
  57. data/examples/rails_openid/script/performance/benchmarker +3 -0
  58. data/examples/rails_openid/script/performance/profiler +3 -0
  59. data/examples/rails_openid/script/plugin +3 -0
  60. data/examples/rails_openid/script/process/reaper +3 -0
  61. data/examples/rails_openid/script/process/spawner +3 -0
  62. data/examples/rails_openid/script/process/spinner +3 -0
  63. data/examples/rails_openid/script/runner +3 -0
  64. data/examples/rails_openid/script/server +3 -0
  65. data/examples/rails_openid/test/functional/login_controller_test.rb +18 -0
  66. data/examples/rails_openid/test/functional/server_controller_test.rb +18 -0
  67. data/examples/rails_openid/test/test_helper.rb +28 -0
  68. data/lib/hmac/hmac.rb +112 -0
  69. data/lib/hmac/sha1.rb +11 -0
  70. data/lib/hmac/sha2.rb +25 -0
  71. data/lib/openid.rb +22 -0
  72. data/lib/openid/association.rb +249 -0
  73. data/lib/openid/consumer.rb +395 -0
  74. data/lib/openid/consumer/associationmanager.rb +344 -0
  75. data/lib/openid/consumer/checkid_request.rb +186 -0
  76. data/lib/openid/consumer/discovery.rb +497 -0
  77. data/lib/openid/consumer/discovery_manager.rb +123 -0
  78. data/lib/openid/consumer/html_parse.rb +134 -0
  79. data/lib/openid/consumer/idres.rb +523 -0
  80. data/lib/openid/consumer/responses.rb +150 -0
  81. data/lib/openid/cryptutil.rb +115 -0
  82. data/lib/openid/dh.rb +89 -0
  83. data/lib/openid/extension.rb +39 -0
  84. data/lib/openid/extensions/ax.rb +539 -0
  85. data/lib/openid/extensions/oauth.rb +91 -0
  86. data/lib/openid/extensions/pape.rb +179 -0
  87. data/lib/openid/extensions/sreg.rb +277 -0
  88. data/lib/openid/extras.rb +11 -0
  89. data/lib/openid/fetchers.rb +258 -0
  90. data/lib/openid/kvform.rb +136 -0
  91. data/lib/openid/kvpost.rb +58 -0
  92. data/lib/openid/message.rb +553 -0
  93. data/lib/openid/protocolerror.rb +12 -0
  94. data/lib/openid/server.rb +1544 -0
  95. data/lib/openid/store.rb +10 -0
  96. data/lib/openid/store/filesystem.rb +272 -0
  97. data/lib/openid/store/interface.rb +75 -0
  98. data/lib/openid/store/memcache.rb +109 -0
  99. data/lib/openid/store/memory.rb +84 -0
  100. data/lib/openid/store/nonce.rb +68 -0
  101. data/lib/openid/trustroot.rb +349 -0
  102. data/lib/openid/urinorm.rb +75 -0
  103. data/lib/openid/util.rb +119 -0
  104. data/lib/openid/version.rb +3 -0
  105. data/lib/openid/yadis.rb +15 -0
  106. data/lib/openid/yadis/accept.rb +148 -0
  107. data/lib/openid/yadis/constants.rb +21 -0
  108. data/lib/openid/yadis/discovery.rb +153 -0
  109. data/lib/openid/yadis/filters.rb +205 -0
  110. data/lib/openid/yadis/htmltokenizer.rb +305 -0
  111. data/lib/openid/yadis/parsehtml.rb +45 -0
  112. data/lib/openid/yadis/services.rb +42 -0
  113. data/lib/openid/yadis/xrds.rb +155 -0
  114. data/lib/openid/yadis/xri.rb +90 -0
  115. data/lib/openid/yadis/xrires.rb +91 -0
  116. data/test/data/test_discover/openid_utf8.html +11 -0
  117. data/test/support/test_data_mixin.rb +127 -0
  118. data/test/support/test_util.rb +53 -0
  119. data/test/support/yadis_data.rb +131 -0
  120. data/test/support/yadis_data/accept.txt +124 -0
  121. data/test/support/yadis_data/dh.txt +29 -0
  122. data/test/support/yadis_data/example-xrds.xml +14 -0
  123. data/test/support/yadis_data/linkparse.txt +587 -0
  124. data/test/support/yadis_data/n2b64 +650 -0
  125. data/test/support/yadis_data/test1-discover.txt +137 -0
  126. data/test/support/yadis_data/test1-parsehtml.txt +152 -0
  127. data/test/support/yadis_data/test_discover/malformed_meta_tag.html +19 -0
  128. data/test/support/yadis_data/test_discover/openid.html +11 -0
  129. data/test/support/yadis_data/test_discover/openid2.html +11 -0
  130. data/test/support/yadis_data/test_discover/openid2_xrds.xml +12 -0
  131. data/test/support/yadis_data/test_discover/openid2_xrds_no_local_id.xml +11 -0
  132. data/test/support/yadis_data/test_discover/openid_1_and_2.html +11 -0
  133. data/test/support/yadis_data/test_discover/openid_1_and_2_xrds.xml +16 -0
  134. data/test/support/yadis_data/test_discover/openid_1_and_2_xrds_bad_delegate.xml +17 -0
  135. data/test/support/yadis_data/test_discover/openid_and_yadis.html +12 -0
  136. data/test/support/yadis_data/test_discover/openid_no_delegate.html +10 -0
  137. data/test/support/yadis_data/test_discover/openid_utf8.html +11 -0
  138. data/test/support/yadis_data/test_discover/yadis_0entries.xml +12 -0
  139. data/test/support/yadis_data/test_discover/yadis_2_bad_local_id.xml +15 -0
  140. data/test/support/yadis_data/test_discover/yadis_2entries_delegate.xml +22 -0
  141. data/test/support/yadis_data/test_discover/yadis_2entries_idp.xml +21 -0
  142. data/test/support/yadis_data/test_discover/yadis_another_delegate.xml +14 -0
  143. data/test/support/yadis_data/test_discover/yadis_idp.xml +12 -0
  144. data/test/support/yadis_data/test_discover/yadis_idp_delegate.xml +13 -0
  145. data/test/support/yadis_data/test_discover/yadis_no_delegate.xml +11 -0
  146. data/test/support/yadis_data/test_xrds/=j3h.2007.11.14.xrds +25 -0
  147. data/test/support/yadis_data/test_xrds/README +12 -0
  148. data/test/support/yadis_data/test_xrds/delegated-20060809-r1.xrds +34 -0
  149. data/test/support/yadis_data/test_xrds/delegated-20060809-r2.xrds +34 -0
  150. data/test/support/yadis_data/test_xrds/delegated-20060809.xrds +34 -0
  151. data/test/support/yadis_data/test_xrds/no-xrd.xml +7 -0
  152. data/test/support/yadis_data/test_xrds/not-xrds.xml +2 -0
  153. data/test/support/yadis_data/test_xrds/prefixsometimes.xrds +34 -0
  154. data/test/support/yadis_data/test_xrds/ref.xrds +109 -0
  155. data/test/support/yadis_data/test_xrds/sometimesprefix.xrds +34 -0
  156. data/test/support/yadis_data/test_xrds/spoof1.xrds +25 -0
  157. data/test/support/yadis_data/test_xrds/spoof2.xrds +25 -0
  158. data/test/support/yadis_data/test_xrds/spoof3.xrds +37 -0
  159. data/test/support/yadis_data/test_xrds/status222.xrds +9 -0
  160. data/test/support/yadis_data/test_xrds/subsegments.xrds +58 -0
  161. data/test/support/yadis_data/test_xrds/valid-populated-xrds.xml +39 -0
  162. data/test/support/yadis_data/trustroot.txt +153 -0
  163. data/test/support/yadis_data/urinorm.txt +79 -0
  164. data/test/test_accept.rb +170 -0
  165. data/test/test_association.rb +268 -0
  166. data/test/test_associationmanager.rb +918 -0
  167. data/test/test_ax.rb +690 -0
  168. data/test/test_checkid_request.rb +293 -0
  169. data/test/test_consumer.rb +260 -0
  170. data/test/test_cryptutil.rb +119 -0
  171. data/test/test_dh.rb +85 -0
  172. data/test/test_discover.rb +848 -0
  173. data/test/test_discovery_manager.rb +259 -0
  174. data/test/test_extension.rb +46 -0
  175. data/test/test_extras.rb +35 -0
  176. data/test/test_fetchers.rb +554 -0
  177. data/test/test_filters.rb +269 -0
  178. data/test/test_helper.rb +4 -0
  179. data/test/test_idres.rb +961 -0
  180. data/test/test_kvform.rb +164 -0
  181. data/test/test_kvpost.rb +64 -0
  182. data/test/test_linkparse.rb +100 -0
  183. data/test/test_message.rb +1115 -0
  184. data/test/test_nonce.rb +89 -0
  185. data/test/test_oauth.rb +176 -0
  186. data/test/test_openid_yadis.rb +177 -0
  187. data/test/test_pape.rb +248 -0
  188. data/test/test_parsehtml.rb +79 -0
  189. data/test/test_responses.rb +63 -0
  190. data/test/test_server.rb +2455 -0
  191. data/test/test_sreg.rb +479 -0
  192. data/test/test_stores.rb +292 -0
  193. data/test/test_trustroot.rb +111 -0
  194. data/test/test_urinorm.rb +34 -0
  195. data/test/test_util.rb +145 -0
  196. data/test/test_xrds.rb +167 -0
  197. data/test/test_xri.rb +48 -0
  198. data/test/test_xrires.rb +67 -0
  199. data/test/test_yadis_discovery.rb +218 -0
  200. metadata +268 -0
@@ -0,0 +1,12 @@
1
+ require 'openid/util'
2
+
3
+ module OpenID
4
+
5
+ # An error in the OpenID protocol
6
+ #
7
+ # Note: there is also a OpenID::Server::ProtocolError which is
8
+ # a distinct class used exclusively in Server contexts
9
+ #
10
+ class ProtocolError < OpenIDError
11
+ end
12
+ 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