ruby-openid 1.1.4 → 2.0.1

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