ruby-openid 1.0

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 (114) hide show
  1. data/COPYING +21 -0
  2. data/INSTALL +34 -0
  3. data/README +67 -0
  4. data/TODO +9 -0
  5. data/examples/README +54 -0
  6. data/examples/cacert.pem +7815 -0
  7. data/examples/consumer.rb +285 -0
  8. data/examples/openid-store/associations/http-localhost_3A3000_2Fserver-EMQbAy3NnHVzA.s0u5KAcplKGzo +6 -0
  9. data/examples/openid-store/auth_key +1 -0
  10. data/examples/rails_active_record_store/README +59 -0
  11. data/examples/rails_active_record_store/XX_add_openidstore.rb +30 -0
  12. data/examples/rails_active_record_store/models/openid_association.rb +12 -0
  13. data/examples/rails_active_record_store/models/openid_nonce.rb +3 -0
  14. data/examples/rails_active_record_store/models/openid_setting.rb +2 -0
  15. data/examples/rails_active_record_store/openid_helper.rb +91 -0
  16. data/examples/rails_active_record_store/openidstore_test.rb +15 -0
  17. data/examples/rails_active_record_store/schema.mysql.sql +22 -0
  18. data/examples/rails_active_record_store/schema.postgresql.sql +21 -0
  19. data/examples/rails_active_record_store/schema.sqlite.sql +21 -0
  20. data/examples/rails_openid_login_generator/USAGE +23 -0
  21. data/examples/rails_openid_login_generator/openid_login_generator.rb +36 -0
  22. data/examples/rails_openid_login_generator/templates/README +116 -0
  23. data/examples/rails_openid_login_generator/templates/controller.rb +116 -0
  24. data/examples/rails_openid_login_generator/templates/controller_test.rb +0 -0
  25. data/examples/rails_openid_login_generator/templates/helper.rb +2 -0
  26. data/examples/rails_openid_login_generator/templates/openid_login_system.rb +87 -0
  27. data/examples/rails_openid_login_generator/templates/user.rb +14 -0
  28. data/examples/rails_openid_login_generator/templates/user_test.rb +0 -0
  29. data/examples/rails_openid_login_generator/templates/users.yml +0 -0
  30. data/examples/rails_openid_login_generator/templates/view_login.rhtml +15 -0
  31. data/examples/rails_openid_login_generator/templates/view_logout.rhtml +10 -0
  32. data/examples/rails_openid_login_generator/templates/view_welcome.rhtml +9 -0
  33. data/examples/rails_server/README +153 -0
  34. data/examples/rails_server/Rakefile +10 -0
  35. data/examples/rails_server/app/controllers/application.rb +4 -0
  36. data/examples/rails_server/app/controllers/login_controller.rb +35 -0
  37. data/examples/rails_server/app/controllers/server_controller.rb +185 -0
  38. data/examples/rails_server/app/helpers/application_helper.rb +3 -0
  39. data/examples/rails_server/app/helpers/login_helper.rb +2 -0
  40. data/examples/rails_server/app/helpers/server_helper.rb +9 -0
  41. data/examples/rails_server/app/views/layouts/server.rhtml +61 -0
  42. data/examples/rails_server/app/views/login/index.rhtml +32 -0
  43. data/examples/rails_server/app/views/server/decide.rhtml +11 -0
  44. data/examples/rails_server/config/boot.rb +19 -0
  45. data/examples/rails_server/config/database.yml +85 -0
  46. data/examples/rails_server/config/environment.rb +53 -0
  47. data/examples/rails_server/config/environments/development.rb +19 -0
  48. data/examples/rails_server/config/environments/production.rb +19 -0
  49. data/examples/rails_server/config/environments/test.rb +19 -0
  50. data/examples/rails_server/config/routes.rb +23 -0
  51. data/examples/rails_server/db/openid-store/associations/http-localhost_2F_7Cnormal-YU.tkND1J4fEZhnuAoT5Zc0yCA0 +6 -0
  52. data/examples/rails_server/doc/README_FOR_APP +2 -0
  53. data/examples/rails_server/log/development.log +6059 -0
  54. data/examples/rails_server/log/production.log +0 -0
  55. data/examples/rails_server/log/server.log +0 -0
  56. data/examples/rails_server/log/test.log +0 -0
  57. data/examples/rails_server/public/404.html +8 -0
  58. data/examples/rails_server/public/500.html +8 -0
  59. data/examples/rails_server/public/dispatch.cgi +12 -0
  60. data/examples/rails_server/public/dispatch.fcgi +26 -0
  61. data/examples/rails_server/public/dispatch.rb +12 -0
  62. data/examples/rails_server/public/favicon.ico +0 -0
  63. data/examples/rails_server/public/images/rails.png +0 -0
  64. data/examples/rails_server/public/javascripts/controls.js +750 -0
  65. data/examples/rails_server/public/javascripts/dragdrop.js +584 -0
  66. data/examples/rails_server/public/javascripts/effects.js +854 -0
  67. data/examples/rails_server/public/javascripts/prototype.js +1785 -0
  68. data/examples/rails_server/public/robots.txt +1 -0
  69. data/examples/rails_server/script/about +3 -0
  70. data/examples/rails_server/script/breakpointer +3 -0
  71. data/examples/rails_server/script/console +3 -0
  72. data/examples/rails_server/script/destroy +3 -0
  73. data/examples/rails_server/script/generate +3 -0
  74. data/examples/rails_server/script/performance/benchmarker +3 -0
  75. data/examples/rails_server/script/performance/profiler +3 -0
  76. data/examples/rails_server/script/plugin +3 -0
  77. data/examples/rails_server/script/process/reaper +3 -0
  78. data/examples/rails_server/script/process/spawner +3 -0
  79. data/examples/rails_server/script/process/spinner +3 -0
  80. data/examples/rails_server/script/runner +3 -0
  81. data/examples/rails_server/script/server +3 -0
  82. data/examples/rails_server/test/functional/login_controller_test.rb +18 -0
  83. data/examples/rails_server/test/functional/server_controller_test.rb +18 -0
  84. data/examples/rails_server/test/test_helper.rb +28 -0
  85. data/lib/hmac-md5.rb +11 -0
  86. data/lib/hmac-rmd160.rb +11 -0
  87. data/lib/hmac-sha1.rb +11 -0
  88. data/lib/hmac-sha2.rb +25 -0
  89. data/lib/hmac.rb +112 -0
  90. data/lib/openid/association.rb +109 -0
  91. data/lib/openid/consumer.rb +928 -0
  92. data/lib/openid/dh.rb +48 -0
  93. data/lib/openid/discovery.rb +89 -0
  94. data/lib/openid/fetchers.rb +119 -0
  95. data/lib/openid/filestore.rb +315 -0
  96. data/lib/openid/htmltokenizer.rb +355 -0
  97. data/lib/openid/parse.rb +23 -0
  98. data/lib/openid/server.rb +951 -0
  99. data/lib/openid/service.rb +135 -0
  100. data/lib/openid/stores.rb +178 -0
  101. data/lib/openid/trustroot.rb +100 -0
  102. data/lib/openid/util.rb +273 -0
  103. data/test/assoc.rb +38 -0
  104. data/test/consumer.rb +384 -0
  105. data/test/dh.rb +20 -0
  106. data/test/extensions.rb +30 -0
  107. data/test/linkparse.rb +305 -0
  108. data/test/runtests.rb +11 -0
  109. data/test/server2.rb +1053 -0
  110. data/test/storetestcase.rb +172 -0
  111. data/test/teststore.rb +23 -0
  112. data/test/trustroot.rb +113 -0
  113. data/test/util.rb +56 -0
  114. metadata +218 -0
@@ -0,0 +1,355 @@
1
+ # = HTMLTokenizer
2
+ #
3
+ # Author:: Ben Giddings (mailto:bg-rubyforge@infofiend.com)
4
+ # Copyright:: Copyright (c) 2004 Ben Giddings
5
+ # License:: Distributes under the same terms as Ruby
6
+ #
7
+ #
8
+ # This is a partial port of the functionality behind Perl's TokeParser
9
+ # Provided a page it progressively returns tokens from that page
10
+ #
11
+ # $Id: htmltokenizer.rb,v 1.7 2005/06/07 21:05:53 merc Exp $
12
+
13
+ #
14
+ # A class to tokenize HTML.
15
+ #
16
+ # Example:
17
+ #
18
+ # page = "<HTML>
19
+ # <HEAD>
20
+ # <TITLE>This is the title</TITLE>
21
+ # </HEAD>
22
+ # <!-- Here comes the <a href=\"missing.link\">blah</a>
23
+ # comment body
24
+ # -->
25
+ # <BODY>
26
+ # <H1>This is the header</H1>
27
+ # <P>
28
+ # This is the paragraph, it contains
29
+ # <a href=\"link.html\">links</a>,
30
+ # <img src=\"blah.gif\" optional alt='images
31
+ # are
32
+ # really cool'>. Ok, here is some more text and
33
+ # <A href=\"http://another.link.com/\" target=\"_blank\">another link</A>.
34
+ # </P>
35
+ # </body>
36
+ # </HTML>
37
+ # "
38
+ # toke = HTMLTokenizer.new(page)
39
+ #
40
+ # assert("<h1>" == toke.getTag("h1", "h2", "h3").to_s.downcase)
41
+ # assert(HTMLTag.new("<a href=\"link.html\">") == toke.getTag("IMG", "A"))
42
+ # assert("links" == toke.getTrimmedText)
43
+ # assert(toke.getTag("IMG", "A").attr_hash['optional'])
44
+ # assert("_blank" == toke.getTag("IMG", "A").attr_hash['target'])
45
+ #
46
+ class HTMLTokenizer
47
+ @@version = 1.0
48
+
49
+ # Get version of HTMLTokenizer lib
50
+ def self.version
51
+ @@version
52
+ end
53
+
54
+ attr_reader :page
55
+
56
+ # Create a new tokenizer, based on the content, used as a string.
57
+ def initialize(content)
58
+ @page = content.to_s
59
+ @cur_pos = 0
60
+ end
61
+
62
+ # Reset the parser, setting the current position back at the stop
63
+ def reset
64
+ @cur_pos = 0
65
+ end
66
+
67
+ # Look at the next token, but don't actually grab it
68
+ def peekNextToken
69
+ if @cur_pos == @page.length then return nil end
70
+
71
+ if ?< == @page[@cur_pos]
72
+ # Next token is a tag of some kind
73
+ if '!--' == @page[(@cur_pos + 1), 3]
74
+ # Token is a comment
75
+ tag_end = @page.index('-->', (@cur_pos + 1))
76
+ if tag_end.nil?
77
+ raise "No end found to started comment:\n#{@page[@cur_pos,80]}"
78
+ end
79
+ # p @page[@cur_pos .. (tag_end+2)]
80
+ HTMLComment.new(@page[@cur_pos .. (tag_end + 2)])
81
+ else
82
+ # Token is a html tag
83
+ tag_end = @page.index('>', (@cur_pos + 1))
84
+ if tag_end.nil?
85
+ raise "No end found to started tag:\n#{@page[@cur_pos,80]}"
86
+ end
87
+ # p @page[@cur_pos .. tag_end]
88
+ HTMLTag.new(@page[@cur_pos .. tag_end])
89
+ end
90
+ else
91
+ # Next token is text
92
+ text_end = @page.index('<', @cur_pos)
93
+ text_end = text_end.nil? ? -1 : (text_end - 1)
94
+ # p @page[@cur_pos .. text_end]
95
+ HTMLText.new(@page[@cur_pos .. text_end])
96
+ end
97
+ end
98
+
99
+ # Get the next token, returns an instance of
100
+ # * HTMLText
101
+ # * HTMLToken
102
+ # * HTMLTag
103
+ def getNextToken
104
+ token = peekNextToken
105
+ if token
106
+ # @page = @page[token.raw.length .. -1]
107
+ # @page.slice!(0, token.raw.length)
108
+ @cur_pos += token.raw.length
109
+ end
110
+ #p token
111
+ #print token.raw
112
+ return token
113
+ end
114
+
115
+ # Get a tag from the specified set of desired tags.
116
+ # For example:
117
+ # <tt>foo = toke.getTag("h1", "h2", "h3")</tt>
118
+ # Will return the next header tag encountered.
119
+ def getTag(*sought_tags)
120
+ sought_tags.collect! {|elm| elm.downcase}
121
+
122
+ while (tag = getNextToken)
123
+ if tag.kind_of?(HTMLTag) and
124
+ (0 == sought_tags.length or sought_tags.include?(tag.tag_name))
125
+ break
126
+ end
127
+ end
128
+ tag
129
+ end
130
+
131
+ # Get all the text between the current position and the next tag
132
+ # (if specified) or a specific later tag
133
+ def getText(until_tag = nil)
134
+ if until_tag.nil?
135
+ if ?< == @page[@cur_pos]
136
+ # Next token is a tag, not text
137
+ ""
138
+ else
139
+ # Next token is text
140
+ getNextToken.text
141
+ end
142
+ else
143
+ ret_str = ""
144
+
145
+ while (tag = peekNextToken)
146
+ if tag.kind_of?(HTMLTag) and tag.tag_name == until_tag
147
+ break
148
+ end
149
+
150
+ if ("" != tag.text)
151
+ ret_str << (tag.text + " ")
152
+ end
153
+ getNextToken
154
+ end
155
+
156
+ ret_str
157
+ end
158
+ end
159
+
160
+ # Like getText, but squeeze all whitespace, getting rid of
161
+ # leading and trailing whitespace, and squeezing multiple
162
+ # spaces into a single space.
163
+ def getTrimmedText(until_tag = nil)
164
+ getText(until_tag).strip.gsub(/\s+/m, " ")
165
+ end
166
+
167
+ end
168
+
169
+ # The parent class for all three types of HTML tokens
170
+ class HTMLToken
171
+ attr_accessor :raw
172
+
173
+ # Initialize the token based on the raw text
174
+ def initialize(text)
175
+ @raw = text
176
+ end
177
+
178
+ # By default, return exactly the string used to create the text
179
+ def to_s
180
+ raw
181
+ end
182
+
183
+ # By default tokens have no text representation
184
+ def text
185
+ ""
186
+ end
187
+
188
+ def trimmed_text
189
+ text.strip.gsub(/\s+/m, " ")
190
+ end
191
+
192
+ # Compare to another based on the raw source
193
+ def ==(other)
194
+ raw == other.to_s
195
+ end
196
+ end
197
+
198
+ # Class representing text that isn't inside a tag
199
+ class HTMLText < HTMLToken
200
+ def text
201
+ raw
202
+ end
203
+ end
204
+
205
+ # Class representing an HTML comment
206
+ class HTMLComment < HTMLToken
207
+ attr_accessor :contents
208
+ def initialize(text)
209
+ super(text)
210
+ temp_arr = text.scan(/^<!--\s*(.*?)\s*-->$/m)
211
+ if temp_arr[0].nil?
212
+ raise "Text passed to HTMLComment.initialize is not a comment"
213
+ end
214
+
215
+ @contents = temp_arr[0][0]
216
+ end
217
+ end
218
+
219
+ # Class representing an HTML tag
220
+ class HTMLTag < HTMLToken
221
+ attr_reader :end_tag, :tag_name
222
+ def initialize(text)
223
+ super(text)
224
+ if ?< != text[0] or ?> != text[-1]
225
+ raise "Text passed to HTMLComment.initialize is not a comment"
226
+ end
227
+
228
+ @attr_hash = Hash.new
229
+ @raw = text
230
+
231
+ tag_name = text.scan(/[\w:-]+/)[0]
232
+ if tag_name.nil?
233
+ raise "Error, tag is nil: #{tag_name}"
234
+ end
235
+
236
+ if ?/ == text[1]
237
+ # It's an end tag
238
+ @end_tag = true
239
+ @tag_name = '/' + tag_name.downcase
240
+ else
241
+ @end_tag = false
242
+ @tag_name = tag_name.downcase
243
+ end
244
+
245
+ @hashed = false
246
+ end
247
+
248
+ # Retrieve a hash of all the tag's attributes.
249
+ # Lazily done, so that if you don't look at a tag's attributes
250
+ # things go quicker
251
+ def attr_hash
252
+ # Lazy initialize == don't build the hash until it's needed
253
+ if !@hashed
254
+ if !@end_tag
255
+ # Get the attributes
256
+ attr_arr = @raw.scan(/<[\w:-]+\s+(.*)>/m)[0]
257
+ if attr_arr.kind_of?(Array)
258
+ # Attributes found, parse them
259
+ attrs = attr_arr[0]
260
+ attr_arr = attrs.scan(/\s*([\w:-]+)(?:\s*=\s*("[^"]*"|'[^']*'|([^"'>][^\s>]*)))?/m)
261
+ # clean up the array by:
262
+ # * setting all nil elements to true
263
+ # * removing enclosing quotes
264
+ attr_arr.each {
265
+ |item|
266
+ val = if item[1].nil?
267
+ item[0]
268
+ elsif '"'[0] == item[1][0] or '\''[0] == item[1][0]
269
+ item[1][1 .. -2]
270
+ else
271
+ item[1]
272
+ end
273
+ @attr_hash[item[0].downcase] = val
274
+ }
275
+ end
276
+ end
277
+ @hashed = true
278
+ end
279
+
280
+ #p self
281
+
282
+ @attr_hash
283
+ end
284
+
285
+ # Get the 'alt' text for a tag, if it exists, or an empty string otherwise
286
+ def text
287
+ if !end_tag
288
+ case tag_name
289
+ when 'img'
290
+ if !attr_hash['alt'].nil?
291
+ return attr_hash['alt']
292
+ end
293
+ when 'applet'
294
+ if !attr_hash['alt'].nil?
295
+ return attr_hash['alt']
296
+ end
297
+ end
298
+ end
299
+ return ''
300
+ end
301
+ end
302
+
303
+ if $0 == __FILE__
304
+ require 'test/unit'
305
+
306
+ class TC_TestHTMLTokenizer < Test::Unit::TestCase
307
+ def test_bad_link
308
+ toke = HTMLTokenizer.new("<p><a href=http://bad.com/link>foo</a></p>")
309
+ assert("http://bad.com/link" == toke.getTag("a").attr_hash['href'])
310
+ end
311
+
312
+ def test_namespace
313
+ toke = HTMLTokenizer.new("<f:table xmlns:f=\"http://www.com/foo\">")
314
+ assert("http://www.com/foo" == toke.getTag("f:table").attr_hash['xmlns:f'])
315
+ end
316
+
317
+ def test_comment
318
+ toke = HTMLTokenizer.new("<!-- comment on me -->")
319
+ t = toke.getNextToken
320
+ assert(HTMLComment == t.class)
321
+ assert("comment on me" == t.contents)
322
+ end
323
+
324
+
325
+ def test_full
326
+ page = "<HTML>
327
+ <HEAD>
328
+ <TITLE>This is the title</TITLE>
329
+ </HEAD>
330
+ <!-- Here comes the <a href=\"missing.link\">blah</a>
331
+ comment body
332
+ -->
333
+ <BODY>
334
+ <H1>This is the header</H1>
335
+ <P>
336
+ This is the paragraph, it contains
337
+ <a href=\"link.html\">links</a>,
338
+ <img src=\"blah.gif\" optional alt='images
339
+ are
340
+ really cool'>. Ok, here is some more text and
341
+ <A href=\"http://another.link.com/\" target=\"_blank\">another link</A>.
342
+ </P>
343
+ </body>
344
+ </HTML>
345
+ "
346
+ toke = HTMLTokenizer.new(page)
347
+
348
+ assert("<h1>" == toke.getTag("h1", "h2", "h3").to_s.downcase)
349
+ assert(HTMLTag.new("<a href=\"link.html\">") == toke.getTag("IMG", "A"))
350
+ assert("links" == toke.getTrimmedText)
351
+ assert(toke.getTag("IMG", "A").attr_hash['optional'])
352
+ assert("_blank" == toke.getTag("IMG", "A").attr_hash['target'])
353
+ end
354
+ end
355
+ end
@@ -0,0 +1,23 @@
1
+ require "openid/htmltokenizer"
2
+
3
+ def parse_link_attrs(data)
4
+ parser = HTMLTokenizer.new(data)
5
+ in_head = false
6
+ begin
7
+ while el = parser.getTag("head", "link", "body")
8
+ if el.tag_name == "head"
9
+ in_head = true
10
+ elsif el.tag_name == "link"
11
+ continue unless in_head
12
+ yield el.attr_hash
13
+ elsif el.tag_name == "body"
14
+ return
15
+ end
16
+ end
17
+ rescue
18
+ return
19
+ end
20
+ end
21
+
22
+
23
+
@@ -0,0 +1,951 @@
1
+ require 'openid/util'
2
+ require 'openid/association'
3
+ require 'openid/dh'
4
+ require 'openid/trustroot'
5
+
6
+ module OpenID
7
+
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
+ module Server
83
+
84
+ HTTP_REDIRECT = 302
85
+ HTTP_OK = 200
86
+ HTTP_ERROR = 400
87
+
88
+ BROWSER_REQUEST_MODES = ['checkid_setup', 'checkid_immediate']
89
+ OPENID_PREFIX = 'openid.'
90
+
91
+ ENCODE_KVFORM = ['kvform'].freeze
92
+ ENCODE_URL = ['URL/redirect'].freeze
93
+
94
+ # Represents an incoming OpenID request.
95
+ class OpenIDRequest
96
+
97
+ attr_reader :mode
98
+
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
103
+ end
104
+
105
+ end
106
+
107
+ # A request to verify the validity of a previous response.
108
+ class CheckAuthRequest < OpenIDRequest
109
+
110
+ attr_accessor :assoc_handle, :sig, :signed, :invalidate_handle
111
+
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"
123
+ #
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.
128
+ #
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')
134
+ @assoc_handle = assoc_handle
135
+ @sig = sig
136
+ @signed = signed
137
+ @invalidate_handle = invalidate_handle
138
+ end
139
+
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")
152
+ end
153
+
154
+ signed = signed.split(',')
155
+ signed_pairs = []
156
+
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]
167
+ end
168
+
169
+ return new(assoc_handle, sig, signed_pairs, invalidate_handle)
170
+ end
171
+
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.
177
+ def answer(signatory)
178
+ is_valid = signatory.verify(@assoc_handle, @sig, @signed)
179
+ signatory.invalidate(assoc_handle, true)
180
+
181
+ response = OpenIDResponse.new(self)
182
+ response.fields['is_valid'] = is_valid ? 'true' : 'false'
183
+
184
+ if @invalidate_handle
185
+ assoc = signatory.get_association(@invalidate_handle, false)
186
+ unless assoc
187
+ response.fields['invalidate_handle'] = @invalidate_handle
188
+ end
189
+ end
190
+
191
+ return response
192
+ end
193
+
194
+ end
195
+
196
+ # An object that knows how to handle association requests with no
197
+ # session type.
198
+ class PlainTextServerSession
199
+
200
+ attr_reader :session_type
201
+
202
+ def initialize
203
+ @session_type = 'plaintext'
204
+ end
205
+
206
+ def PlainTextServerSession.from_query(query)
207
+ new
208
+ end
209
+
210
+ def answer(secret)
211
+ return {'mac_key' => OpenID::Util.to_base64(secret)}
212
+ end
213
+
214
+ end
215
+
216
+ # An object that knows how to handle Diffie Hellman (DH-SHA1) association
217
+ # requests.
218
+ class DiffieHellmanServerSession
219
+
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
+ def initialize(dh, consumer_pubkey)
233
+ @dh = dh
234
+ @consumer_pubkey = consumer_pubkey
235
+ @session_type = 'DH-SHA1'
236
+ end
237
+
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)
245
+
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)
250
+ end
251
+
252
+ consumer_pubkey = OpenID::Util.base64_to_num(consumer_pubkey)
253
+ return new(dh, consumer_pubkey)
254
+ end
255
+
256
+ # Generate the arguments to be added to the response using +secret+.
257
+ def answer(secret)
258
+ mac_key = @dh.xor_secrect(@consumer_pubkey, secret)
259
+ return {
260
+ 'dh_server_public' => OpenID::Util.num_to_base64(@dh.public),
261
+ 'enc_mac_key' => OpenID::Util.to_base64(mac_key)
262
+ }
263
+ end
264
+
265
+ end
266
+
267
+ # A request to establish an OpenID association. This object contains the
268
+ # logic for handling "openid.mode=associate" requests.
269
+ class AssociateRequest < OpenIDRequest
270
+
271
+ attr_accessor :assoc_type, :session
272
+
273
+ # +session+ is an instance of PlainTextServerSession or
274
+ # DiffieHellmanServerSession
275
+ def initialize(session)
276
+ super('associate')
277
+ @assoc_type = 'HMAC-SHA1'
278
+ @session = session
279
+ end
280
+
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)
289
+ else
290
+ raise ProtocolError.new(query,
291
+ "Unknown session_type #{session_type}")
292
+ end
293
+
294
+ return new(session)
295
+ end
296
+
297
+ # Respond to this request with an association. +assoc+ is an
298
+ # OpenID::Association instance that represents the association to
299
+ # respond with.
300
+ def answer(assoc)
301
+ response = OpenIDResponse.new(self)
302
+
303
+ fields = {
304
+ 'expires_in' => assoc.expires_in.to_s,
305
+ 'assoc_type' => 'HMAC-SHA1',
306
+ 'assoc_handle' => assoc.handle
307
+ }
308
+
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
315
+ end
316
+
317
+ return response
318
+ end
319
+
320
+ end
321
+
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.
325
+ 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)
340
+ @identity = identity
341
+ @return_to = return_to
342
+ @trust_root = trust_root
343
+ @immediate = mode == 'checkid_immediate' ? true : false
344
+ @assoc_handle = assoc_handle
345
+ @query = {}
346
+ 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
+
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
358
+
359
+ trust_root = query['openid.trust_root']
360
+
361
+ unless OpenID::TrustRoot.parse(return_to)
362
+ raise MalformedReturnURL.new(query, return_to)
363
+ end
364
+
365
+ if trust_root and not OpenID::TrustRoot.parse(return_to)
366
+ raise MalformedTrustRoot.new(query, trust_root)
367
+ end
368
+
369
+ assoc_handle = query['openid.assoc_handle']
370
+
371
+ req = new(mode, identity, return_to, trust_root, assoc_handle)
372
+ req.query = query
373
+
374
+ unless req.trust_root_valid
375
+ raise UntrustedReturnURL.new(query, return_to, trust_root)
376
+ end
377
+
378
+ return req
379
+ end
380
+
381
+ # Returns +true+ or +false+ according to whether the return_to
382
+ # is under the supplied trust_root.
383
+ def trust_root_valid
384
+ return true unless @trust_root
385
+ tr = OpenID::TrustRoot.parse(@trust_root)
386
+ raise MalformedTrustRoot.new(nil, @trust_root) if tr.nil?
387
+ return tr.validate_url(@return_to)
388
+ end
389
+
390
+ # Generate a response to this checkid_* request.
391
+ #
392
+ # ==Paramters
393
+ #
394
+ # [+allow+]
395
+ # Boolean value stating whether or not to allow this user to "claim"
396
+ # supplied identity and let the consumer have the information. The
397
+ # value of allow should be follow the following algorithm:
398
+ #
399
+ # The identity URL provided (openid.identity) and available
400
+ # through the +identity_url+ method of this object is owned by
401
+ # the logged in user, and they have approved the consumer receive
402
+ # the identity assertion.
403
+ #
404
+ # [+server_url+]
405
+ # When an immeditate mode request does not succeed, it gets back a URL
406
+ # where the request may be continued in a not-so-immediate fashion.
407
+ # The URL returned is generated using the supplied +server_url+ here.
408
+ # +server_url+ should be the full URL of you openid server endpoint.
409
+ def answer(allow, server_url=nil)
410
+ if allow or @immediate
411
+ mode = 'id_res'
412
+ else
413
+ mode = 'cancel'
414
+ end
415
+
416
+ response = OpenIDResponse.new(self)
417
+
418
+ if allow
419
+ response.add_fields(nil, {
420
+ 'mode' => mode,
421
+ 'identity' => @identity,
422
+ 'return_to' => @return_to
423
+ })
424
+ else
425
+ response.add_field(nil, 'mode', mode, false)
426
+ response.signed.clear
427
+ if @immediate
428
+ unless server_url
429
+ raise ArgumentError, "setup_url is required for allow=false in immediate mode"
430
+ end
431
+ # make a request just like this one, but immediate mode
432
+ setup_request = self.class.new('checkid_immediate',
433
+ @identity,
434
+ @return_to,
435
+ @trust_root)
436
+ setup_url = setup_request.encode_to_url(server_url)
437
+ response.add_field(nil, 'user_setup_url', setup_url, false)
438
+ end
439
+
440
+ end
441
+
442
+ return response
443
+ end
444
+
445
+ # Encode this request as a GET URL, returning the URL.
446
+ def encode_to_url(server_url)
447
+ q = {
448
+ 'openid.mode' => @mode,
449
+ 'openid.identity' => @identity,
450
+ 'openid.return_to' => @return_to
451
+ }
452
+
453
+ q['openid.trust_root'] = @trust_root if @trust_root
454
+ q['openid.assoc_handle'] = @assoc_handle if @assoc_handle
455
+ return OpenID::Util.append_args(server_url, q)
456
+ end
457
+
458
+ # Create the URL to cancel this request. Useful for creating a "Cancel"
459
+ # button on your "approve this openid transaction" form.
460
+ def cancel_url
461
+ if @immediate
462
+ raise ProtocolError.new(nil, 'cancel is not an appropriate reponse to immediate mode requests')
463
+ end
464
+ return OpenID::Util.append_args(@return_to,{'openid.mode' => 'cancel'})
465
+ end
466
+
467
+ # The identity_url which was requested to be verified. Your server
468
+ # should provide a page at identity_url, and be able to assert
469
+ # that the logged in user does or does not "own" that URL.
470
+ # "Owning" an identity_url is in the details of the
471
+ # server account name to URL mapping.
472
+ def identity_url
473
+ @identity
474
+ end
475
+
476
+ end
477
+
478
+ # Object representing a response to an OpenIDRequest
479
+ class OpenIDResponse
480
+
481
+ attr_accessor :request, :fields, :signed
482
+
483
+ # +request+ is a subclass of OpenIDRequest that this object
484
+ # should respond to.
485
+ def initialize(request)
486
+ @request = request
487
+ @fields = {}
488
+ @signed = []
489
+ end
490
+
491
+ # Add an extra field to this response.
492
+ #
493
+ # [+namespace+]
494
+ # Extension namespace the field is in with no leading "openid.". For
495
+ # example, if you are adding a simple registration argument, you would
496
+ # pass 'sreg' as the namespace.
497
+ def add_field(namespace, key, value, signed=true)
498
+ if namespace and namespace != ''
499
+ key = namespace + '.' + key
500
+ end
501
+ @fields[key] = value
502
+ if signed and not @signed.member?(key)
503
+ @signed << key
504
+ end
505
+ end
506
+
507
+ # Same as OpenIDResponse.add_field, except that it accepts a Hash
508
+ # fields to be added as the +fields+ argument.
509
+ def add_fields(namespace, fields, signed=true)
510
+ fields.each {|k,v| self.add_field(namespace, k, v, signed)}
511
+ end
512
+
513
+ # Update the fields of this request with another OpenIDResponse, +other+.
514
+ def update(namespace, other)
515
+ if namespace and namespace != ''
516
+ namespaced_fields = {}
517
+ other.fields.each {|k,v| namespaced_fields[namespace+'.'+k] = v}
518
+ namespaced_signed = other.signed.collect {|k| namespace+'.'+k}
519
+ else
520
+ namespaced_fields = other.fields.dup
521
+ namespaced_signed = other.signed.dup
522
+ end
523
+
524
+ @fields.update(namespaced_fields)
525
+ @signed |= namespaced_signed
526
+ end
527
+
528
+ # Returns a boolean saying whether or not this response requires
529
+ # signing.
530
+ def needs_signing?
531
+ ['checkid_immediate','checkid_setup'].member?(@request.mode) and \
532
+ @signed.length > 0
533
+ end
534
+
535
+ # OpenID responses can be sent back as a URL redirect or as a kvform
536
+ # reponse to a POST. This method returns a code describing how the
537
+ # response should be encoded, and return either
538
+ # OpenID::Server::ENCODE_URL or OpenID::Server::ENCODE_KVFORM.
539
+ def which_encoding?
540
+ if BROWSER_REQUEST_MODES.member?(@request.mode)
541
+ return ENCODE_URL
542
+ else
543
+ return ENCODE_KVFORM
544
+ end
545
+ end
546
+
547
+ # Encode the response to a URL, suitable to be send via 302 redirect.
548
+ def encode_to_url
549
+ fields = {}
550
+ @fields.each {|k,v| fields['openid.'+k] = v}
551
+ return OpenID::Util.append_args(@request.return_to, fields)
552
+ end
553
+
554
+ # Encode the response to kvform format.
555
+ def encode_to_kvform
556
+ return OpenID::Util.kvform(@fields)
557
+ end
558
+
559
+ end
560
+
561
+ # Object responsible for signing responses, and checking signatures.
562
+ class Signatory
563
+ @@secret_lifetime = 14 * 24 * 60 * 6
564
+ @@normal_key = 'http://localhost/|normal'
565
+ @@dumb_key = 'http://localhost/|dumb'
566
+
567
+ attr_reader :dumb_key
568
+
569
+ # Accepts an object that implements the OpenID::Store interface. See
570
+ # OpenID::FilesystemStore for a simple file based store.
571
+ def initialize(store)
572
+ @store = store
573
+ end
574
+
575
+ # Verify that the signature for some data is valid.
576
+ #
577
+ # ==Paramters
578
+ #
579
+ # [+assoc_handle+]
580
+ # The association handle used to get the secret out of the store, and
581
+ # passed in via openid.assoc_handle.
582
+ #
583
+ # [+sig+]
584
+ # Value of openid.sig
585
+ #
586
+ # [+signed_pairs+]
587
+ # Array of Array pairs of key, value signed data.
588
+ #
589
+ # [+dumb+]
590
+ # boolean representing whether this is a dumb mode request
591
+ def verify(assoc_handle, sig, signed_pairs, dumb=true)
592
+ assoc = self.get_association(assoc_handle, dumb)
593
+ unless assoc
594
+ OpenID::Util.log("failed to get assoc with handle #{assoc_handle} to verify sig #{sig}")
595
+ return false
596
+ end
597
+
598
+ expected_sig = OpenID::Util.to_base64(assoc.sign(signed_pairs))
599
+
600
+ if sig == expected_sig
601
+ return true
602
+ else
603
+ OpenID::Util.log("signture mismatch: expected #{expected_sig}, got #{sig}")
604
+ return false
605
+ end
606
+ end
607
+
608
+ # Sign a response, creating a signature based on everything in it's
609
+ # +signed+ array. Returns a new response object with sig and signed
610
+ # values in its field set.
611
+ def sign(response)
612
+ # get a deep copy of the response
613
+ signed_response = Marshal.load(Marshal.dump(response))
614
+ assoc_handle = response.request.assoc_handle
615
+
616
+ if assoc_handle
617
+ assoc = self.get_association(assoc_handle, false)
618
+ unless assoc
619
+ # no assoc for handle, fall back to dumb mode
620
+ signed_response.fields['invalidate_handle'] = assoc_handle
621
+ assoc = self.create_association(true)
622
+ end
623
+ else
624
+ # dumb mode
625
+ assoc = self.create_association(true)
626
+ end
627
+
628
+ signed_response.fields['assoc_handle'] = assoc.handle
629
+ assoc.add_signature(signed_response.signed,
630
+ signed_response.fields, '')
631
+ return signed_response
632
+ end
633
+
634
+ # Make a new assocation.
635
+ def create_association(dumb=true, assoc_type='HMAC-SHA1')
636
+ secret = OpenID::Util.get_random_bytes(20)
637
+ uniq = OpenID::Util.to_base64(OpenID::Util.get_random_bytes(4))
638
+ handle = "{%s}{%x}{%s}" % [assoc_type, Time.now.to_i, uniq]
639
+ assoc = Association.from_expires_in(@@secret_lifetime,
640
+ handle,
641
+ secret,
642
+ assoc_type)
643
+
644
+ key = dumb ? @@dumb_key : @@normal_key
645
+ @store.store_association(key, assoc)
646
+ return assoc
647
+ end
648
+
649
+ # Get an association by assoc_handle and mode
650
+ def get_association(assoc_handle, dumb)
651
+ if assoc_handle.nil?
652
+ raise ArgumentError, 'assoc_handle must not be nil'
653
+ end
654
+
655
+ key = dumb ? @@dumb_key : @@normal_key
656
+
657
+ assoc = @store.get_association(key, assoc_handle)
658
+ if assoc and assoc.expired?
659
+ @store.remove_association(key, assoc_handle)
660
+ assoc = nil
661
+ end
662
+
663
+ return assoc
664
+ end
665
+
666
+ # Invalidate an association by assoc_handle and mode
667
+ def invalidate(assoc_handle, dumb)
668
+ key = dumb ? @@dumb_key : @@normal_key
669
+ @store.remove_association(key, assoc_handle)
670
+ end
671
+
672
+ end
673
+
674
+ # Response to an OpenID request that a web server can understand. Proper
675
+ # responses can be issued to your framework by examining the +code+
676
+ # method and associated methods.
677
+ class WebResponse
678
+
679
+ attr_accessor :code, :headers, :body
680
+
681
+ def initialize
682
+ @code = HTTP_OK
683
+ @headers = {}
684
+ @body = ''
685
+ end
686
+
687
+ def set_redirect(url)
688
+ @code = HTTP_REDIRECT
689
+ @headers['location'] = url
690
+ end
691
+
692
+ # If this method returns true, a redirect should be issued by the
693
+ # webserver to the value returned by the redirect_url method.
694
+ #
695
+ # If this method returns false, the value returned by the +body+
696
+ # method should be returned to ther server as-is without modification.
697
+ def is_redirect?
698
+ @code == HTTP_REDIRECT
699
+ end
700
+
701
+ def redirect_url
702
+ @headers['location']
703
+ end
704
+
705
+ end
706
+
707
+ # Object that encodes OpenIDResponse objects into WebResponse objects.
708
+ class Encoder
709
+
710
+ def encode(response)
711
+ webresponse = WebResponse.new
712
+
713
+ case response.which_encoding?
714
+ when ENCODE_KVFORM
715
+ webresponse.code = HTTP_ERROR if response.kind_of?(Exception)
716
+ webresponse.body = response.encode_to_kvform
717
+
718
+ when ENCODE_URL
719
+ webresponse.set_redirect(response.encode_to_url)
720
+
721
+ else
722
+ # don't know how to encode response
723
+ raise EncodingError.new(response)
724
+ end
725
+
726
+ return webresponse
727
+ end
728
+
729
+ end
730
+
731
+ # Object that encodes OpenIDResponse objects into WebResponse objects,
732
+ # potentially adding a signature along the way.
733
+ class SigningEncoder < Encoder
734
+
735
+ def initialize(signatory)
736
+ if signatory.nil?
737
+ raise ArgumentError, "signatory must not be nil"
738
+ end
739
+ @signatory = signatory
740
+ end
741
+
742
+ def encode(response)
743
+ if (not response.kind_of?(Exception)) and response.needs_signing?
744
+ if response.fields.has_key?('sig')
745
+ raise AlreadySigned
746
+ end
747
+ response = @signatory.sign(response)
748
+ end
749
+ return super(response)
750
+ end
751
+
752
+ end
753
+
754
+ # Decode incoming web requests into an OpenIDRequest object.
755
+ class Decoder
756
+
757
+ def decode(query)
758
+ return nil if query.length == 0
759
+
760
+ mode = query['openid.mode']
761
+ return nil if mode.nil?
762
+
763
+ if mode.class == Array
764
+ raise ArgumentError, 'query hash must have one value for each key'
765
+ end
766
+
767
+ case mode
768
+ when 'checkid_setup', 'checkid_immediate'
769
+ return CheckIDRequest.from_query(query)
770
+
771
+ when 'check_authentication'
772
+ return CheckAuthRequest.from_query(query)
773
+
774
+ when 'associate'
775
+ return AssociateRequest.from_query(query)
776
+
777
+ else
778
+ raise ProtocolError.new(query, "Unknown mode #{mode}")
779
+ end
780
+
781
+ end
782
+
783
+ end
784
+
785
+ # Top level object that handles incoming requests for an OpenID server.
786
+ #
787
+ # Some types of requests (those which are not CheckIDRequest objects) may
788
+ # be handed to the handle_request method, and an appropriate response
789
+ # will be returned.
790
+ #
791
+ # For convenienve, decode and encode methods are exposed which should be
792
+ # used as the entry and exit points of the OpenID server logic. The first
793
+ # step when handling an OpenID server action should be to call
794
+ # Server.decode_request with the query arguments.
795
+ #
796
+ # This object needs an instance of OpenID::Store to store state between
797
+ # sessions and associations. See OpenID::FilesystemStore for a simple
798
+ # file based solution.
799
+ #
800
+ # ==Pseudo Code
801
+ #
802
+ # Below is some pseudo code for using this object to handle OpenID server
803
+ # requests. The +params+ variable represents a Hash of the incoming
804
+ # arguments. is_authorized and show_decide_page are methods you provide.
805
+ # At the end you have a WebResponse object suitable for examining and
806
+ # issuing a response to your web server.
807
+ #
808
+ # include OpenID
809
+ # store = FilesystemStore.new('/var/openid/store')
810
+ # server = Server::Server.new(store)
811
+ # request = server.decode_request(params)
812
+ # if request.kind_of?(CheckIDRequest)
813
+ # if is_authorized(request.identity, request.trust_root)
814
+ # response = request.answer(true)
815
+ # elsif request.immediate
816
+ # response = request.answer(false,'http://example.com/openid-server')
817
+ # else
818
+ # show_decide_page(request)
819
+ # return
820
+ # end
821
+ # else
822
+ # response = server.handle_request(request)
823
+ # end
824
+ #
825
+ # web_response = server.encode_response(response)
826
+ #
827
+ # For an actual working example, please see the rails_server
828
+ # directory inside of the examples directory. Have a look at the
829
+ # app/controllers/server_controller.rb and the +index+ method of the
830
+ # ServerController object.
831
+ class Server
832
+
833
+ # +store+ is a kind of OpenID::Store
834
+ def initialize(store)
835
+ @store = store
836
+ @signatory = Signatory.new(store)
837
+ @encoder = SigningEncoder.new(@signatory)
838
+ @decoder = Decoder.new
839
+ end
840
+
841
+ # Decode an incoming web request into a kind of OpenIDRequest object.
842
+ # +query+ should be a hash of request arguments. Rails users will want to
843
+ # pass in the @params instance variable of the ActionController.
844
+ def decode_request(query)
845
+ return @decoder.decode(query)
846
+ end
847
+
848
+ # Handle all non checkid_* OpenID requests.
849
+ def handle_request(request)
850
+ return self.send('openid_'+request.mode, request)
851
+ end
852
+
853
+ # Return a WebResponse object given an OpenIDResponse object
854
+ def encode_response(response)
855
+ return @encoder.encode(response)
856
+ end
857
+
858
+ # called by handle_request to perform check auth calls
859
+ def openid_check_authentication(request)
860
+ return request.answer(@signatory)
861
+ end
862
+
863
+ # called by handle_request to perform openid.mode=associate calls.
864
+ def openid_associate(request)
865
+ assoc = @signatory.create_association(false)
866
+ return request.answer(assoc)
867
+ end
868
+
869
+ end
870
+
871
+ # Raised when an OpenID request encounters some kind of protocol error.
872
+ class ProtocolError < Exception
873
+
874
+ attr_reader :query
875
+
876
+ def initialize(query, text=nil)
877
+ super(text)
878
+ @query = query
879
+ end
880
+
881
+ def has_return_to?
882
+ @query.has_key?('openid.return_to')
883
+ end
884
+
885
+ def encode_to_url
886
+ return_to = @query['openid.return_to']
887
+ unless return_to
888
+ raise ArgumentError, 'No return_to in query'
889
+ end
890
+
891
+ args = {
892
+ 'openid.mode' => 'error',
893
+ 'openid.error' => self.to_s
894
+ }
895
+
896
+ return OpenID::Util.append_args(return_to, args)
897
+ end
898
+
899
+ def encode_to_kvform
900
+ args = {
901
+ 'mode' => 'error',
902
+ 'error' => self.to_s
903
+ }
904
+ return OpenID::Util.kvform(args)
905
+ end
906
+
907
+ def which_encoding?
908
+ if self.has_return_to?
909
+ return ENCODE_URL
910
+ end
911
+
912
+ mode = @query['openid.mode']
913
+ if mode and not BROWSER_REQUEST_MODES.member?(mode)
914
+ return ENCODE_KVFORM
915
+ end
916
+
917
+ return nil
918
+ end
919
+
920
+ end
921
+
922
+ class EncodingError < Exception; end
923
+ class AlreadySigned < EncodingError; end
924
+
925
+ class MalformedReturnURL < ProtocolError
926
+
927
+ attr_reader :return_to
928
+
929
+ def initialize(query, return_to)
930
+ super(query)
931
+ @return_to = return_to
932
+ end
933
+
934
+ end
935
+
936
+ class MalformedTrustRoot < ProtocolError; end
937
+
938
+ class UntrustedReturnURL < ProtocolError
939
+
940
+ attr_reader :return_to, :trust_root
941
+
942
+ def initialize(query, return_to, trust_root)
943
+ super(query)
944
+ @return_to = return_to
945
+ @trust_root = trust_root
946
+ end
947
+ end
948
+
949
+ end
950
+
951
+ end