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,395 @@
1
+ require "openid/consumer/idres.rb"
2
+ require "openid/consumer/checkid_request.rb"
3
+ require "openid/consumer/associationmanager.rb"
4
+ require "openid/consumer/responses.rb"
5
+ require "openid/consumer/discovery_manager"
6
+ require "openid/consumer/discovery"
7
+ require "openid/message"
8
+ require "openid/yadis/discovery"
9
+ require "openid/store/nonce"
10
+
11
+ module OpenID
12
+ # OpenID support for Relying Parties (aka Consumers).
13
+ #
14
+ # This module documents the main interface with the OpenID consumer
15
+ # library. The only part of the library which has to be used and
16
+ # isn't documented in full here is the store required to create an
17
+ # Consumer instance.
18
+ #
19
+ # = OVERVIEW
20
+ #
21
+ # The OpenID identity verification process most commonly uses the
22
+ # following steps, as visible to the user of this library:
23
+ #
24
+ # 1. The user enters their OpenID into a field on the consumer's
25
+ # site, and hits a login button.
26
+ #
27
+ # 2. The consumer site discovers the user's OpenID provider using
28
+ # the Yadis protocol.
29
+ #
30
+ # 3. The consumer site sends the browser a redirect to the OpenID
31
+ # provider. This is the authentication request as described in
32
+ # the OpenID specification.
33
+ #
34
+ # 4. The OpenID provider's site sends the browser a redirect back to
35
+ # the consumer site. This redirect contains the provider's
36
+ # response to the authentication request.
37
+ #
38
+ # The most important part of the flow to note is the consumer's site
39
+ # must handle two separate HTTP requests in order to perform the
40
+ # full identity check.
41
+ #
42
+ # = LIBRARY DESIGN
43
+ #
44
+ # This consumer library is designed with that flow in mind. The
45
+ # goal is to make it as easy as possible to perform the above steps
46
+ # securely.
47
+ #
48
+ # At a high level, there are two important parts in the consumer
49
+ # library. The first important part is this module, which contains
50
+ # the interface to actually use this library. The second is
51
+ # openid/store/interface.rb, which describes the interface to use if
52
+ # you need to create a custom method for storing the state this
53
+ # library needs to maintain between requests.
54
+ #
55
+ # In general, the second part is less important for users of the
56
+ # library to know about, as several implementations are provided
57
+ # which cover a wide variety of situations in which consumers may
58
+ # use the library.
59
+ #
60
+ # The Consumer class has methods corresponding to the actions
61
+ # necessary in each of steps 2, 3, and 4 described in the overview.
62
+ # Use of this library should be as easy as creating an Consumer
63
+ # instance and calling the methods appropriate for the action the
64
+ # site wants to take.
65
+ #
66
+ # This library automatically detects which version of the OpenID
67
+ # protocol should be used for a transaction and constructs the
68
+ # proper requests and responses. Users of this library do not need
69
+ # to worry about supporting multiple protocol versions; the library
70
+ # supports them implicitly. Depending on the version of the
71
+ # protocol in use, the OpenID transaction may be more secure. See
72
+ # the OpenID specifications for more information.
73
+ #
74
+ # = SESSIONS, STORES, AND STATELESS MODE
75
+ #
76
+ # The Consumer object keeps track of two types of state:
77
+ #
78
+ # 1. State of the user's current authentication attempt. Things
79
+ # like the identity URL, the list of endpoints discovered for
80
+ # that URL, and in case where some endpoints are unreachable, the
81
+ # list of endpoints already tried. This state needs to be held
82
+ # from Consumer.begin() to Consumer.complete(), but it is only
83
+ # applicable to a single session with a single user agent, and at
84
+ # the end of the authentication process (i.e. when an OP replies
85
+ # with either <tt>id_res</tt>. or <tt>cancel</tt> it may be
86
+ # discarded.
87
+ #
88
+ # 2. State of relationships with servers, i.e. shared secrets
89
+ # (associations) with servers and nonces seen on signed messages.
90
+ # This information should persist from one session to the next
91
+ # and should not be bound to a particular user-agent.
92
+ #
93
+ # These two types of storage are reflected in the first two
94
+ # arguments of Consumer's constructor, <tt>session</tt> and
95
+ # <tt>store</tt>. <tt>session</tt> is a dict-like object and we
96
+ # hope your web framework provides you with one of these bound to
97
+ # the user agent. <tt>store</tt> is an instance of Store.
98
+ #
99
+ # Since the store does hold secrets shared between your application
100
+ # and the OpenID provider, you should be careful about how you use
101
+ # it in a shared hosting environment. If the filesystem or database
102
+ # permissions of your web host allow strangers to read from them, do
103
+ # not store your data there! If you have no safe place to store
104
+ # your data, construct your consumer with nil for the store, and it
105
+ # will operate only in stateless mode. Stateless mode may be
106
+ # slower, put more load on the OpenID provider, and trusts the
107
+ # provider to keep you safe from replay attacks.
108
+ #
109
+ # Several store implementation are provided, and the interface is
110
+ # fully documented so that custom stores can be used as well. See
111
+ # the documentation for the Consumer class for more information on
112
+ # the interface for stores. The implementations that are provided
113
+ # allow the consumer site to store the necessary data in several
114
+ # different ways, including several SQL databases and normal files
115
+ # on disk.
116
+ #
117
+ # = IMMEDIATE MODE
118
+ #
119
+ # In the flow described above, the user may need to confirm to the
120
+ # OpenID provider that it's ok to disclose his or her identity. The
121
+ # provider may draw pages asking for information from the user
122
+ # before it redirects the browser back to the consumer's site. This
123
+ # is generally transparent to the consumer site, so it is typically
124
+ # ignored as an implementation detail.
125
+ #
126
+ # There can be times, however, where the consumer site wants to get
127
+ # a response immediately. When this is the case, the consumer can
128
+ # put the library in immediate mode. In immediate mode, there is an
129
+ # extra response possible from the server, which is essentially the
130
+ # server reporting that it doesn't have enough information to answer
131
+ # the question yet.
132
+ #
133
+ # = USING THIS LIBRARY
134
+ #
135
+ # Integrating this library into an application is usually a
136
+ # relatively straightforward process. The process should basically
137
+ # follow this plan:
138
+ #
139
+ # Add an OpenID login field somewhere on your site. When an OpenID
140
+ # is entered in that field and the form is submitted, it should make
141
+ # a request to the your site which includes that OpenID URL.
142
+ #
143
+ # First, the application should instantiate a Consumer with a
144
+ # session for per-user state and store for shared state using the
145
+ # store of choice.
146
+ #
147
+ # Next, the application should call the <tt>begin</tt> method of
148
+ # Consumer instance. This method takes the OpenID URL as entered by
149
+ # the user. The <tt>begin</tt> method returns a CheckIDRequest
150
+ # object.
151
+ #
152
+ # Next, the application should call the redirect_url method on the
153
+ # CheckIDRequest object. The parameter <tt>return_to</tt> is the
154
+ # URL that the OpenID server will send the user back to after
155
+ # attempting to verify his or her identity. The <tt>realm</tt>
156
+ # parameter is the URL (or URL pattern) that identifies your web
157
+ # site to the user when he or she is authorizing it. Send a
158
+ # redirect to the resulting URL to the user's browser.
159
+ #
160
+ # That's the first half of the authentication process. The second
161
+ # half of the process is done after the user's OpenID Provider sends
162
+ # the user's browser a redirect back to your site to complete their
163
+ # login.
164
+ #
165
+ # When that happens, the user will contact your site at the URL
166
+ # given as the <tt>return_to</tt> URL to the redirect_url call made
167
+ # above. The request will have several query parameters added to
168
+ # the URL by the OpenID provider as the information necessary to
169
+ # finish the request.
170
+ #
171
+ # Get a Consumer instance with the same session and store as before
172
+ # and call its complete() method, passing in all the received query
173
+ # arguments and URL currently being handled.
174
+ #
175
+ # There are multiple possible return types possible from that
176
+ # method. These indicate the whether or not the login was
177
+ # successful, and include any additional information appropriate for
178
+ # their type.
179
+ class Consumer
180
+ attr_accessor :session_key_prefix
181
+
182
+ # Initialize a Consumer instance.
183
+ #
184
+ # You should create a new instance of the Consumer object with
185
+ # every HTTP request that handles OpenID transactions.
186
+ #
187
+ # session: the session object to use to store request information.
188
+ # The session should behave like a hash.
189
+ #
190
+ # store: an object that implements the interface in Store.
191
+ def initialize(session, store)
192
+ @session = session
193
+ @store = store
194
+ @session_key_prefix = 'OpenID::Consumer::'
195
+ end
196
+
197
+ # Start the OpenID authentication process. See steps 1-2 in the
198
+ # overview for the Consumer class.
199
+ #
200
+ # user_url: Identity URL given by the user. This method performs a
201
+ # textual transformation of the URL to try and make sure it is
202
+ # normalized. For example, a user_url of example.com will be
203
+ # normalized to http://example.com/ normalizing and resolving any
204
+ # redirects the server might issue.
205
+ #
206
+ # anonymous: A boolean value. Whether to make an anonymous
207
+ # request of the OpenID provider. Such a request does not ask for
208
+ # an authorization assertion for an OpenID identifier, but may be
209
+ # used with extensions to pass other data. e.g. "I don't care who
210
+ # you are, but I'd like to know your time zone."
211
+ #
212
+ # Returns a CheckIDRequest object containing the discovered
213
+ # information, with a method for building a redirect URL to the
214
+ # server, as described in step 3 of the overview. This object may
215
+ # also be used to add extension arguments to the request, using
216
+ # its add_extension_arg method.
217
+ #
218
+ # Raises DiscoveryFailure when no OpenID server can be found for
219
+ # this URL.
220
+ def begin(openid_identifier, anonymous=false)
221
+ manager = discovery_manager(openid_identifier)
222
+ service = manager.get_next_service(&method(:discover))
223
+
224
+ if service.nil?
225
+ raise DiscoveryFailure.new("No usable OpenID services were found "\
226
+ "for #{openid_identifier.inspect}", nil)
227
+ else
228
+ begin_without_discovery(service, anonymous)
229
+ end
230
+ end
231
+
232
+ # Start OpenID verification without doing OpenID server
233
+ # discovery. This method is used internally by Consumer.begin()
234
+ # after discovery is performed, and exists to provide an interface
235
+ # for library users needing to perform their own discovery.
236
+ #
237
+ # service: an OpenID service endpoint descriptor. This object and
238
+ # factories for it are found in the openid/consumer/discovery.rb
239
+ # module.
240
+ #
241
+ # Returns an OpenID authentication request object.
242
+ def begin_without_discovery(service, anonymous)
243
+ assoc = association_manager(service).get_association
244
+ checkid_request = CheckIDRequest.new(assoc, service)
245
+ checkid_request.anonymous = anonymous
246
+
247
+ if service.compatibility_mode
248
+ rt_args = checkid_request.return_to_args
249
+ rt_args[Consumer.openid1_return_to_nonce_name] = Nonce.mk_nonce
250
+ rt_args[Consumer.openid1_return_to_claimed_id_name] =
251
+ service.claimed_id
252
+ end
253
+
254
+ self.last_requested_endpoint = service
255
+ return checkid_request
256
+ end
257
+
258
+ # Called to interpret the server's response to an OpenID
259
+ # request. It is called in step 4 of the flow described in the
260
+ # Consumer overview.
261
+ #
262
+ # query: A hash of the query parameters for this HTTP request.
263
+ # Note that in rails, this is <b>not</b> <tt>params</tt> but
264
+ # <tt>params.reject{|k,v|request.path_parameters[k]}</tt>
265
+ # because <tt>controller</tt> and <tt>action</tt> and other
266
+ # "path parameters" are included in params.
267
+ #
268
+ # current_url: Extract the URL of the current request from your
269
+ # application's web request framework and specify it here to have it
270
+ # checked against the openid.return_to value in the response. Do not
271
+ # just pass <tt>args['openid.return_to']</tt> here; that will defeat the
272
+ # purpose of this check. (See OpenID Authentication 2.0 section 11.1.)
273
+ #
274
+ # If the return_to URL check fails, the status of the completion will be
275
+ # FAILURE.
276
+
277
+ #
278
+ # Returns a subclass of Response. The type of response is
279
+ # indicated by the status attribute, which will be one of
280
+ # SUCCESS, CANCEL, FAILURE, or SETUP_NEEDED.
281
+ def complete(query, current_url)
282
+ message = Message.from_post_args(query)
283
+ mode = message.get_arg(OPENID_NS, 'mode', 'invalid')
284
+ begin
285
+ meth = method('complete_' + mode)
286
+ rescue NameError
287
+ meth = method(:complete_invalid)
288
+ end
289
+ response = meth.call(message, current_url)
290
+ cleanup_last_requested_endpoint
291
+ if [SUCCESS, CANCEL].member?(response.status)
292
+ cleanup_session
293
+ end
294
+ return response
295
+ end
296
+
297
+ protected
298
+
299
+ def session_get(name)
300
+ @session[session_key(name)]
301
+ end
302
+
303
+ def session_set(name, val)
304
+ @session[session_key(name)] = val
305
+ end
306
+
307
+ def session_key(suffix)
308
+ @session_key_prefix + suffix
309
+ end
310
+
311
+ def last_requested_endpoint
312
+ session_get('last_requested_endpoint')
313
+ end
314
+
315
+ def last_requested_endpoint=(endpoint)
316
+ session_set('last_requested_endpoint', endpoint)
317
+ end
318
+
319
+ def cleanup_last_requested_endpoint
320
+ @session[session_key('last_requested_endpoint')] = nil
321
+ end
322
+
323
+ def discovery_manager(openid_identifier)
324
+ DiscoveryManager.new(@session, openid_identifier, @session_key_prefix)
325
+ end
326
+
327
+ def cleanup_session
328
+ discovery_manager(nil).cleanup(true)
329
+ end
330
+
331
+
332
+ def discover(identifier)
333
+ OpenID.discover(identifier)
334
+ end
335
+
336
+ def negotiator
337
+ DefaultNegotiator
338
+ end
339
+
340
+ def association_manager(service)
341
+ AssociationManager.new(@store, service.server_url,
342
+ service.compatibility_mode, negotiator)
343
+ end
344
+
345
+ def handle_idres(message, current_url)
346
+ IdResHandler.new(message, current_url, @store, last_requested_endpoint)
347
+ end
348
+
349
+ def complete_invalid(message, unused_return_to)
350
+ mode = message.get_arg(OPENID_NS, 'mode', '<No mode set>')
351
+ return FailureResponse.new(last_requested_endpoint,
352
+ "Invalid openid.mode: #{mode}")
353
+ end
354
+
355
+ def complete_cancel(unused_message, unused_return_to)
356
+ return CancelResponse.new(last_requested_endpoint)
357
+ end
358
+
359
+ def complete_error(message, unused_return_to)
360
+ error = message.get_arg(OPENID_NS, 'error')
361
+ contact = message.get_arg(OPENID_NS, 'contact')
362
+ reference = message.get_arg(OPENID_NS, 'reference')
363
+
364
+ return FailureResponse.new(last_requested_endpoint,
365
+ error, contact, reference)
366
+ end
367
+
368
+ def complete_setup_needed(message, unused_return_to)
369
+ if message.is_openid1
370
+ return complete_invalid(message, nil)
371
+ else
372
+ setup_url = message.get_arg(OPENID2_NS, 'user_setup_url')
373
+ return SetupNeededResponse.new(last_requested_endpoint, setup_url)
374
+ end
375
+ end
376
+
377
+ def complete_id_res(message, current_url)
378
+ if message.is_openid1
379
+ setup_url = message.get_arg(OPENID_NS, 'user_setup_url')
380
+ if !setup_url.nil?
381
+ return SetupNeededResponse.new(last_requested_endpoint, setup_url)
382
+ end
383
+ end
384
+
385
+ begin
386
+ idres = handle_idres(message, current_url)
387
+ rescue OpenIDError => why
388
+ return FailureResponse.new(last_requested_endpoint, why.message)
389
+ else
390
+ return SuccessResponse.new(idres.endpoint, message,
391
+ idres.signed_fields)
392
+ end
393
+ end
394
+ end
395
+ end
@@ -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