ruby-openid 1.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of ruby-openid might be problematic. Click here for more details.

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