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.
- data/COPYING +21 -0
- data/INSTALL +34 -0
- data/README +67 -0
- data/TODO +9 -0
- data/examples/README +54 -0
- data/examples/cacert.pem +7815 -0
- data/examples/consumer.rb +285 -0
- data/examples/openid-store/associations/http-localhost_3A3000_2Fserver-EMQbAy3NnHVzA.s0u5KAcplKGzo +6 -0
- data/examples/openid-store/auth_key +1 -0
- data/examples/rails_active_record_store/README +59 -0
- data/examples/rails_active_record_store/XX_add_openidstore.rb +30 -0
- data/examples/rails_active_record_store/models/openid_association.rb +12 -0
- data/examples/rails_active_record_store/models/openid_nonce.rb +3 -0
- data/examples/rails_active_record_store/models/openid_setting.rb +2 -0
- data/examples/rails_active_record_store/openid_helper.rb +91 -0
- data/examples/rails_active_record_store/openidstore_test.rb +15 -0
- data/examples/rails_active_record_store/schema.mysql.sql +22 -0
- data/examples/rails_active_record_store/schema.postgresql.sql +21 -0
- data/examples/rails_active_record_store/schema.sqlite.sql +21 -0
- data/examples/rails_openid_login_generator/USAGE +23 -0
- data/examples/rails_openid_login_generator/openid_login_generator.rb +36 -0
- data/examples/rails_openid_login_generator/templates/README +116 -0
- data/examples/rails_openid_login_generator/templates/controller.rb +116 -0
- data/examples/rails_openid_login_generator/templates/controller_test.rb +0 -0
- data/examples/rails_openid_login_generator/templates/helper.rb +2 -0
- data/examples/rails_openid_login_generator/templates/openid_login_system.rb +87 -0
- data/examples/rails_openid_login_generator/templates/user.rb +14 -0
- data/examples/rails_openid_login_generator/templates/user_test.rb +0 -0
- data/examples/rails_openid_login_generator/templates/users.yml +0 -0
- data/examples/rails_openid_login_generator/templates/view_login.rhtml +15 -0
- data/examples/rails_openid_login_generator/templates/view_logout.rhtml +10 -0
- data/examples/rails_openid_login_generator/templates/view_welcome.rhtml +9 -0
- data/examples/rails_server/README +153 -0
- data/examples/rails_server/Rakefile +10 -0
- data/examples/rails_server/app/controllers/application.rb +4 -0
- data/examples/rails_server/app/controllers/login_controller.rb +35 -0
- data/examples/rails_server/app/controllers/server_controller.rb +185 -0
- data/examples/rails_server/app/helpers/application_helper.rb +3 -0
- data/examples/rails_server/app/helpers/login_helper.rb +2 -0
- data/examples/rails_server/app/helpers/server_helper.rb +9 -0
- data/examples/rails_server/app/views/layouts/server.rhtml +61 -0
- data/examples/rails_server/app/views/login/index.rhtml +32 -0
- data/examples/rails_server/app/views/server/decide.rhtml +11 -0
- data/examples/rails_server/config/boot.rb +19 -0
- data/examples/rails_server/config/database.yml +85 -0
- data/examples/rails_server/config/environment.rb +53 -0
- data/examples/rails_server/config/environments/development.rb +19 -0
- data/examples/rails_server/config/environments/production.rb +19 -0
- data/examples/rails_server/config/environments/test.rb +19 -0
- data/examples/rails_server/config/routes.rb +23 -0
- data/examples/rails_server/db/openid-store/associations/http-localhost_2F_7Cnormal-YU.tkND1J4fEZhnuAoT5Zc0yCA0 +6 -0
- data/examples/rails_server/doc/README_FOR_APP +2 -0
- data/examples/rails_server/log/development.log +6059 -0
- data/examples/rails_server/log/production.log +0 -0
- data/examples/rails_server/log/server.log +0 -0
- data/examples/rails_server/log/test.log +0 -0
- data/examples/rails_server/public/404.html +8 -0
- data/examples/rails_server/public/500.html +8 -0
- data/examples/rails_server/public/dispatch.cgi +12 -0
- data/examples/rails_server/public/dispatch.fcgi +26 -0
- data/examples/rails_server/public/dispatch.rb +12 -0
- data/examples/rails_server/public/favicon.ico +0 -0
- data/examples/rails_server/public/images/rails.png +0 -0
- data/examples/rails_server/public/javascripts/controls.js +750 -0
- data/examples/rails_server/public/javascripts/dragdrop.js +584 -0
- data/examples/rails_server/public/javascripts/effects.js +854 -0
- data/examples/rails_server/public/javascripts/prototype.js +1785 -0
- data/examples/rails_server/public/robots.txt +1 -0
- data/examples/rails_server/script/about +3 -0
- data/examples/rails_server/script/breakpointer +3 -0
- data/examples/rails_server/script/console +3 -0
- data/examples/rails_server/script/destroy +3 -0
- data/examples/rails_server/script/generate +3 -0
- data/examples/rails_server/script/performance/benchmarker +3 -0
- data/examples/rails_server/script/performance/profiler +3 -0
- data/examples/rails_server/script/plugin +3 -0
- data/examples/rails_server/script/process/reaper +3 -0
- data/examples/rails_server/script/process/spawner +3 -0
- data/examples/rails_server/script/process/spinner +3 -0
- data/examples/rails_server/script/runner +3 -0
- data/examples/rails_server/script/server +3 -0
- data/examples/rails_server/test/functional/login_controller_test.rb +18 -0
- data/examples/rails_server/test/functional/server_controller_test.rb +18 -0
- data/examples/rails_server/test/test_helper.rb +28 -0
- data/lib/hmac-md5.rb +11 -0
- data/lib/hmac-rmd160.rb +11 -0
- data/lib/hmac-sha1.rb +11 -0
- data/lib/hmac-sha2.rb +25 -0
- data/lib/hmac.rb +112 -0
- data/lib/openid/association.rb +109 -0
- data/lib/openid/consumer.rb +928 -0
- data/lib/openid/dh.rb +48 -0
- data/lib/openid/discovery.rb +89 -0
- data/lib/openid/fetchers.rb +119 -0
- data/lib/openid/filestore.rb +315 -0
- data/lib/openid/htmltokenizer.rb +355 -0
- data/lib/openid/parse.rb +23 -0
- data/lib/openid/server.rb +951 -0
- data/lib/openid/service.rb +135 -0
- data/lib/openid/stores.rb +178 -0
- data/lib/openid/trustroot.rb +100 -0
- data/lib/openid/util.rb +273 -0
- data/test/assoc.rb +38 -0
- data/test/consumer.rb +384 -0
- data/test/dh.rb +20 -0
- data/test/extensions.rb +30 -0
- data/test/linkparse.rb +305 -0
- data/test/runtests.rb +11 -0
- data/test/server2.rb +1053 -0
- data/test/storetestcase.rb +172 -0
- data/test/teststore.rb +23 -0
- data/test/trustroot.rb +113 -0
- data/test/util.rb +56 -0
- 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
|
data/lib/openid/parse.rb
ADDED
@@ -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
|