pelle-ruby-openid 2.1.8

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 (197) 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 +82 -0
  7. data/UPGRADE +127 -0
  8. data/VERSION +1 -0
  9. data/examples/README +32 -0
  10. data/examples/active_record_openid_store/README +58 -0
  11. data/examples/active_record_openid_store/XXX_add_open_id_store_to_db.rb +24 -0
  12. data/examples/active_record_openid_store/XXX_upgrade_open_id_store.rb +26 -0
  13. data/examples/active_record_openid_store/init.rb +8 -0
  14. data/examples/active_record_openid_store/lib/association.rb +10 -0
  15. data/examples/active_record_openid_store/lib/nonce.rb +3 -0
  16. data/examples/active_record_openid_store/lib/open_id_setting.rb +4 -0
  17. data/examples/active_record_openid_store/lib/openid_ar_store.rb +57 -0
  18. data/examples/active_record_openid_store/test/store_test.rb +212 -0
  19. data/examples/discover +49 -0
  20. data/examples/rails_openid/README +153 -0
  21. data/examples/rails_openid/Rakefile +10 -0
  22. data/examples/rails_openid/app/controllers/application.rb +4 -0
  23. data/examples/rails_openid/app/controllers/consumer_controller.rb +122 -0
  24. data/examples/rails_openid/app/controllers/login_controller.rb +45 -0
  25. data/examples/rails_openid/app/controllers/server_controller.rb +265 -0
  26. data/examples/rails_openid/app/helpers/application_helper.rb +3 -0
  27. data/examples/rails_openid/app/helpers/login_helper.rb +2 -0
  28. data/examples/rails_openid/app/helpers/server_helper.rb +9 -0
  29. data/examples/rails_openid/app/views/consumer/index.rhtml +81 -0
  30. data/examples/rails_openid/app/views/layouts/server.rhtml +68 -0
  31. data/examples/rails_openid/app/views/login/index.rhtml +56 -0
  32. data/examples/rails_openid/app/views/server/decide.rhtml +26 -0
  33. data/examples/rails_openid/config/boot.rb +19 -0
  34. data/examples/rails_openid/config/database.yml +74 -0
  35. data/examples/rails_openid/config/environment.rb +54 -0
  36. data/examples/rails_openid/config/environments/development.rb +19 -0
  37. data/examples/rails_openid/config/environments/production.rb +19 -0
  38. data/examples/rails_openid/config/environments/test.rb +19 -0
  39. data/examples/rails_openid/config/routes.rb +24 -0
  40. data/examples/rails_openid/doc/README_FOR_APP +2 -0
  41. data/examples/rails_openid/public/.htaccess +40 -0
  42. data/examples/rails_openid/public/404.html +8 -0
  43. data/examples/rails_openid/public/500.html +8 -0
  44. data/examples/rails_openid/public/dispatch.cgi +12 -0
  45. data/examples/rails_openid/public/dispatch.fcgi +26 -0
  46. data/examples/rails_openid/public/dispatch.rb +12 -0
  47. data/examples/rails_openid/public/favicon.ico +0 -0
  48. data/examples/rails_openid/public/images/openid_login_bg.gif +0 -0
  49. data/examples/rails_openid/public/javascripts/controls.js +750 -0
  50. data/examples/rails_openid/public/javascripts/dragdrop.js +584 -0
  51. data/examples/rails_openid/public/javascripts/effects.js +854 -0
  52. data/examples/rails_openid/public/javascripts/prototype.js +1785 -0
  53. data/examples/rails_openid/public/robots.txt +1 -0
  54. data/examples/rails_openid/script/about +3 -0
  55. data/examples/rails_openid/script/breakpointer +3 -0
  56. data/examples/rails_openid/script/console +3 -0
  57. data/examples/rails_openid/script/destroy +3 -0
  58. data/examples/rails_openid/script/generate +3 -0
  59. data/examples/rails_openid/script/performance/benchmarker +3 -0
  60. data/examples/rails_openid/script/performance/profiler +3 -0
  61. data/examples/rails_openid/script/plugin +3 -0
  62. data/examples/rails_openid/script/process/reaper +3 -0
  63. data/examples/rails_openid/script/process/spawner +3 -0
  64. data/examples/rails_openid/script/process/spinner +3 -0
  65. data/examples/rails_openid/script/runner +3 -0
  66. data/examples/rails_openid/script/server +3 -0
  67. data/examples/rails_openid/test/functional/login_controller_test.rb +18 -0
  68. data/examples/rails_openid/test/functional/server_controller_test.rb +18 -0
  69. data/examples/rails_openid/test/test_helper.rb +28 -0
  70. data/lib/hmac/hmac.rb +112 -0
  71. data/lib/hmac/sha1.rb +11 -0
  72. data/lib/hmac/sha2.rb +25 -0
  73. data/lib/openid/association.rb +249 -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 +498 -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 +148 -0
  81. data/lib/openid/consumer.rb +395 -0
  82. data/lib/openid/cryptutil.rb +97 -0
  83. data/lib/openid/dh.rb +89 -0
  84. data/lib/openid/extension.rb +39 -0
  85. data/lib/openid/extensions/ax.rb +516 -0
  86. data/lib/openid/extensions/oauth.rb +91 -0
  87. data/lib/openid/extensions/pape.rb +179 -0
  88. data/lib/openid/extensions/sreg.rb +277 -0
  89. data/lib/openid/extras.rb +11 -0
  90. data/lib/openid/fetchers.rb +238 -0
  91. data/lib/openid/kvform.rb +136 -0
  92. data/lib/openid/kvpost.rb +58 -0
  93. data/lib/openid/message.rb +553 -0
  94. data/lib/openid/protocolerror.rb +8 -0
  95. data/lib/openid/server.rb +1544 -0
  96. data/lib/openid/store/filesystem.rb +271 -0
  97. data/lib/openid/store/interface.rb +75 -0
  98. data/lib/openid/store/memcache.rb +107 -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 +110 -0
  104. data/lib/openid/yadis/accept.rb +148 -0
  105. data/lib/openid/yadis/constants.rb +21 -0
  106. data/lib/openid/yadis/discovery.rb +153 -0
  107. data/lib/openid/yadis/filters.rb +205 -0
  108. data/lib/openid/yadis/htmltokenizer.rb +305 -0
  109. data/lib/openid/yadis/parsehtml.rb +45 -0
  110. data/lib/openid/yadis/services.rb +42 -0
  111. data/lib/openid/yadis/xrds.rb +155 -0
  112. data/lib/openid/yadis/xri.rb +90 -0
  113. data/lib/openid/yadis/xrires.rb +106 -0
  114. data/lib/openid.rb +20 -0
  115. data/setup.rb +1551 -0
  116. data/test/data/accept.txt +124 -0
  117. data/test/data/dh.txt +29 -0
  118. data/test/data/example-xrds.xml +14 -0
  119. data/test/data/linkparse.txt +587 -0
  120. data/test/data/n2b64 +650 -0
  121. data/test/data/test1-discover.txt +137 -0
  122. data/test/data/test1-parsehtml.txt +152 -0
  123. data/test/data/test_discover/malformed_meta_tag.html +19 -0
  124. data/test/data/test_discover/openid.html +11 -0
  125. data/test/data/test_discover/openid2.html +11 -0
  126. data/test/data/test_discover/openid2_xrds.xml +12 -0
  127. data/test/data/test_discover/openid2_xrds_no_local_id.xml +11 -0
  128. data/test/data/test_discover/openid_1_and_2.html +11 -0
  129. data/test/data/test_discover/openid_1_and_2_xrds.xml +16 -0
  130. data/test/data/test_discover/openid_1_and_2_xrds_bad_delegate.xml +17 -0
  131. data/test/data/test_discover/openid_and_yadis.html +12 -0
  132. data/test/data/test_discover/openid_no_delegate.html +10 -0
  133. data/test/data/test_discover/yadis_0entries.xml +12 -0
  134. data/test/data/test_discover/yadis_2_bad_local_id.xml +15 -0
  135. data/test/data/test_discover/yadis_2entries_delegate.xml +22 -0
  136. data/test/data/test_discover/yadis_2entries_idp.xml +21 -0
  137. data/test/data/test_discover/yadis_another_delegate.xml +14 -0
  138. data/test/data/test_discover/yadis_idp.xml +12 -0
  139. data/test/data/test_discover/yadis_idp_delegate.xml +13 -0
  140. data/test/data/test_discover/yadis_no_delegate.xml +11 -0
  141. data/test/data/test_xrds/=j3h.2007.11.14.xrds +25 -0
  142. data/test/data/test_xrds/README +12 -0
  143. data/test/data/test_xrds/delegated-20060809-r1.xrds +34 -0
  144. data/test/data/test_xrds/delegated-20060809-r2.xrds +34 -0
  145. data/test/data/test_xrds/delegated-20060809.xrds +34 -0
  146. data/test/data/test_xrds/no-xrd.xml +7 -0
  147. data/test/data/test_xrds/not-xrds.xml +2 -0
  148. data/test/data/test_xrds/prefixsometimes.xrds +34 -0
  149. data/test/data/test_xrds/ref.xrds +109 -0
  150. data/test/data/test_xrds/sometimesprefix.xrds +34 -0
  151. data/test/data/test_xrds/spoof1.xrds +25 -0
  152. data/test/data/test_xrds/spoof2.xrds +25 -0
  153. data/test/data/test_xrds/spoof3.xrds +37 -0
  154. data/test/data/test_xrds/status222.xrds +9 -0
  155. data/test/data/test_xrds/subsegments.xrds +58 -0
  156. data/test/data/test_xrds/valid-populated-xrds.xml +39 -0
  157. data/test/data/trustroot.txt +153 -0
  158. data/test/data/urinorm.txt +79 -0
  159. data/test/discoverdata.rb +131 -0
  160. data/test/test_accept.rb +170 -0
  161. data/test/test_association.rb +266 -0
  162. data/test/test_associationmanager.rb +917 -0
  163. data/test/test_ax.rb +648 -0
  164. data/test/test_checkid_request.rb +294 -0
  165. data/test/test_consumer.rb +257 -0
  166. data/test/test_cryptutil.rb +119 -0
  167. data/test/test_dh.rb +86 -0
  168. data/test/test_discover.rb +838 -0
  169. data/test/test_discovery_manager.rb +262 -0
  170. data/test/test_extension.rb +46 -0
  171. data/test/test_extras.rb +35 -0
  172. data/test/test_fetchers.rb +538 -0
  173. data/test/test_filters.rb +270 -0
  174. data/test/test_idres.rb +963 -0
  175. data/test/test_kvform.rb +165 -0
  176. data/test/test_kvpost.rb +65 -0
  177. data/test/test_linkparse.rb +101 -0
  178. data/test/test_message.rb +1116 -0
  179. data/test/test_nonce.rb +89 -0
  180. data/test/test_oauth.rb +175 -0
  181. data/test/test_openid_yadis.rb +178 -0
  182. data/test/test_pape.rb +247 -0
  183. data/test/test_parsehtml.rb +80 -0
  184. data/test/test_responses.rb +63 -0
  185. data/test/test_server.rb +2457 -0
  186. data/test/test_sreg.rb +479 -0
  187. data/test/test_stores.rb +298 -0
  188. data/test/test_trustroot.rb +113 -0
  189. data/test/test_urinorm.rb +35 -0
  190. data/test/test_util.rb +145 -0
  191. data/test/test_xrds.rb +169 -0
  192. data/test/test_xri.rb +48 -0
  193. data/test/test_xrires.rb +63 -0
  194. data/test/test_yadis_discovery.rb +220 -0
  195. data/test/testutil.rb +127 -0
  196. data/test/util.rb +53 -0
  197. metadata +316 -0
@@ -0,0 +1,344 @@
1
+ require "openid/dh"
2
+ require "openid/util"
3
+ require "openid/kvpost"
4
+ require "openid/cryptutil"
5
+ require "openid/protocolerror"
6
+ require "openid/association"
7
+
8
+ module OpenID
9
+ class Consumer
10
+
11
+ # A superclass for implementing Diffie-Hellman association sessions.
12
+ class DiffieHellmanSession
13
+ class << self
14
+ attr_reader :session_type, :secret_size, :allowed_assoc_types,
15
+ :hashfunc
16
+ end
17
+
18
+ def initialize(dh=nil)
19
+ if dh.nil?
20
+ dh = DiffieHellman.from_defaults
21
+ end
22
+ @dh = dh
23
+ end
24
+
25
+ # Return the query parameters for requesting an association
26
+ # using this Diffie-Hellman association session
27
+ def get_request
28
+ args = {'dh_consumer_public' => CryptUtil.num_to_base64(@dh.public)}
29
+ if (!@dh.using_default_values?)
30
+ args['dh_modulus'] = CryptUtil.num_to_base64(@dh.modulus)
31
+ args['dh_gen'] = CryptUtil.num_to_base64(@dh.generator)
32
+ end
33
+
34
+ return args
35
+ end
36
+
37
+ # Process the response from a successful association request and
38
+ # return the shared secret for this association
39
+ def extract_secret(response)
40
+ dh_server_public64 = response.get_arg(OPENID_NS, 'dh_server_public',
41
+ NO_DEFAULT)
42
+ enc_mac_key64 = response.get_arg(OPENID_NS, 'enc_mac_key', NO_DEFAULT)
43
+ dh_server_public = CryptUtil.base64_to_num(dh_server_public64)
44
+ enc_mac_key = Util.from_base64(enc_mac_key64)
45
+ return @dh.xor_secret(self.class.hashfunc,
46
+ dh_server_public, enc_mac_key)
47
+ end
48
+ end
49
+
50
+ # A Diffie-Hellman association session that uses SHA1 as its hash
51
+ # function
52
+ class DiffieHellmanSHA1Session < DiffieHellmanSession
53
+ @session_type = 'DH-SHA1'
54
+ @secret_size = 20
55
+ @allowed_assoc_types = ['HMAC-SHA1']
56
+ @hashfunc = CryptUtil.method(:sha1)
57
+ end
58
+
59
+ # A Diffie-Hellman association session that uses SHA256 as its hash
60
+ # function
61
+ class DiffieHellmanSHA256Session < DiffieHellmanSession
62
+ @session_type = 'DH-SHA256'
63
+ @secret_size = 32
64
+ @allowed_assoc_types = ['HMAC-SHA256']
65
+ @hashfunc = CryptUtil.method(:sha256)
66
+ end
67
+
68
+ # An association session that does not use encryption
69
+ class NoEncryptionSession
70
+ class << self
71
+ attr_reader :session_type, :allowed_assoc_types
72
+ end
73
+ @session_type = 'no-encryption'
74
+ @allowed_assoc_types = ['HMAC-SHA1', 'HMAC-SHA256']
75
+
76
+ def get_request
77
+ return {}
78
+ end
79
+
80
+ def extract_secret(response)
81
+ mac_key64 = response.get_arg(OPENID_NS, 'mac_key', NO_DEFAULT)
82
+ return Util.from_base64(mac_key64)
83
+ end
84
+ end
85
+
86
+ # An object that manages creating and storing associations for an
87
+ # OpenID provider endpoint
88
+ class AssociationManager
89
+ def self.create_session(session_type)
90
+ case session_type
91
+ when 'no-encryption'
92
+ NoEncryptionSession.new
93
+ when 'DH-SHA1'
94
+ DiffieHellmanSHA1Session.new
95
+ when 'DH-SHA256'
96
+ DiffieHellmanSHA256Session.new
97
+ else
98
+ raise ArgumentError, "Unknown association session type: "\
99
+ "#{session_type.inspect}"
100
+ end
101
+ end
102
+
103
+ def initialize(store, server_url, compatibility_mode=false,
104
+ negotiator=nil)
105
+ @store = store
106
+ @server_url = server_url
107
+ @compatibility_mode = compatibility_mode
108
+ @negotiator = negotiator || DefaultNegotiator
109
+ end
110
+
111
+ def get_association
112
+ if @store.nil?
113
+ return nil
114
+ end
115
+
116
+ assoc = @store.get_association(@server_url)
117
+ if assoc.nil? || assoc.expires_in <= 0
118
+ assoc = negotiate_association
119
+ if !assoc.nil?
120
+ @store.store_association(@server_url, assoc)
121
+ end
122
+ end
123
+
124
+ return assoc
125
+ end
126
+
127
+ def negotiate_association
128
+ assoc_type, session_type = @negotiator.get_allowed_type
129
+ begin
130
+ return request_association(assoc_type, session_type)
131
+ rescue ServerError => why
132
+ supported_types = extract_supported_association_type(why, assoc_type)
133
+ if !supported_types.nil?
134
+ # Attempt to create an association from the assoc_type and
135
+ # session_type that the server told us it supported.
136
+ assoc_type, session_type = supported_types
137
+ begin
138
+ return request_association(assoc_type, session_type)
139
+ rescue ServerError => why
140
+ Util.log("Server #{@server_url} refused its suggested " \
141
+ "association type: session_type=#{session_type}, " \
142
+ "assoc_type=#{assoc_type}")
143
+ return nil
144
+ end
145
+ end
146
+ rescue InvalidOpenIDNamespace
147
+ Util.log("Server #{@server_url} returned a malformed association " \
148
+ "response. Falling back to check_id mode for this request.")
149
+ return nil
150
+ end
151
+ end
152
+
153
+ protected
154
+ def extract_supported_association_type(server_error, assoc_type)
155
+ # Any error message whose code is not 'unsupported-type' should
156
+ # be considered a total failure.
157
+ if (server_error.error_code != 'unsupported-type' or
158
+ server_error.message.is_openid1)
159
+ Util.log("Server error when requesting an association from "\
160
+ "#{@server_url}: #{server_error.error_text}")
161
+ return nil
162
+ end
163
+
164
+ # The server didn't like the association/session type that we
165
+ # sent, and it sent us back a message that might tell us how to
166
+ # handle it.
167
+ Util.log("Unsupported association type #{assoc_type}: "\
168
+ "#{server_error.error_text}")
169
+
170
+ # Extract the session_type and assoc_type from the error message
171
+ assoc_type = server_error.message.get_arg(OPENID_NS, 'assoc_type')
172
+ session_type = server_error.message.get_arg(OPENID_NS, 'session_type')
173
+
174
+ if assoc_type.nil? or session_type.nil?
175
+ Util.log("Server #{@server_url} responded with unsupported "\
176
+ "association session but did not supply a fallback.")
177
+ return nil
178
+ elsif !@negotiator.allowed?(assoc_type, session_type)
179
+ Util.log("Server sent unsupported session/association type: "\
180
+ "session_type=#{session_type}, assoc_type=#{assoc_type}")
181
+ return nil
182
+ else
183
+ return [assoc_type, session_type]
184
+ end
185
+ end
186
+
187
+ # Make and process one association request to this endpoint's OP
188
+ # endpoint URL. Returns an association object or nil if the
189
+ # association processing failed. Raises ServerError when the
190
+ # remote OpenID server returns an error.
191
+ def request_association(assoc_type, session_type)
192
+ assoc_session, args = create_associate_request(assoc_type, session_type)
193
+
194
+ begin
195
+ response = OpenID.make_kv_post(args, @server_url)
196
+ return extract_association(response, assoc_session)
197
+ rescue HTTPStatusError => why
198
+ Util.log("Got HTTP status error when requesting association: #{why}")
199
+ return nil
200
+ rescue Message::KeyNotFound => why
201
+ Util.log("Missing required parameter in response from "\
202
+ "#{@server_url}: #{why}")
203
+ return nil
204
+
205
+ rescue ProtocolError => why
206
+ Util.log("Protocol error processing response from #{@server_url}: "\
207
+ "#{why}")
208
+ return nil
209
+ end
210
+ end
211
+
212
+ # Create an association request for the given assoc_type and
213
+ # session_type. Returns a pair of the association session object
214
+ # and the request message that will be sent to the server.
215
+ def create_associate_request(assoc_type, session_type)
216
+ assoc_session = self.class.create_session(session_type)
217
+ args = {
218
+ 'mode' => 'associate',
219
+ 'assoc_type' => assoc_type,
220
+ }
221
+
222
+ if !@compatibility_mode
223
+ args['ns'] = OPENID2_NS
224
+ end
225
+
226
+ # Leave out the session type if we're in compatibility mode
227
+ # *and* it's no-encryption.
228
+ if !@compatibility_mode ||
229
+ assoc_session.class.session_type != 'no-encryption'
230
+ args['session_type'] = assoc_session.class.session_type
231
+ end
232
+
233
+ args.merge!(assoc_session.get_request)
234
+ message = Message.from_openid_args(args)
235
+ return assoc_session, message
236
+ end
237
+
238
+ # Given an association response message, extract the OpenID 1.X
239
+ # session type. Returns the association type for this message
240
+ #
241
+ # This function mostly takes care of the 'no-encryption' default
242
+ # behavior in OpenID 1.
243
+ #
244
+ # If the association type is plain-text, this function will
245
+ # return 'no-encryption'
246
+ def get_openid1_session_type(assoc_response)
247
+ # If it's an OpenID 1 message, allow session_type to default
248
+ # to nil (which signifies "no-encryption")
249
+ session_type = assoc_response.get_arg(OPENID_NS, 'session_type')
250
+
251
+ # Handle the differences between no-encryption association
252
+ # respones in OpenID 1 and 2:
253
+
254
+ # no-encryption is not really a valid session type for
255
+ # OpenID 1, but we'll accept it anyway, while issuing a
256
+ # warning.
257
+ if session_type == 'no-encryption'
258
+ Util.log("WARNING: #{@server_url} sent 'no-encryption'"\
259
+ "for OpenID 1.X")
260
+
261
+ # Missing or empty session type is the way to flag a
262
+ # 'no-encryption' response. Change the session type to
263
+ # 'no-encryption' so that it can be handled in the same
264
+ # way as OpenID 2 'no-encryption' respones.
265
+ elsif session_type == '' || session_type.nil?
266
+ session_type = 'no-encryption'
267
+ end
268
+
269
+ return session_type
270
+ end
271
+
272
+ def self.extract_expires_in(message)
273
+ # expires_in should be a base-10 string.
274
+ expires_in_str = message.get_arg(OPENID_NS, 'expires_in', NO_DEFAULT)
275
+ if !(/\A\d+\Z/ =~ expires_in_str)
276
+ raise ProtocolError, "Invalid expires_in field: #{expires_in_str}"
277
+ end
278
+ expires_in_str.to_i
279
+ end
280
+
281
+ # Attempt to extract an association from the response, given the
282
+ # association response message and the established association
283
+ # session.
284
+ def extract_association(assoc_response, assoc_session)
285
+ # Extract the common fields from the response, raising an
286
+ # exception if they are not found
287
+ assoc_type = assoc_response.get_arg(OPENID_NS, 'assoc_type',
288
+ NO_DEFAULT)
289
+ assoc_handle = assoc_response.get_arg(OPENID_NS, 'assoc_handle',
290
+ NO_DEFAULT)
291
+ expires_in = self.class.extract_expires_in(assoc_response)
292
+
293
+ # OpenID 1 has funny association session behaviour.
294
+ if assoc_response.is_openid1
295
+ session_type = get_openid1_session_type(assoc_response)
296
+ else
297
+ session_type = assoc_response.get_arg(OPENID2_NS, 'session_type',
298
+ NO_DEFAULT)
299
+ end
300
+
301
+ # Session type mismatch
302
+ if assoc_session.class.session_type != session_type
303
+ if (assoc_response.is_openid1 and session_type == 'no-encryption')
304
+ # In OpenID 1, any association request can result in a
305
+ # 'no-encryption' association response. Setting
306
+ # assoc_session to a new no-encryption session should
307
+ # make the rest of this function work properly for
308
+ # that case.
309
+ assoc_session = NoEncryptionSession.new
310
+ else
311
+ # Any other mismatch, regardless of protocol version
312
+ # results in the failure of the association session
313
+ # altogether.
314
+ raise ProtocolError, "Session type mismatch. Expected "\
315
+ "#{assoc_session.class.session_type}, got "\
316
+ "#{session_type}"
317
+ end
318
+ end
319
+
320
+ # Make sure assoc_type is valid for session_type
321
+ if !assoc_session.class.allowed_assoc_types.member?(assoc_type)
322
+ raise ProtocolError, "Unsupported assoc_type for session "\
323
+ "#{assoc_session.class.session_type} "\
324
+ "returned: #{assoc_type}"
325
+ end
326
+
327
+ # Delegate to the association session to extract the secret
328
+ # from the response, however is appropriate for that session
329
+ # type.
330
+ begin
331
+ secret = assoc_session.extract_secret(assoc_response)
332
+ rescue Message::KeyNotFound, ArgumentError => why
333
+ raise ProtocolError, "Malformed response for "\
334
+ "#{assoc_session.class.session_type} "\
335
+ "session: #{why.message}"
336
+ end
337
+
338
+
339
+ return Association.from_expires_in(expires_in, assoc_handle, secret,
340
+ assoc_type)
341
+ end
342
+ end
343
+ end
344
+ end
@@ -0,0 +1,186 @@
1
+ require "openid/message"
2
+ require "openid/util"
3
+
4
+ module OpenID
5
+ class Consumer
6
+ # An object that holds the state necessary for generating an
7
+ # OpenID authentication request. This object holds the association
8
+ # with the server and the discovered information with which the
9
+ # request will be made.
10
+ #
11
+ # It is separate from the consumer because you may wish to add
12
+ # things to the request before sending it on its way to the
13
+ # server. It also has serialization options that let you encode
14
+ # the authentication request as a URL or as a form POST.
15
+ class CheckIDRequest
16
+ attr_accessor :return_to_args, :message
17
+ attr_reader :endpoint
18
+
19
+ # Users of this library should not create instances of this
20
+ # class. Instances of this class are created by the library
21
+ # when needed.
22
+ def initialize(assoc, endpoint)
23
+ @assoc = assoc
24
+ @endpoint = endpoint
25
+ @return_to_args = {}
26
+ @message = Message.new(endpoint.preferred_namespace)
27
+ @anonymous = false
28
+ end
29
+
30
+ attr_reader :anonymous
31
+
32
+ # Set whether this request should be made anonymously. If a
33
+ # request is anonymous, the identifier will not be sent in the
34
+ # request. This is only useful if you are making another kind of
35
+ # request with an extension in this request.
36
+ #
37
+ # Anonymous requests are not allowed when the request is made
38
+ # with OpenID 1.
39
+ def anonymous=(is_anonymous)
40
+ if is_anonymous && @message.is_openid1
41
+ raise ArgumentError, ("OpenID1 requests MUST include the "\
42
+ "identifier in the request")
43
+ end
44
+ @anonymous = is_anonymous
45
+ end
46
+
47
+ # Add an object that implements the extension interface for
48
+ # adding arguments to an OpenID message to this checkid request.
49
+ #
50
+ # extension_request: an OpenID::Extension object.
51
+ def add_extension(extension_request)
52
+ extension_request.to_message(@message)
53
+ end
54
+
55
+ # Add an extension argument to this OpenID authentication
56
+ # request. You probably want to use add_extension and the
57
+ # OpenID::Extension interface.
58
+ #
59
+ # Use caution when adding arguments, because they will be
60
+ # URL-escaped and appended to the redirect URL, which can easily
61
+ # get quite long.
62
+ def add_extension_arg(namespace, key, value)
63
+ @message.set_arg(namespace, key, value)
64
+ end
65
+
66
+ # Produce a OpenID::Message representing this request.
67
+ #
68
+ # Not specifying a return_to URL means that the user will not be
69
+ # returned to the site issuing the request upon its completion.
70
+ #
71
+ # If immediate mode is requested, the OpenID provider is to send
72
+ # back a response immediately, useful for behind-the-scenes
73
+ # authentication attempts. Otherwise the OpenID provider may
74
+ # engage the user before providing a response. This is the
75
+ # default case, as the user may need to provide credentials or
76
+ # approve the request before a positive response can be sent.
77
+ def get_message(realm, return_to=nil, immediate=false)
78
+ if !return_to.nil?
79
+ return_to = Util.append_args(return_to, @return_to_args)
80
+ elsif immediate
81
+ raise ArgumentError, ('"return_to" is mandatory when using '\
82
+ '"checkid_immediate"')
83
+ elsif @message.is_openid1
84
+ raise ArgumentError, ('"return_to" is mandatory for OpenID 1 '\
85
+ 'requests')
86
+ elsif @return_to_args.empty?
87
+ raise ArgumentError, ('extra "return_to" arguments were specified, '\
88
+ 'but no return_to was specified')
89
+ end
90
+
91
+
92
+ message = @message.copy
93
+
94
+ mode = immediate ? 'checkid_immediate' : 'checkid_setup'
95
+ message.set_arg(OPENID_NS, 'mode', mode)
96
+
97
+ realm_key = message.is_openid1 ? 'trust_root' : 'realm'
98
+ message.set_arg(OPENID_NS, realm_key, realm)
99
+
100
+ if !return_to.nil?
101
+ message.set_arg(OPENID_NS, 'return_to', return_to)
102
+ end
103
+
104
+ if not @anonymous
105
+ if @endpoint.is_op_identifier
106
+ # This will never happen when we're in OpenID 1
107
+ # compatibility mode, as long as is_op_identifier()
108
+ # returns false whenever preferred_namespace returns
109
+ # OPENID1_NS.
110
+ claimed_id = request_identity = IDENTIFIER_SELECT
111
+ else
112
+ request_identity = @endpoint.get_local_id
113
+ claimed_id = @endpoint.claimed_id
114
+ end
115
+
116
+ # This is true for both OpenID 1 and 2
117
+ message.set_arg(OPENID_NS, 'identity', request_identity)
118
+
119
+ if message.is_openid2
120
+ message.set_arg(OPENID2_NS, 'claimed_id', claimed_id)
121
+ end
122
+ end
123
+
124
+ if @assoc
125
+ message.set_arg(OPENID_NS, 'assoc_handle', @assoc.handle)
126
+ assoc_log_msg = "with assocication #{@assoc.handle}"
127
+ else
128
+ assoc_log_msg = 'using stateless mode.'
129
+ end
130
+
131
+ Util.log("Generated #{mode} request to #{@endpoint.server_url} "\
132
+ "#{assoc_log_msg}")
133
+ return message
134
+ end
135
+
136
+ # Returns a URL with an encoded OpenID request.
137
+ #
138
+ # The resulting URL is the OpenID provider's endpoint URL with
139
+ # parameters appended as query arguments. You should redirect
140
+ # the user agent to this URL.
141
+ #
142
+ # OpenID 2.0 endpoints also accept POST requests, see
143
+ # 'send_redirect?' and 'form_markup'.
144
+ def redirect_url(realm, return_to=nil, immediate=false)
145
+ message = get_message(realm, return_to, immediate)
146
+ return message.to_url(@endpoint.server_url)
147
+ end
148
+
149
+ # Get html for a form to submit this request to the IDP.
150
+ #
151
+ # form_tag_attrs is a hash of attributes to be added to the form
152
+ # tag. 'accept-charset' and 'enctype' have defaults that can be
153
+ # overridden. If a value is supplied for 'action' or 'method',
154
+ # it will be replaced.
155
+ def form_markup(realm, return_to=nil, immediate=false,
156
+ form_tag_attrs=nil)
157
+ message = get_message(realm, return_to, immediate)
158
+ return message.to_form_markup(@endpoint.server_url, form_tag_attrs)
159
+ end
160
+
161
+ # Get a complete HTML document that autosubmits the request to the IDP
162
+ # with javascript. This method wraps form_markup - see that method's
163
+ # documentation for help with the parameters.
164
+ def html_markup(realm, return_to=nil, immediate=false,
165
+ form_tag_attrs=nil)
166
+ Util.auto_submit_html(form_markup(realm,
167
+ return_to,
168
+ immediate,
169
+ form_tag_attrs))
170
+ end
171
+
172
+ # Should this OpenID authentication request be sent as a HTTP
173
+ # redirect or as a POST (form submission)?
174
+ #
175
+ # This takes the same parameters as redirect_url or form_markup
176
+ def send_redirect?(realm, return_to=nil, immediate=false)
177
+ if @endpoint.compatibility_mode
178
+ return true
179
+ else
180
+ url = redirect_url(realm, return_to, immediate)
181
+ return url.length <= OPENID1_URL_LIMIT
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end