nov-ruby-openid 2.1.9

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 (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,170 @@
1
+
2
+ require 'test/unit'
3
+ require 'openid/yadis/accept'
4
+ require 'openid/extras'
5
+ require 'openid/util'
6
+
7
+ module OpenID
8
+
9
+ class AcceptTest < Test::Unit::TestCase
10
+ include TestDataMixin
11
+
12
+ def getTestData()
13
+ # Read the test data off of disk
14
+ #
15
+ # () -> [(int, str)]
16
+ lines = read_data_file('accept.txt')
17
+ line_no = 1
18
+ return lines.collect { |line|
19
+ pair = [line_no, line]
20
+ line_no += 1
21
+ pair
22
+ }
23
+ end
24
+
25
+ def chunk(lines)
26
+ # Return groups of lines separated by whitespace or comments
27
+ #
28
+ # [(int, str)] -> [[(int, str)]]
29
+ chunks = []
30
+ chunk = []
31
+ lines.each { |lineno, line|
32
+ stripped = line.strip()
33
+ if (stripped == '') or stripped.starts_with?('#')
34
+ if chunk.length > 0
35
+ chunks << chunk
36
+ chunk = []
37
+ end
38
+ else
39
+ chunk << [lineno, stripped]
40
+ end
41
+ }
42
+
43
+ if chunk.length > 0
44
+ chunks << chunk
45
+ end
46
+
47
+ return chunks
48
+ end
49
+
50
+ def parseLines(chunk)
51
+ # Take the given chunk of lines and turn it into a test data
52
+ # dictionary
53
+ #
54
+ # [(int, str)] -> {str:(int, str)}
55
+ items = {}
56
+ chunk.each { |lineno, line|
57
+ header, data = line.split(':', 2)
58
+ header = header.downcase
59
+ items[header] = [lineno, data.strip]
60
+ }
61
+ return items
62
+ end
63
+
64
+ def parseAvailable(available_text)
65
+ # Parse an Available: line's data
66
+ #
67
+ # str -> [str]
68
+ return available_text.split(',', -1).collect { |s| s.strip }
69
+ end
70
+
71
+ def parseExpected(expected_text)
72
+ # Parse an Expected: line's data
73
+ #
74
+ # str -> [(str, float)]
75
+ expected = []
76
+ if expected_text != ''
77
+ expected_text.split(',', -1).each { |chunk|
78
+ chunk = chunk.strip
79
+ mtype, qstuff = chunk.split(';', -1)
80
+ mtype = mtype.strip
81
+ Util.assert(!mtype.index('/').nil?)
82
+ qstuff = qstuff.strip
83
+ q, qstr = qstuff.split('=', -1)
84
+ Util.assert(q == 'q')
85
+ qval = qstr.to_f
86
+ expected << [mtype, qval]
87
+ }
88
+ end
89
+
90
+ return expected
91
+ end
92
+
93
+ def test_accept_headers
94
+ lines = getTestData()
95
+ chunks = chunk(lines)
96
+ data_sets = chunks.collect { |chunk| parseLines(chunk) }
97
+ cases = []
98
+ data_sets.each { |data|
99
+ lnos = []
100
+ lno, header = data['accept']
101
+ lnos << lno
102
+ lno, avail_data = data['available']
103
+ lnos << lno
104
+ begin
105
+ available = parseAvailable(avail_data)
106
+ rescue
107
+ print 'On line', lno
108
+ raise
109
+ end
110
+
111
+ lno, exp_data = data['expected']
112
+ lnos << lno
113
+ begin
114
+ expected = parseExpected(exp_data)
115
+ rescue
116
+ print 'On line', lno
117
+ raise
118
+ end
119
+
120
+ descr = sprintf('MatchAcceptTest for lines %s', lnos)
121
+
122
+ # Test:
123
+ accepted = Yadis.parse_accept_header(header)
124
+ actual = Yadis.match_types(accepted, available)
125
+ assert_equal(expected, actual)
126
+
127
+ assert_equal(Yadis.get_acceptable(header, available),
128
+ expected.collect { |mtype, _| mtype })
129
+ }
130
+ end
131
+
132
+ def test_generate_accept_header
133
+ # TODO: move this into a test case file and write parsing code
134
+ # for it.
135
+
136
+ # Form: [input_array, expected_header_string]
137
+ cases = [
138
+ # Empty input list
139
+ [[], ""],
140
+ # Content type name only; no q value
141
+ [["test"], "test"],
142
+ # q = 1.0 should be omitted from the header
143
+ [[["test", 1.0]], "test"],
144
+ # Test conversion of float to string
145
+ [["test", ["with_q", 0.8]], "with_q; q=0.8, test"],
146
+ # Allow string q values, too
147
+ [["test", ["with_q_str", "0.7"]], "with_q_str; q=0.7, test"],
148
+ # Test q values out of bounds
149
+ [[["test", -1.0]], nil],
150
+ [[["test", 1.1]], nil],
151
+ # Test sorting of types by q value
152
+ [[["middle", 0.5], ["min", 0.1], "max"],
153
+ "min; q=0.1, middle; q=0.5, max"],
154
+
155
+ ].each { |input, expected_header|
156
+
157
+ if expected_header.nil?
158
+ assert_raise(ArgumentError) {
159
+ Yadis.generate_accept_header(*input)
160
+ }
161
+ else
162
+ assert_equal(expected_header, Yadis.generate_accept_header(*input),
163
+ [input, expected_header].inspect)
164
+ end
165
+ }
166
+ end
167
+
168
+ end
169
+
170
+ end
@@ -0,0 +1,266 @@
1
+ require "test/unit"
2
+ require "openid/association"
3
+
4
+ module OpenID
5
+ class AssociationTestCase < Test::Unit::TestCase
6
+ def setup
7
+ # Use this funny way of getting a time so that it does not have
8
+ # fractional seconds, and so can be serialized exactly using our
9
+ # standard code.
10
+ issued = Time.at(Time.now.to_i)
11
+ lifetime = 600
12
+
13
+ @assoc = Association.new('handle', 'secret', issued,
14
+ lifetime, 'HMAC-SHA1')
15
+ end
16
+
17
+ def test_round_trip
18
+ assoc2 = Association.deserialize(@assoc.serialize())
19
+ [:handle, :secret, :lifetime, :assoc_type].each do |attr|
20
+ assert_equal(@assoc.send(attr), assoc2.send(attr))
21
+ end
22
+ end
23
+
24
+ def test_deserialize_failure
25
+ field_list = Util.kv_to_seq(@assoc.serialize)
26
+ kv = Util.seq_to_kv(field_list + [['monkeys', 'funny']])
27
+ assert_raises(ProtocolError) {
28
+ Association.deserialize(kv)
29
+ }
30
+
31
+ bad_version_list = field_list.dup
32
+ bad_version_list[0] = ['version', 'moon']
33
+ bad_version_kv = Util.seq_to_kv(bad_version_list)
34
+ assert_raises(ProtocolError) {
35
+ Association.deserialize(bad_version_kv)
36
+ }
37
+ end
38
+
39
+ def test_serialization_identity
40
+ assoc2 = Association.deserialize(@assoc.serialize)
41
+ assert_equal(@assoc, assoc2)
42
+ end
43
+
44
+ def test_expires_in
45
+ # Allow one second of slop
46
+ assert(@assoc.expires_in.between?(599,600))
47
+ assert(@assoc.expires_in(Time.now.to_i).between?(599,600))
48
+ assert_equal(0,@assoc.expires_in(Time.now.to_i + 10000),"negative expires_in")
49
+ end
50
+
51
+ def test_from_expires_in
52
+ start_time = Time.now
53
+ expires_in = @assoc.expires_in
54
+ assoc = Association.from_expires_in(expires_in,
55
+ @assoc.handle,
56
+ @assoc.secret,
57
+ @assoc.assoc_type)
58
+
59
+ # Allow one second of slop here for code execution time
60
+ assert_in_delta(1, assoc.expires_in, @assoc.expires_in)
61
+ [:handle, :secret, :assoc_type].each do |attr|
62
+ assert_equal(@assoc.send(attr), assoc.send(attr))
63
+ end
64
+
65
+ # Make sure the issued time is near the start
66
+ assert(assoc.issued >= start_time)
67
+ assert_in_delta(1, assoc.issued.to_f, start_time.to_f)
68
+ end
69
+
70
+ def test_sign_sha1
71
+ pairs = [['key1', 'value1'],
72
+ ['key2', 'value2']]
73
+
74
+ [['HMAC-SHA256', "\xfd\xaa\xfe;\xac\xfc*\x988\xad\x05d6-"\
75
+ "\xeaVy\xd5\xa5Z.<\xa9\xed\x18\x82\\$"\
76
+ "\x95x\x1c&"],
77
+ ['HMAC-SHA1', "\xe0\x1bv\x04\xf1G\xc0\xbb\x7f\x9a\x8b"\
78
+ "\xe9\xbc\xee}\\\xe5\xbb7*"],
79
+ ].each do |assoc_type, expected|
80
+ assoc = Association.from_expires_in(3600, "handle", 'very_secret',
81
+ assoc_type)
82
+ sig = assoc.sign(pairs)
83
+ assert_equal(sig, expected)
84
+
85
+ m = Message.new(OPENID2_NS)
86
+ pairs.each { |k, v|
87
+ m.set_arg(OPENID_NS, k, v)
88
+ }
89
+ m.set_arg(BARE_NS, "not_an_openid_arg", "bogus")
90
+
91
+ signed_m = assoc.sign_message(m)
92
+ assert(signed_m.has_key?(OPENID_NS, 'sig'))
93
+ assert_equal(signed_m.get_arg(OPENID_NS, 'signed'),
94
+ 'assoc_handle,key1,key2,ns,signed')
95
+ end
96
+ end
97
+
98
+ def test_sign_message_with_sig
99
+ assoc = Association.from_expires_in(3600, "handle", "very_secret",
100
+ "HMAC-SHA1")
101
+ m = Message.new(OPENID2_NS)
102
+ m.set_arg(OPENID_NS, 'sig', 'noise')
103
+ assert_raises(ArgumentError) {
104
+ assoc.sign_message(m)
105
+ }
106
+ end
107
+
108
+ def test_sign_message_with_signed
109
+ assoc = Association.from_expires_in(3600, "handle", "very_secret",
110
+ "HMAC-SHA1")
111
+ m = Message.new(OPENID2_NS)
112
+ m.set_arg(OPENID_NS, 'signed', 'fields')
113
+ assert_raises(ArgumentError) {
114
+ assoc.sign_message(m)
115
+ }
116
+ end
117
+
118
+ def test_sign_different_assoc_handle
119
+ assoc = Association.from_expires_in(3600, "handle", "very_secret",
120
+ "HMAC-SHA1")
121
+ m = Message.new(OPENID2_NS)
122
+ m.set_arg(OPENID_NS, 'assoc_handle', 'different')
123
+ assert_raises(ArgumentError) {
124
+ assoc.sign_message(m)
125
+ }
126
+ end
127
+
128
+ def test_sign_bad_assoc_type
129
+ @assoc.instance_eval { @assoc_type = 'Cookies' }
130
+ assert_raises(ProtocolError) {
131
+ @assoc.sign([])
132
+ }
133
+ end
134
+
135
+ def test_make_pairs
136
+ msg = Message.new(OPENID2_NS)
137
+ msg.update_args(OPENID2_NS, {
138
+ 'mode' => 'id_res',
139
+ 'identifier' => '=example',
140
+ 'signed' => 'identifier,mode',
141
+ 'sig' => 'cephalopod',
142
+ })
143
+ msg.update_args(BARE_NS, {'xey' => 'value'})
144
+ assoc = Association.from_expires_in(3600, '{sha1}', 'very_secret',
145
+ "HMAC-SHA1")
146
+ pairs = assoc.make_pairs(msg)
147
+ assert_equal([['identifier', '=example'],
148
+ ['mode', 'id_res']], pairs)
149
+ end
150
+
151
+ def test_check_message_signature_no_signed
152
+ m = Message.new(OPENID2_NS)
153
+ m.update_args(OPENID2_NS, {'mode' => 'id_res',
154
+ 'identifier' => '=example',
155
+ 'sig' => 'coyote',
156
+ })
157
+ assoc = Association.from_expires_in(3600, '{sha1}', 'very_secret',
158
+ "HMAC-SHA1")
159
+ assert_raises(ProtocolError) {
160
+ assoc.check_message_signature(m)
161
+ }
162
+ end
163
+
164
+ def test_check_message_signature_no_sig
165
+ m = Message.new(OPENID2_NS)
166
+ m.update_args(OPENID2_NS, {'mode' => 'id_res',
167
+ 'identifier' => '=example',
168
+ 'signed' => 'mode',
169
+ })
170
+ assoc = Association.from_expires_in(3600, '{sha1}', 'very_secret',
171
+ "HMAC-SHA1")
172
+ assert_raises(ProtocolError) {
173
+ assoc.check_message_signature(m)
174
+ }
175
+ end
176
+
177
+ def test_check_message_signature_bad_sig
178
+ m = Message.new(OPENID2_NS)
179
+ m.update_args(OPENID2_NS, {'mode' => 'id_res',
180
+ 'identifier' => '=example',
181
+ 'signed' => 'mode',
182
+ 'sig' => Util.to_base64('coyote'),
183
+ })
184
+ assoc = Association.from_expires_in(3600, '{sha1}', 'very_secret',
185
+ "HMAC-SHA1")
186
+ assert(!assoc.check_message_signature(m))
187
+ end
188
+
189
+ def test_check_message_signature_good_sig
190
+ m = Message.new(OPENID2_NS)
191
+ m.update_args(OPENID2_NS, {'mode' => 'id_res',
192
+ 'identifier' => '=example',
193
+ 'signed' => 'mode',
194
+ 'sig' => Util.to_base64('coyote'),
195
+ })
196
+ assoc = Association.from_expires_in(3600, '{sha1}', 'very_secret',
197
+ "HMAC-SHA1")
198
+ class << assoc
199
+ # Override sign, because it's already tested elsewhere
200
+ def sign(pairs)
201
+ "coyote"
202
+ end
203
+ end
204
+
205
+ assert(assoc.check_message_signature(m))
206
+ end
207
+ end
208
+
209
+ class AssociationNegotiatorTestCase < Test::Unit::TestCase
210
+ def assert_equal_under(item1, item2)
211
+ val1 = yield(item1)
212
+ val2 = yield(item2)
213
+ assert_equal(val1, val2)
214
+ end
215
+
216
+ def test_copy
217
+ neg = AssociationNegotiator.new([['HMAC-SHA1', 'DH-SHA1']])
218
+ neg2 = neg.copy
219
+ assert_equal_under(neg, neg2) {|n| n.instance_eval{@allowed_types} }
220
+ assert(neg.object_id != neg2.object_id)
221
+ end
222
+
223
+ def test_add_allowed
224
+ neg = AssociationNegotiator.new([])
225
+ assert(!neg.allowed?('HMAC-SHA1', 'DH-SHA1'))
226
+ assert(!neg.allowed?('HMAC-SHA1', 'no-encryption'))
227
+ assert(!neg.allowed?('HMAC-SHA256', 'DH-SHA256'))
228
+ assert(!neg.allowed?('HMAC-SHA256', 'no-encryption'))
229
+ neg.add_allowed_type('HMAC-SHA1')
230
+ assert(neg.allowed?('HMAC-SHA1', 'DH-SHA1'))
231
+ assert(neg.allowed?('HMAC-SHA1', 'no-encryption'))
232
+ assert(!neg.allowed?('HMAC-SHA256', 'DH-SHA256'))
233
+ assert(!neg.allowed?('HMAC-SHA256', 'no-encryption'))
234
+ neg.add_allowed_type('HMAC-SHA256', 'DH-SHA256')
235
+ assert(neg.allowed?('HMAC-SHA1', 'DH-SHA1'))
236
+ assert(neg.allowed?('HMAC-SHA1', 'no-encryption'))
237
+ assert(neg.allowed?('HMAC-SHA256', 'DH-SHA256'))
238
+ assert(!neg.allowed?('HMAC-SHA256', 'no-encryption'))
239
+ assert_equal(neg.get_allowed_type, ['HMAC-SHA1', 'DH-SHA1'])
240
+ end
241
+
242
+ def test_bad_assoc_type
243
+ assert_raises(ProtocolError) {
244
+ AssociationNegotiator.new([['OMG', 'Ponies']])
245
+ }
246
+ end
247
+
248
+ def test_bad_session_type
249
+ assert_raises(ProtocolError) {
250
+ AssociationNegotiator.new([['HMAC-SHA1', 'OMG-Ponies']])
251
+ }
252
+ end
253
+
254
+ def test_default_negotiator
255
+ assert_equal(DefaultNegotiator.get_allowed_type,
256
+ ['HMAC-SHA1', 'DH-SHA1'])
257
+ assert(DefaultNegotiator.allowed?('HMAC-SHA256', 'no-encryption'))
258
+ end
259
+
260
+ def test_encrypted_negotiator
261
+ assert_equal(EncryptedNegotiator.get_allowed_type,
262
+ ['HMAC-SHA1', 'DH-SHA1'])
263
+ assert(!EncryptedNegotiator.allowed?('HMAC-SHA256', 'no-encryption'))
264
+ end
265
+ end
266
+ end
@@ -0,0 +1,917 @@
1
+ require "openid/consumer/associationmanager"
2
+ require "openid/association"
3
+ require "openid/dh"
4
+ require "openid/util"
5
+ require "openid/cryptutil"
6
+ require "openid/message"
7
+ require "openid/store/memory"
8
+ require "test/unit"
9
+ require "util"
10
+ require "time"
11
+
12
+ module OpenID
13
+ class DHAssocSessionTest < Test::Unit::TestCase
14
+ def test_sha1_get_request
15
+ # Initialized without an explicit DH gets defaults
16
+ sess = Consumer::DiffieHellmanSHA1Session.new
17
+ assert_equal(['dh_consumer_public'], sess.get_request.keys)
18
+ assert_nothing_raised do
19
+ Util::from_base64(sess.get_request['dh_consumer_public'])
20
+ end
21
+ end
22
+
23
+ def test_sha1_get_request_custom_dh
24
+ dh = DiffieHellman.new(1299721, 2)
25
+ sess = Consumer::DiffieHellmanSHA1Session.new(dh)
26
+ req = sess.get_request
27
+ assert_equal(['dh_consumer_public', 'dh_modulus', 'dh_gen'].sort,
28
+ req.keys.sort)
29
+ assert_equal(dh.modulus, CryptUtil.base64_to_num(req['dh_modulus']))
30
+ assert_equal(dh.generator, CryptUtil.base64_to_num(req['dh_gen']))
31
+ assert_nothing_raised do
32
+ Util::from_base64(req['dh_consumer_public'])
33
+ end
34
+ end
35
+ end
36
+
37
+ module TestDiffieHellmanResponseParametersMixin
38
+ def setup
39
+ session_cls = self.class.session_cls
40
+
41
+ # Pre-compute DH with small prime so tests run quickly.
42
+ @server_dh = DiffieHellman.new(100389557, 2)
43
+ @consumer_dh = DiffieHellman.new(100389557, 2)
44
+
45
+ # base64(btwoc(g ^ xb mod p))
46
+ @dh_server_public = CryptUtil.num_to_base64(@server_dh.public)
47
+
48
+ @secret = CryptUtil.random_string(session_cls.secret_size)
49
+
50
+ enc_mac_key_unencoded =
51
+ @server_dh.xor_secret(session_cls.hashfunc,
52
+ @consumer_dh.public,
53
+ @secret)
54
+
55
+ @enc_mac_key = Util.to_base64(enc_mac_key_unencoded)
56
+
57
+ @consumer_session = session_cls.new(@consumer_dh)
58
+
59
+ @msg = Message.new(self.class.message_namespace)
60
+ end
61
+
62
+ def test_extract_secret
63
+ @msg.set_arg(OPENID_NS, 'dh_server_public', @dh_server_public)
64
+ @msg.set_arg(OPENID_NS, 'enc_mac_key', @enc_mac_key)
65
+
66
+ extracted = @consumer_session.extract_secret(@msg)
67
+ assert_equal(extracted, @secret)
68
+ end
69
+
70
+ def test_absent_serve_public
71
+ @msg.set_arg(OPENID_NS, 'enc_mac_key', @enc_mac_key)
72
+
73
+ assert_raises(Message::KeyNotFound) {
74
+ @consumer_session.extract_secret(@msg)
75
+ }
76
+ end
77
+
78
+ def test_absent_mac_key
79
+ @msg.set_arg(OPENID_NS, 'dh_server_public', @dh_server_public)
80
+
81
+ assert_raises(Message::KeyNotFound) {
82
+ @consumer_session.extract_secret(@msg)
83
+ }
84
+ end
85
+
86
+ def test_invalid_base64_public
87
+ @msg.set_arg(OPENID_NS, 'dh_server_public', 'n o t b a s e 6 4.')
88
+ @msg.set_arg(OPENID_NS, 'enc_mac_key', @enc_mac_key)
89
+
90
+ assert_raises(ArgumentError) {
91
+ @consumer_session.extract_secret(@msg)
92
+ }
93
+ end
94
+
95
+ def test_invalid_base64_mac_key
96
+ @msg.set_arg(OPENID_NS, 'dh_server_public', @dh_server_public)
97
+ @msg.set_arg(OPENID_NS, 'enc_mac_key', 'n o t base 64')
98
+
99
+ assert_raises(ArgumentError) {
100
+ @consumer_session.extract_secret(@msg)
101
+ }
102
+ end
103
+ end
104
+
105
+ class TestConsumerOpenID1DHSHA1 < Test::Unit::TestCase
106
+ include TestDiffieHellmanResponseParametersMixin
107
+ class << self
108
+ attr_reader :session_cls, :message_namespace
109
+ end
110
+
111
+ @session_cls = Consumer::DiffieHellmanSHA1Session
112
+ @message_namespace = OPENID1_NS
113
+ end
114
+
115
+ class TestConsumerOpenID2DHSHA1 < Test::Unit::TestCase
116
+ include TestDiffieHellmanResponseParametersMixin
117
+ class << self
118
+ attr_reader :session_cls, :message_namespace
119
+ end
120
+
121
+ @session_cls = Consumer::DiffieHellmanSHA1Session
122
+ @message_namespace = OPENID2_NS
123
+ end
124
+
125
+ class TestConsumerOpenID2DHSHA256 < Test::Unit::TestCase
126
+ include TestDiffieHellmanResponseParametersMixin
127
+ class << self
128
+ attr_reader :session_cls, :message_namespace
129
+ end
130
+
131
+ @session_cls = Consumer::DiffieHellmanSHA256Session
132
+ @message_namespace = OPENID2_NS
133
+ end
134
+
135
+ class TestConsumerNoEncryptionSession < Test::Unit::TestCase
136
+ def setup
137
+ @sess = Consumer::NoEncryptionSession.new
138
+ end
139
+
140
+ def test_empty_request
141
+ assert_equal(@sess.get_request, {})
142
+ end
143
+
144
+ def test_get_secret
145
+ secret = 'shhh!' * 4
146
+ mac_key = Util.to_base64(secret)
147
+ msg = Message.from_openid_args({'mac_key' => mac_key})
148
+ assert_equal(secret, @sess.extract_secret(msg))
149
+ end
150
+ end
151
+
152
+ class TestCreateAssociationRequest < Test::Unit::TestCase
153
+ def setup
154
+ @server_url = 'http://invalid/'
155
+ @assoc_manager = Consumer::AssociationManager.new(nil, @server_url)
156
+ class << @assoc_manager
157
+ def compatibility_mode=(val)
158
+ @compatibility_mode = val
159
+ end
160
+ end
161
+ @assoc_type = 'HMAC-SHA1'
162
+ end
163
+
164
+ def test_no_encryption_sends_type
165
+ session_type = 'no-encryption'
166
+ session, args = @assoc_manager.send(:create_associate_request,
167
+ @assoc_type,
168
+ session_type)
169
+
170
+ assert(session.is_a?(Consumer::NoEncryptionSession))
171
+ expected = Message.from_openid_args(
172
+ {'ns' => OPENID2_NS,
173
+ 'session_type' => session_type,
174
+ 'mode' => 'associate',
175
+ 'assoc_type' => @assoc_type,
176
+ })
177
+
178
+ assert_equal(expected, args)
179
+ end
180
+
181
+ def test_no_encryption_compatibility
182
+ @assoc_manager.compatibility_mode = true
183
+ session_type = 'no-encryption'
184
+ session, args = @assoc_manager.send(:create_associate_request,
185
+ @assoc_type,
186
+ session_type)
187
+
188
+ assert(session.is_a?(Consumer::NoEncryptionSession))
189
+ assert_equal(Message.from_openid_args({'mode' => 'associate',
190
+ 'assoc_type' => @assoc_type,
191
+ }), args)
192
+ end
193
+
194
+ def test_dh_sha1_compatibility
195
+ @assoc_manager.compatibility_mode = true
196
+ session_type = 'DH-SHA1'
197
+ session, args = @assoc_manager.send(:create_associate_request,
198
+ @assoc_type,
199
+ session_type)
200
+
201
+
202
+ assert(session.is_a?(Consumer::DiffieHellmanSHA1Session))
203
+
204
+ # This is a random base-64 value, so just check that it's
205
+ # present.
206
+ assert_not_nil(args.get_arg(OPENID1_NS, 'dh_consumer_public'))
207
+ args.del_arg(OPENID1_NS, 'dh_consumer_public')
208
+
209
+ # OK, session_type is set here and not for no-encryption
210
+ # compatibility
211
+ expected = Message.from_openid_args({'mode' => 'associate',
212
+ 'session_type' => 'DH-SHA1',
213
+ 'assoc_type' => @assoc_type,
214
+ })
215
+ assert_equal(expected, args)
216
+ end
217
+ end
218
+
219
+ class TestAssociationManagerExpiresIn < Test::Unit::TestCase
220
+ def expires_in_msg(val)
221
+ msg = Message.from_openid_args({'expires_in' => val})
222
+ Consumer::AssociationManager.extract_expires_in(msg)
223
+ end
224
+
225
+ def test_parse_fail
226
+ ['',
227
+ '-2',
228
+ ' 1',
229
+ ' ',
230
+ '0x00',
231
+ 'foosball',
232
+ '1\n',
233
+ '100,000,000,000',
234
+ ].each do |x|
235
+ assert_raises(ProtocolError) {expires_in_msg(x)}
236
+ end
237
+ end
238
+
239
+ def test_parse
240
+ ['0',
241
+ '1',
242
+ '1000',
243
+ '9999999',
244
+ '01',
245
+ ].each do |n|
246
+ assert_equal(n.to_i, expires_in_msg(n))
247
+ end
248
+ end
249
+ end
250
+
251
+ class TestAssociationManagerCreateSession < Test::Unit::TestCase
252
+ def test_invalid
253
+ assert_raises(ArgumentError) {
254
+ Consumer::AssociationManager.create_session('monkeys')
255
+ }
256
+ end
257
+
258
+ def test_sha256
259
+ sess = Consumer::AssociationManager.create_session('DH-SHA256')
260
+ assert(sess.is_a?(Consumer::DiffieHellmanSHA256Session))
261
+ end
262
+ end
263
+
264
+ module NegotiationTestMixin
265
+ include TestUtil
266
+ def mk_message(args)
267
+ args['ns'] = @openid_ns
268
+ Message.from_openid_args(args)
269
+ end
270
+
271
+ def call_negotiate(responses, negotiator=nil)
272
+ store = nil
273
+ compat = self.class::Compat
274
+ assoc_manager = Consumer::AssociationManager.new(store, @server_url,
275
+ compat, negotiator)
276
+ class << assoc_manager
277
+ attr_accessor :responses
278
+
279
+ def request_association(assoc_type, session_type)
280
+ m = @responses.shift
281
+ if m.is_a?(Message)
282
+ raise ServerError.from_message(m)
283
+ else
284
+ return m
285
+ end
286
+ end
287
+ end
288
+ assoc_manager.responses = responses
289
+ assoc_manager.negotiate_association
290
+ end
291
+ end
292
+
293
+ # Test the session type negotiation behavior of an OpenID 2
294
+ # consumer.
295
+ class TestOpenID2SessionNegotiation < Test::Unit::TestCase
296
+ include NegotiationTestMixin
297
+
298
+ Compat = false
299
+
300
+ def setup
301
+ @server_url = 'http://invalid/'
302
+ @openid_ns = OPENID2_NS
303
+ end
304
+
305
+ # Test the case where the response to an associate request is a
306
+ # server error or is otherwise undecipherable.
307
+ def test_bad_response
308
+ assert_log_matches('Server error when requesting an association') {
309
+ assert_equal(call_negotiate([mk_message({})]), nil)
310
+ }
311
+ end
312
+
313
+ # Test the case where the association type (assoc_type) returned
314
+ # in an unsupported-type response is absent.
315
+ def test_empty_assoc_type
316
+ msg = mk_message({'error' => 'Unsupported type',
317
+ 'error_code' => 'unsupported-type',
318
+ 'session_type' => 'new-session-type',
319
+ })
320
+
321
+ assert_log_matches('Unsupported association type',
322
+ "Server #{@server_url} responded with unsupported "\
323
+ "association session but did not supply a fallback."
324
+ ) {
325
+ assert_equal(call_negotiate([msg]), nil)
326
+ }
327
+
328
+ end
329
+
330
+ # Test the case where the session type (session_type) returned
331
+ # in an unsupported-type response is absent.
332
+ def test_empty_session_type
333
+ msg = mk_message({'error' => 'Unsupported type',
334
+ 'error_code' => 'unsupported-type',
335
+ 'assoc_type' => 'new-assoc-type',
336
+ })
337
+
338
+ assert_log_matches('Unsupported association type',
339
+ "Server #{@server_url} responded with unsupported "\
340
+ "association session but did not supply a fallback."
341
+ ) {
342
+ assert_equal(call_negotiate([msg]), nil)
343
+ }
344
+ end
345
+
346
+ # Test the case where an unsupported-type response specifies a
347
+ # preferred (assoc_type, session_type) combination that is not
348
+ # allowed by the consumer's SessionNegotiator.
349
+ def test_not_allowed
350
+ negotiator = AssociationNegotiator.new([])
351
+ negotiator.instance_eval{
352
+ @allowed_types = [['assoc_bogus', 'session_bogus']]
353
+ }
354
+ msg = mk_message({'error' => 'Unsupported type',
355
+ 'error_code' => 'unsupported-type',
356
+ 'assoc_type' => 'not-allowed',
357
+ 'session_type' => 'not-allowed',
358
+ })
359
+
360
+ assert_log_matches('Unsupported association type',
361
+ 'Server sent unsupported session/association type:') {
362
+ assert_equal(call_negotiate([msg], negotiator), nil)
363
+ }
364
+ end
365
+
366
+ # Test the case where an unsupported-type response triggers a
367
+ # retry to get an association with the new preferred type.
368
+ def test_unsupported_with_retry
369
+ msg = mk_message({'error' => 'Unsupported type',
370
+ 'error_code' => 'unsupported-type',
371
+ 'assoc_type' => 'HMAC-SHA1',
372
+ 'session_type' => 'DH-SHA1',
373
+ })
374
+
375
+ assoc = Association.new('handle', 'secret', Time.now, 10000, 'HMAC-SHA1')
376
+
377
+ assert_log_matches('Unsupported association type') {
378
+ assert_equal(assoc, call_negotiate([msg, assoc]))
379
+ }
380
+ end
381
+
382
+ # Test the case where an unsupported-typ response triggers a
383
+ # retry, but the retry fails and nil is returned instead.
384
+ def test_unsupported_with_retry_and_fail
385
+ msg = mk_message({'error' => 'Unsupported type',
386
+ 'error_code' => 'unsupported-type',
387
+ 'assoc_type' => 'HMAC-SHA1',
388
+ 'session_type' => 'DH-SHA1',
389
+ })
390
+
391
+ assert_log_matches('Unsupported association type',
392
+ "Server #{@server_url} refused") {
393
+ assert_equal(call_negotiate([msg, msg]), nil)
394
+ }
395
+ end
396
+
397
+ # Test the valid case, wherein an association is returned on the
398
+ # first attempt to get one.
399
+ def test_valid
400
+ assoc = Association.new('handle', 'secret', Time.now, 10000, 'HMAC-SHA1')
401
+
402
+ assert_log_matches() {
403
+ assert_equal(call_negotiate([assoc]), assoc)
404
+ }
405
+ end
406
+ end
407
+
408
+
409
+ # Tests for the OpenID 1 consumer association session behavior. See
410
+ # the docs for TestOpenID2SessionNegotiation. Notice that this
411
+ # class is not a subclass of the OpenID 2 tests. Instead, it uses
412
+ # many of the same inputs but inspects the log messages logged with
413
+ # oidutil.log. See the calls to self.failUnlessLogMatches. Some of
414
+ # these tests pass openid2-style messages to the openid 1
415
+ # association processing logic to be sure it ignores the extra data.
416
+ class TestOpenID1SessionNegotiation < Test::Unit::TestCase
417
+ include NegotiationTestMixin
418
+
419
+ Compat = true
420
+
421
+ def setup
422
+ @server_url = 'http://invalid/'
423
+ @openid_ns = OPENID1_NS
424
+ end
425
+
426
+ def test_bad_response
427
+ assert_log_matches('Server error when requesting an association') {
428
+ response = call_negotiate([mk_message({})])
429
+ assert_equal(nil, response)
430
+ }
431
+ end
432
+
433
+ def test_empty_assoc_type
434
+ msg = mk_message({'error' => 'Unsupported type',
435
+ 'error_code' => 'unsupported-type',
436
+ 'session_type' => 'new-session-type',
437
+ })
438
+
439
+ assert_log_matches('Server error when requesting an association') {
440
+ response = call_negotiate([msg])
441
+ assert_equal(nil, response)
442
+ }
443
+ end
444
+
445
+ def test_empty_session_type
446
+ msg = mk_message({'error' => 'Unsupported type',
447
+ 'error_code' => 'unsupported-type',
448
+ 'assoc_type' => 'new-assoc-type',
449
+ })
450
+
451
+ assert_log_matches('Server error when requesting an association') {
452
+ response = call_negotiate([msg])
453
+ assert_equal(nil, response)
454
+ }
455
+ end
456
+
457
+ def test_not_allowed
458
+ negotiator = AssociationNegotiator.new([])
459
+ negotiator.instance_eval{
460
+ @allowed_types = [['assoc_bogus', 'session_bogus']]
461
+ }
462
+
463
+ msg = mk_message({'error' => 'Unsupported type',
464
+ 'error_code' => 'unsupported-type',
465
+ 'assoc_type' => 'not-allowed',
466
+ 'session_type' => 'not-allowed',
467
+ })
468
+
469
+ assert_log_matches('Server error when requesting an association') {
470
+ response = call_negotiate([msg])
471
+ assert_equal(nil, response)
472
+ }
473
+ end
474
+
475
+ def test_unsupported_with_retry
476
+ msg = mk_message({'error' => 'Unsupported type',
477
+ 'error_code' => 'unsupported-type',
478
+ 'assoc_type' => 'HMAC-SHA1',
479
+ 'session_type' => 'DH-SHA1',
480
+ })
481
+
482
+ assoc = Association.new('handle', 'secret', Time.now, 10000, 'HMAC-SHA1')
483
+
484
+
485
+ assert_log_matches('Server error when requesting an association') {
486
+ response = call_negotiate([msg, assoc])
487
+ assert_equal(nil, response)
488
+ }
489
+ end
490
+
491
+ def test_valid
492
+ assoc = Association.new('handle', 'secret', Time.now, 10000, 'HMAC-SHA1')
493
+ assert_log_matches() {
494
+ response = call_negotiate([assoc])
495
+ assert_equal(assoc, response)
496
+ }
497
+ end
498
+ end
499
+
500
+
501
+ class TestExtractAssociation < Test::Unit::TestCase
502
+ include ProtocolErrorMixin
503
+
504
+ # An OpenID associate response (without the namespace)
505
+ DEFAULTS = {
506
+ 'expires_in' => '1000',
507
+ 'assoc_handle' => 'a handle',
508
+ 'assoc_type' => 'a type',
509
+ 'session_type' => 'a session type',
510
+ }
511
+
512
+ def setup
513
+ @assoc_manager = Consumer::AssociationManager.new(nil, nil)
514
+ end
515
+
516
+ # Make tests that ensure that an association response that is
517
+ # missing required fields will raise an Message::KeyNotFound.
518
+ #
519
+ # According to 'Association Session Response' subsection 'Common
520
+ # Response Parameters', the following fields are required for
521
+ # OpenID 2.0:
522
+ #
523
+ # * ns
524
+ # * session_type
525
+ # * assoc_handle
526
+ # * assoc_type
527
+ # * expires_in
528
+ #
529
+ # In OpenID 1, everything except 'session_type' and 'ns' are
530
+ # required.
531
+ MISSING_FIELD_SETS = ([["no_fields", []]] +
532
+ (DEFAULTS.keys.map do |f|
533
+ fields = DEFAULTS.keys
534
+ fields.delete(f)
535
+ ["missing_#{f}", fields]
536
+ end)
537
+ )
538
+
539
+ [OPENID1_NS, OPENID2_NS].each do |ns|
540
+ MISSING_FIELD_SETS.each do |name, fields|
541
+ # OpenID 1 is allowed to be missing session_type
542
+ if ns != OPENID1_NS and name != 'missing_session_type'
543
+ test = lambda do
544
+ msg = Message.new(ns)
545
+ fields.each do |field|
546
+ msg.set_arg(ns, field, DEFAULTS[field])
547
+ end
548
+ assert_raises(Message::KeyNotFound) do
549
+ @assoc_manager.send(:extract_association, msg, nil)
550
+ end
551
+ end
552
+ define_method("test_#{name}", test)
553
+ end
554
+ end
555
+ end
556
+
557
+ # assert that extracting a response that contains the given
558
+ # response session type when the request was made for the given
559
+ # request session type will raise a ProtocolError indicating
560
+ # session type mismatch
561
+ def assert_session_mismatch(req_type, resp_type, ns)
562
+ # Create an association session that has "req_type" as its
563
+ # session_type and no allowed_assoc_types
564
+ assoc_session_class = Class.new do
565
+ @session_type = req_type
566
+ def self.session_type
567
+ @session_type
568
+ end
569
+ def self.allowed_assoc_types
570
+ []
571
+ end
572
+ end
573
+ assoc_session = assoc_session_class.new
574
+
575
+ # Build an OpenID 1 or 2 association response message that has
576
+ # the specified association session type
577
+ msg = Message.new(ns)
578
+ msg.update_args(ns, DEFAULTS)
579
+ msg.set_arg(ns, 'session_type', resp_type)
580
+
581
+ # The request type and response type have been chosen to produce
582
+ # a session type mismatch.
583
+ assert_protocol_error('Session type mismatch') {
584
+ @assoc_manager.send(:extract_association, msg, assoc_session)
585
+ }
586
+ end
587
+
588
+ [['no-encryption', '', OPENID2_NS],
589
+ ['DH-SHA1', 'no-encryption', OPENID2_NS],
590
+ ['DH-SHA256', 'no-encryption', OPENID2_NS],
591
+ ['no-encryption', 'DH-SHA1', OPENID2_NS],
592
+ ['DH-SHA1', 'DH-SHA256', OPENID1_NS],
593
+ ['DH-SHA256', 'DH-SHA1', OPENID1_NS],
594
+ ['no-encryption', 'DH-SHA1', OPENID1_NS],
595
+ ].each do |req_type, resp_type, ns|
596
+ test = lambda { assert_session_mismatch(req_type, resp_type, ns) }
597
+ name = "test_mismatch_req_#{req_type}_resp_#{resp_type}_#{ns}"
598
+ define_method(name, test)
599
+ end
600
+
601
+ def test_openid1_no_encryption_fallback
602
+ # A DH-SHA1 session
603
+ assoc_session = Consumer::DiffieHellmanSHA1Session.new
604
+
605
+ # An OpenID 1 no-encryption association response
606
+ msg = Message.from_openid_args({
607
+ 'expires_in' => '1000',
608
+ 'assoc_handle' => 'a handle',
609
+ 'assoc_type' => 'HMAC-SHA1',
610
+ 'mac_key' => 'X' * 20,
611
+ })
612
+
613
+ # Should succeed
614
+ assoc = @assoc_manager.send(:extract_association, msg, assoc_session)
615
+ assert_equal('a handle', assoc.handle)
616
+ assert_equal('HMAC-SHA1', assoc.assoc_type)
617
+ assert(assoc.expires_in.between?(999, 1000))
618
+ assert('X' * 20, assoc.secret)
619
+ end
620
+ end
621
+
622
+ class GetOpenIDSessionTypeTest < Test::Unit::TestCase
623
+ include TestUtil
624
+
625
+ SERVER_URL = 'http://invalid/'
626
+
627
+ def do_test(expected_session_type, session_type_value)
628
+ # Create a Message with just 'session_type' in it, since
629
+ # that's all this function will use. 'session_type' may be
630
+ # absent if it's set to None.
631
+ args = {}
632
+ if !session_type_value.nil?
633
+ args['session_type'] = session_type_value
634
+ end
635
+ message = Message.from_openid_args(args)
636
+ assert(message.is_openid1)
637
+
638
+ assoc_manager = Consumer::AssociationManager.new(nil, SERVER_URL)
639
+ actual_session_type = assoc_manager.send(:get_openid1_session_type,
640
+ message)
641
+ error_message = ("Returned session type parameter #{session_type_value}"\
642
+ "was expected to yield session type "\
643
+ "#{expected_session_type}, but yielded "\
644
+ "#{actual_session_type}")
645
+ assert_equal(expected_session_type, actual_session_type, error_message)
646
+ end
647
+
648
+
649
+ [['nil', 'no-encryption', nil],
650
+ ['empty', 'no-encryption', ''],
651
+ ['dh_sha1', 'DH-SHA1', 'DH-SHA1'],
652
+ ['dh_sha256', 'DH-SHA256', 'DH-SHA256'],
653
+ ].each {|name, expected, input|
654
+ # Define a test method that will check what session type will be
655
+ # used if the OpenID 1 response to an associate call sets the
656
+ # 'session_type' field to `session_type_value`
657
+ test = lambda {assert_log_matches() { do_test(expected, input) } }
658
+ define_method("test_#{name}", &test)
659
+ }
660
+
661
+ # This one's different because it expects log messages
662
+ def test_explicit_no_encryption
663
+ assert_log_matches("WARNING: #{SERVER_URL} sent 'no-encryption'"){
664
+ do_test('no-encryption', 'no-encryption')
665
+ }
666
+ end
667
+ end
668
+
669
+ class ExtractAssociationTest < Test::Unit::TestCase
670
+ include ProtocolErrorMixin
671
+
672
+ SERVER_URL = 'http://invalid/'
673
+
674
+ def setup
675
+ @session_type = 'testing-session'
676
+
677
+ # This must something that works for Association::from_expires_in
678
+ @assoc_type = 'HMAC-SHA1'
679
+
680
+ @assoc_handle = 'testing-assoc-handle'
681
+
682
+ # These arguments should all be valid
683
+ @assoc_response =
684
+ Message.from_openid_args({
685
+ 'expires_in' => '1000',
686
+ 'assoc_handle' => @assoc_handle,
687
+ 'assoc_type' => @assoc_type,
688
+ 'session_type' => @session_type,
689
+ 'ns' => OPENID2_NS,
690
+ })
691
+ assoc_session_cls = Class.new do
692
+ class << self
693
+ attr_accessor :allowed_assoc_types, :session_type
694
+ end
695
+
696
+ attr_reader :extract_secret_called, :secret
697
+ def initialize
698
+ @extract_secret_called = false
699
+ @secret = 'shhhhh!'
700
+ end
701
+
702
+ def extract_secret(_)
703
+ @extract_secret_called = true
704
+ @secret
705
+ end
706
+ end
707
+ @assoc_session = assoc_session_cls.new
708
+ @assoc_session.class.allowed_assoc_types = [@assoc_type]
709
+ @assoc_session.class.session_type = @session_type
710
+
711
+ @assoc_manager = Consumer::AssociationManager.new(nil, SERVER_URL)
712
+ end
713
+
714
+ def call_extract
715
+ @assoc_manager.send(:extract_association,
716
+ @assoc_response, @assoc_session)
717
+ end
718
+
719
+ # Handle a full successful association response
720
+ def test_works_with_good_fields
721
+ assoc = call_extract
722
+ assert(@assoc_session.extract_secret_called)
723
+ assert_equal(@assoc_session.secret, assoc.secret)
724
+ assert_equal(1000, assoc.lifetime)
725
+ assert_equal(@assoc_handle, assoc.handle)
726
+ assert_equal(@assoc_type, assoc.assoc_type)
727
+ end
728
+
729
+ def test_bad_assoc_type
730
+ # Make sure that the assoc type in the response is not valid
731
+ # for the given session.
732
+ @assoc_session.class.allowed_assoc_types = []
733
+ assert_protocol_error('Unsupported assoc_type for sess') {call_extract}
734
+ end
735
+
736
+ def test_bad_expires_in
737
+ # Invalid value for expires_in should cause failure
738
+ @assoc_response.set_arg(OPENID_NS, 'expires_in', 'forever')
739
+ assert_protocol_error('Invalid expires_in') {call_extract}
740
+ end
741
+ end
742
+
743
+ class TestExtractAssociationDiffieHellman < Test::Unit::TestCase
744
+ include ProtocolErrorMixin
745
+
746
+ SECRET = 'x' * 20
747
+
748
+ def setup
749
+ @assoc_manager = Consumer::AssociationManager.new(nil, nil)
750
+ end
751
+
752
+ def setup_dh
753
+ sess, message = @assoc_manager.send(:create_associate_request,
754
+ 'HMAC-SHA1', 'DH-SHA1')
755
+
756
+ server_dh = DiffieHellman.new
757
+ cons_dh = sess.instance_variable_get('@dh')
758
+
759
+ enc_mac_key = server_dh.xor_secret(CryptUtil.method(:sha1),
760
+ cons_dh.public, SECRET)
761
+
762
+ server_resp = {
763
+ 'dh_server_public' => CryptUtil.num_to_base64(server_dh.public),
764
+ 'enc_mac_key' => Util.to_base64(enc_mac_key),
765
+ 'assoc_type' => 'HMAC-SHA1',
766
+ 'assoc_handle' => 'handle',
767
+ 'expires_in' => '1000',
768
+ 'session_type' => 'DH-SHA1',
769
+ }
770
+ if @assoc_manager.instance_variable_get(:@compatibility_mode)
771
+ server_resp['ns'] = OPENID2_NS
772
+ end
773
+ return [sess, Message.from_openid_args(server_resp)]
774
+ end
775
+
776
+ def test_success
777
+ sess, server_resp = setup_dh
778
+ ret = @assoc_manager.send(:extract_association, server_resp, sess)
779
+ assert(!ret.nil?)
780
+ assert_equal(ret.assoc_type, 'HMAC-SHA1')
781
+ assert_equal(ret.secret, SECRET)
782
+ assert_equal(ret.handle, 'handle')
783
+ assert_equal(ret.lifetime, 1000)
784
+ end
785
+
786
+ def test_openid2success
787
+ # Use openid 1 type in endpoint so _setUpDH checks
788
+ # compatibility mode state properly
789
+ @assoc_manager.instance_variable_set('@compatibility_mode', true)
790
+ test_success()
791
+ end
792
+
793
+ def test_bad_dh_values
794
+ sess, server_resp = setup_dh
795
+ server_resp.set_arg(OPENID_NS, 'enc_mac_key', '\x00\x00\x00')
796
+ assert_protocol_error('Malformed response for') {
797
+ @assoc_manager.send(:extract_association, server_resp, sess)
798
+ }
799
+ end
800
+ end
801
+
802
+ class TestAssocManagerGetAssociation < Test::Unit::TestCase
803
+ include FetcherMixin
804
+ include TestUtil
805
+
806
+ attr_reader :negotiate_association
807
+
808
+ def setup
809
+ @server_url = 'http://invalid/'
810
+ @store = Store::Memory.new
811
+ @assoc_manager = Consumer::AssociationManager.new(@store, @server_url)
812
+ @assoc_manager.extend(Const)
813
+ @assoc = Association.new('handle', 'secret', Time.now, 10000,
814
+ 'HMAC-SHA1')
815
+ end
816
+
817
+ def set_negotiate_response(assoc)
818
+ @assoc_manager.const(:negotiate_association, assoc)
819
+ end
820
+
821
+ def test_not_in_store_no_response
822
+ set_negotiate_response(nil)
823
+ assert_equal(nil, @assoc_manager.get_association)
824
+ end
825
+
826
+ def test_not_in_store_negotiate_assoc
827
+ # Not stored beforehand:
828
+ stored_assoc = @store.get_association(@server_url, @assoc.handle)
829
+ assert_equal(nil, stored_assoc)
830
+
831
+ # Returned from associate call:
832
+ set_negotiate_response(@assoc)
833
+ assert_equal(@assoc, @assoc_manager.get_association)
834
+
835
+ # It should have been stored:
836
+ stored_assoc = @store.get_association(@server_url, @assoc.handle)
837
+ assert_equal(@assoc, stored_assoc)
838
+ end
839
+
840
+ def test_in_store_no_response
841
+ set_negotiate_response(nil)
842
+ @store.store_association(@server_url, @assoc)
843
+ assert_equal(@assoc, @assoc_manager.get_association)
844
+ end
845
+
846
+ def test_request_assoc_with_status_error
847
+ fetcher_class = Class.new do
848
+ define_method(:fetch) do |*args|
849
+ MockResponse.new(500, '')
850
+ end
851
+ end
852
+ with_fetcher(fetcher_class.new) do
853
+ assert_log_matches('Got HTTP status error when requesting') {
854
+ result = @assoc_manager.send(:request_association, 'HMAC-SHA1',
855
+ 'no-encryption')
856
+ assert(result.nil?)
857
+ }
858
+ end
859
+ end
860
+ end
861
+
862
+ class TestAssocManagerRequestAssociation < Test::Unit::TestCase
863
+ include FetcherMixin
864
+ include TestUtil
865
+
866
+ def setup
867
+ @assoc_manager = Consumer::AssociationManager.new(nil, 'http://invalid/')
868
+ @assoc_type = 'HMAC-SHA1'
869
+ @session_type = 'no-encryption'
870
+ @message = Message.new(OPENID2_NS)
871
+ @message.update_args(OPENID_NS, {
872
+ 'assoc_type' => @assoc_type,
873
+ 'session_type' => @session_type,
874
+ 'assoc_handle' => 'kaboodle',
875
+ 'expires_in' => '1000',
876
+ 'mac_key' => 'X' * 20,
877
+ })
878
+ end
879
+
880
+ def make_request
881
+ kv = @message.to_kvform
882
+ fetcher_class = Class.new do
883
+ define_method(:fetch) do |*args|
884
+ MockResponse.new(200, kv)
885
+ end
886
+ end
887
+ with_fetcher(fetcher_class.new) do
888
+ @assoc_manager.send(:request_association, @assoc_type, @session_type)
889
+ end
890
+ end
891
+
892
+ # The association we get is from valid processing of our result,
893
+ # and that no errors are raised
894
+ def test_success
895
+ assert_equal('kaboodle', make_request.handle)
896
+ end
897
+
898
+ # A missing parameter gets translated into a log message and
899
+ # causes the method to return nil
900
+ def test_missing_fields
901
+ @message.del_arg(OPENID_NS, 'assoc_type')
902
+ assert_log_matches('Missing required par') {
903
+ assert_equal(nil, make_request)
904
+ }
905
+ end
906
+
907
+ # A bad value results in a log message and causes the method to
908
+ # return nil
909
+ def test_protocol_error
910
+ @message.set_arg(OPENID_NS, 'expires_in', 'goats')
911
+ assert_log_matches('Protocol error processing') {
912
+ assert_equal(nil, make_request)
913
+ }
914
+ end
915
+ end
916
+
917
+ end