ruby-openid2 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +136 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/CONTRIBUTING.md +54 -0
- data/LICENSE.txt +210 -0
- data/README.md +81 -0
- data/SECURITY.md +15 -0
- data/lib/hmac/hmac.rb +110 -0
- data/lib/hmac/sha1.rb +11 -0
- data/lib/hmac/sha2.rb +25 -0
- data/lib/openid/association.rb +246 -0
- data/lib/openid/consumer/associationmanager.rb +354 -0
- data/lib/openid/consumer/checkid_request.rb +179 -0
- data/lib/openid/consumer/discovery.rb +516 -0
- data/lib/openid/consumer/discovery_manager.rb +144 -0
- data/lib/openid/consumer/html_parse.rb +142 -0
- data/lib/openid/consumer/idres.rb +513 -0
- data/lib/openid/consumer/responses.rb +147 -0
- data/lib/openid/consumer/session.rb +36 -0
- data/lib/openid/consumer.rb +406 -0
- data/lib/openid/cryptutil.rb +112 -0
- data/lib/openid/dh.rb +84 -0
- data/lib/openid/extension.rb +38 -0
- data/lib/openid/extensions/ax.rb +552 -0
- data/lib/openid/extensions/oauth.rb +88 -0
- data/lib/openid/extensions/pape.rb +170 -0
- data/lib/openid/extensions/sreg.rb +268 -0
- data/lib/openid/extensions/ui.rb +49 -0
- data/lib/openid/fetchers.rb +277 -0
- data/lib/openid/kvform.rb +113 -0
- data/lib/openid/kvpost.rb +62 -0
- data/lib/openid/message.rb +555 -0
- data/lib/openid/protocolerror.rb +7 -0
- data/lib/openid/server.rb +1571 -0
- data/lib/openid/store/filesystem.rb +260 -0
- data/lib/openid/store/interface.rb +73 -0
- data/lib/openid/store/memcache.rb +109 -0
- data/lib/openid/store/memory.rb +79 -0
- data/lib/openid/store/nonce.rb +72 -0
- data/lib/openid/trustroot.rb +597 -0
- data/lib/openid/urinorm.rb +72 -0
- data/lib/openid/util.rb +119 -0
- data/lib/openid/version.rb +5 -0
- data/lib/openid/yadis/accept.rb +141 -0
- data/lib/openid/yadis/constants.rb +16 -0
- data/lib/openid/yadis/discovery.rb +151 -0
- data/lib/openid/yadis/filters.rb +192 -0
- data/lib/openid/yadis/htmltokenizer.rb +290 -0
- data/lib/openid/yadis/parsehtml.rb +50 -0
- data/lib/openid/yadis/services.rb +44 -0
- data/lib/openid/yadis/xrds.rb +160 -0
- data/lib/openid/yadis/xri.rb +86 -0
- data/lib/openid/yadis/xrires.rb +87 -0
- data/lib/openid.rb +27 -0
- data/lib/ruby-openid.rb +1 -0
- data.tar.gz.sig +0 -0
- metadata +331 -0
- metadata.gz.sig +0 -0
@@ -0,0 +1,142 @@
|
|
1
|
+
require_relative "../yadis/htmltokenizer"
|
2
|
+
|
3
|
+
module OpenID
|
4
|
+
# Stuff to remove before we start looking for tags
|
5
|
+
REMOVED_RE = %r{
|
6
|
+
# Comments
|
7
|
+
<!--.*?-->
|
8
|
+
|
9
|
+
# CDATA blocks
|
10
|
+
| <!\[CDATA\[.*?\]\]>
|
11
|
+
|
12
|
+
# script blocks
|
13
|
+
| <script\b
|
14
|
+
|
15
|
+
# make sure script is not an XML namespace
|
16
|
+
(?!:)
|
17
|
+
|
18
|
+
[^>]*>.*?</script>
|
19
|
+
|
20
|
+
}mix
|
21
|
+
|
22
|
+
def self.openid_unescape(s)
|
23
|
+
s.gsub("&", "&").gsub("<", "<").gsub(">", ">").gsub(""", '"')
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.unescape_hash(h)
|
27
|
+
newh = {}
|
28
|
+
h.map do |k, v|
|
29
|
+
newh[k] = openid_unescape(v)
|
30
|
+
end
|
31
|
+
newh
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.parse_link_attrs(html)
|
35
|
+
begin
|
36
|
+
stripped = html.gsub(REMOVED_RE, "")
|
37
|
+
rescue ArgumentError
|
38
|
+
begin
|
39
|
+
stripped = html.encode("UTF-8", "binary", invalid: :replace, undef: :replace, replace: "").gsub(
|
40
|
+
REMOVED_RE, ""
|
41
|
+
)
|
42
|
+
rescue Encoding::UndefinedConversionError, Encoding::ConverterNotFoundError
|
43
|
+
# needed for a problem in JRuby where it can't handle the conversion.
|
44
|
+
# see details here: https://github.com/jruby/jruby/issues/829
|
45
|
+
stripped = html.encode("UTF-8", "ASCII", invalid: :replace, undef: :replace, replace: "").gsub(
|
46
|
+
REMOVED_RE, ""
|
47
|
+
)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
parser = HTMLTokenizer.new(stripped)
|
51
|
+
|
52
|
+
links = []
|
53
|
+
# to keep track of whether or not we are in the head element
|
54
|
+
in_head = false
|
55
|
+
in_html = false
|
56
|
+
saw_head = false
|
57
|
+
|
58
|
+
begin
|
59
|
+
while el = parser.getTag(
|
60
|
+
"head",
|
61
|
+
"/head",
|
62
|
+
"link",
|
63
|
+
"body",
|
64
|
+
"/body",
|
65
|
+
"html",
|
66
|
+
"/html",
|
67
|
+
)
|
68
|
+
|
69
|
+
# we are leaving head or have reached body, so we bail
|
70
|
+
return links if ["/head", "body", "/body", "/html"].member?(el.tag_name)
|
71
|
+
|
72
|
+
# enforce html > head > link
|
73
|
+
in_html = true if el.tag_name == "html"
|
74
|
+
next unless in_html
|
75
|
+
|
76
|
+
if el.tag_name == "head"
|
77
|
+
if saw_head
|
78
|
+
return links # only allow one head
|
79
|
+
end
|
80
|
+
|
81
|
+
saw_head = true
|
82
|
+
in_head = true unless el.to_s[-2] == 47 # tag ends with a /: a short tag
|
83
|
+
end
|
84
|
+
next unless in_head
|
85
|
+
|
86
|
+
return links if el.tag_name == "html"
|
87
|
+
|
88
|
+
links << unescape_hash(el.attr_hash) if el.tag_name == "link"
|
89
|
+
|
90
|
+
end
|
91
|
+
rescue Exception # just stop parsing if there's an error
|
92
|
+
end
|
93
|
+
links
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.rel_matches(rel_attr, target_rel)
|
97
|
+
# Does this target_rel appear in the rel_str?
|
98
|
+
# XXX: TESTME
|
99
|
+
rels = rel_attr.strip.split
|
100
|
+
rels.each do |rel|
|
101
|
+
rel = rel.downcase
|
102
|
+
return true if rel == target_rel
|
103
|
+
end
|
104
|
+
|
105
|
+
false
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.link_has_rel(link_attrs, target_rel)
|
109
|
+
# Does this link have target_rel as a relationship?
|
110
|
+
|
111
|
+
# XXX: TESTME
|
112
|
+
rel_attr = link_attrs["rel"]
|
113
|
+
(rel_attr and rel_matches(rel_attr, target_rel))
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.find_links_rel(link_attrs_list, target_rel)
|
117
|
+
# Filter the list of link attributes on whether it has target_rel
|
118
|
+
# as a relationship.
|
119
|
+
|
120
|
+
# XXX: TESTME
|
121
|
+
matches_target = ->(attrs) { link_has_rel(attrs, target_rel) }
|
122
|
+
result = []
|
123
|
+
|
124
|
+
link_attrs_list.each do |item|
|
125
|
+
result << item if matches_target.call(item)
|
126
|
+
end
|
127
|
+
|
128
|
+
result
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.find_first_href(link_attrs_list, target_rel)
|
132
|
+
# Return the value of the href attribute for the first link tag in
|
133
|
+
# the list that has target_rel as a relationship.
|
134
|
+
|
135
|
+
# XXX: TESTME
|
136
|
+
matches = find_links_rel(link_attrs_list, target_rel)
|
137
|
+
return if !matches or matches.empty?
|
138
|
+
|
139
|
+
first = matches[0]
|
140
|
+
first["href"]
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,513 @@
|
|
1
|
+
require_relative "../message"
|
2
|
+
require_relative "../protocolerror"
|
3
|
+
require_relative "../kvpost"
|
4
|
+
require_relative "../urinorm"
|
5
|
+
require_relative "discovery"
|
6
|
+
|
7
|
+
module OpenID
|
8
|
+
class TypeURIMismatch < ProtocolError
|
9
|
+
attr_reader :type_uri, :endpoint
|
10
|
+
|
11
|
+
def initialize(type_uri, endpoint)
|
12
|
+
@type_uri = type_uri
|
13
|
+
@endpoint = endpoint
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class Consumer
|
18
|
+
@openid1_return_to_nonce_name = "rp_nonce"
|
19
|
+
@openid1_return_to_claimed_id_name = "openid1_claimed_id"
|
20
|
+
|
21
|
+
# Set the name of the query parameter that this library will use
|
22
|
+
# to thread a nonce through an OpenID 1 transaction. It will be
|
23
|
+
# appended to the return_to URL.
|
24
|
+
class << self
|
25
|
+
attr_accessor :openid1_return_to_nonce_name
|
26
|
+
end
|
27
|
+
|
28
|
+
# Set the name of the query parameter that this library will use
|
29
|
+
# to thread the requested URL through an OpenID 1 transaction (for
|
30
|
+
# use when verifying discovered information). It will be appended
|
31
|
+
# to the return_to URL.
|
32
|
+
class << self
|
33
|
+
attr_accessor :openid1_return_to_claimed_id_name
|
34
|
+
end
|
35
|
+
|
36
|
+
# Handles an openid.mode=id_res response. This object is
|
37
|
+
# instantiated and used by the Consumer.
|
38
|
+
class IdResHandler
|
39
|
+
attr_reader :endpoint, :message
|
40
|
+
|
41
|
+
def initialize(message, current_url, store = nil, endpoint = nil)
|
42
|
+
@store = store # Fer the nonce and invalidate_handle
|
43
|
+
@message = message
|
44
|
+
@endpoint = endpoint
|
45
|
+
@current_url = current_url
|
46
|
+
@signed_list = nil
|
47
|
+
|
48
|
+
# Start the verification process
|
49
|
+
id_res
|
50
|
+
end
|
51
|
+
|
52
|
+
def signed_fields
|
53
|
+
signed_list.map { |x| "openid." + x }
|
54
|
+
end
|
55
|
+
|
56
|
+
protected
|
57
|
+
|
58
|
+
# This method will raise ProtocolError unless the request is a
|
59
|
+
# valid id_res response. Once it has been verified, the methods
|
60
|
+
# 'endpoint', 'message', and 'signed_fields' contain the
|
61
|
+
# verified information.
|
62
|
+
def id_res
|
63
|
+
check_for_fields
|
64
|
+
check_signature
|
65
|
+
check_nonce
|
66
|
+
verify_return_to
|
67
|
+
verify_discovery_results
|
68
|
+
end
|
69
|
+
|
70
|
+
def server_url
|
71
|
+
@endpoint.nil? ? nil : @endpoint.server_url
|
72
|
+
end
|
73
|
+
|
74
|
+
def openid_namespace
|
75
|
+
@message.get_openid_namespace
|
76
|
+
end
|
77
|
+
|
78
|
+
def fetch(field, default = NO_DEFAULT)
|
79
|
+
@message.get_arg(OPENID_NS, field, default)
|
80
|
+
end
|
81
|
+
|
82
|
+
def signed_list
|
83
|
+
if @signed_list.nil?
|
84
|
+
signed_list_str = fetch("signed", nil)
|
85
|
+
raise ProtocolError, "Response missing signed list" if signed_list_str.nil?
|
86
|
+
|
87
|
+
@signed_list = signed_list_str.split(",", -1)
|
88
|
+
end
|
89
|
+
@signed_list
|
90
|
+
end
|
91
|
+
|
92
|
+
def check_for_fields
|
93
|
+
# XXX: if a field is missing, we should not have to explicitly
|
94
|
+
# check that it's present, just make sure that the fields are
|
95
|
+
# actually being used by the rest of the code in
|
96
|
+
# tests. Although, which fields are signed does need to be
|
97
|
+
# checked somewhere.
|
98
|
+
basic_fields = %w[return_to assoc_handle sig signed]
|
99
|
+
basic_sig_fields = %w[return_to identity]
|
100
|
+
|
101
|
+
case openid_namespace
|
102
|
+
when OPENID2_NS
|
103
|
+
require_fields = basic_fields + ["op_endpoint"]
|
104
|
+
require_sigs = basic_sig_fields +
|
105
|
+
%w[response_nonce claimed_id assoc_handle op_endpoint]
|
106
|
+
when OPENID1_NS, OPENID11_NS
|
107
|
+
require_fields = basic_fields + ["identity"]
|
108
|
+
require_sigs = basic_sig_fields
|
109
|
+
else
|
110
|
+
raise "check_for_fields doesn't know about " \
|
111
|
+
"namespace #{openid_namespace.inspect}"
|
112
|
+
end
|
113
|
+
|
114
|
+
require_fields.each do |field|
|
115
|
+
raise ProtocolError, "Missing required field #{field}" unless @message.has_key?(OPENID_NS, field)
|
116
|
+
end
|
117
|
+
|
118
|
+
require_sigs.each do |field|
|
119
|
+
# Field is present and not in signed list
|
120
|
+
if @message.has_key?(OPENID_NS, field) && !signed_list.member?(field)
|
121
|
+
raise ProtocolError, "#{field.inspect} not signed"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def verify_return_to
|
127
|
+
begin
|
128
|
+
msg_return_to = URI.parse(URINorm.urinorm(fetch("return_to")))
|
129
|
+
rescue URI::InvalidURIError
|
130
|
+
raise ProtocolError, ("return_to is not a valid URI")
|
131
|
+
end
|
132
|
+
|
133
|
+
verify_return_to_args(msg_return_to)
|
134
|
+
return if @current_url.nil?
|
135
|
+
|
136
|
+
verify_return_to_base(msg_return_to)
|
137
|
+
end
|
138
|
+
|
139
|
+
def verify_return_to_args(msg_return_to)
|
140
|
+
return_to_parsed_query = {}
|
141
|
+
unless msg_return_to.query.nil?
|
142
|
+
CGI.parse(msg_return_to.query).each_pair do |k, vs|
|
143
|
+
return_to_parsed_query[k] = vs[0]
|
144
|
+
end
|
145
|
+
end
|
146
|
+
query = @message.to_post_args
|
147
|
+
return_to_parsed_query.each_pair do |rt_key, rt_val|
|
148
|
+
msg_val = query[rt_key]
|
149
|
+
if msg_val.nil? && !rt_val.nil?
|
150
|
+
raise ProtocolError, "Message missing return_to argument '#{rt_key}'"
|
151
|
+
elsif msg_val != rt_val
|
152
|
+
raise ProtocolError, "Parameter '#{rt_key}' value " \
|
153
|
+
"#{msg_val.inspect} does not match " \
|
154
|
+
"return_to's value #{rt_val.inspect}"
|
155
|
+
end
|
156
|
+
end
|
157
|
+
@message.get_args(BARE_NS).each_pair do |bare_key, bare_val|
|
158
|
+
rt_val = return_to_parsed_query[bare_key]
|
159
|
+
unless return_to_parsed_query.has_key?(bare_key)
|
160
|
+
# This may be caused by your web framework throwing extra
|
161
|
+
# entries in to your parameters hash that were not GET or
|
162
|
+
# POST parameters. For example, Rails has been known to
|
163
|
+
# add "controller" and "action" keys; another server adds
|
164
|
+
# at least a "format" key.
|
165
|
+
raise ProtocolError, "Unexpected parameter (not on return_to): " \
|
166
|
+
"'#{bare_key}'=#{rt_val.inspect})"
|
167
|
+
end
|
168
|
+
next unless rt_val != bare_val
|
169
|
+
|
170
|
+
raise ProtocolError, "Parameter '#{bare_key}' value " \
|
171
|
+
"#{bare_val.inspect} does not match " \
|
172
|
+
"return_to's value #{rt_val.inspect}"
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def verify_return_to_base(msg_return_to)
|
177
|
+
begin
|
178
|
+
app_parsed = URI.parse(URINorm.urinorm(@current_url))
|
179
|
+
rescue URI::InvalidURIError
|
180
|
+
raise ProtocolError, "current_url is not a valid URI: #{@current_url}"
|
181
|
+
end
|
182
|
+
|
183
|
+
%i[scheme host port path].each do |meth|
|
184
|
+
raise ProtocolError, "return_to #{meth} does not match" if msg_return_to.send(meth) != app_parsed.send(meth)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# Raises ProtocolError if the signature is bad
|
189
|
+
def check_signature
|
190
|
+
# ----------------------------------------------------------------------
|
191
|
+
# The server url must be defined within the endpoint instance for the
|
192
|
+
# OpenID2 namespace in order for the signature check to complete
|
193
|
+
# successfully.
|
194
|
+
#
|
195
|
+
# This fix corrects issue #125 - Unable to complete OpenID login
|
196
|
+
# with ruby-openid 2.9.0/2.9.1
|
197
|
+
# ---------------------------------------------------------------------
|
198
|
+
set_endpoint_flag = false
|
199
|
+
if @endpoint.nil? && openid_namespace == OPENID2_NS
|
200
|
+
@endpoint = OpenIDServiceEndpoint.new
|
201
|
+
@endpoint.server_url = fetch("op_endpoint")
|
202
|
+
set_endpoint_flag = true
|
203
|
+
end
|
204
|
+
|
205
|
+
assoc = if @store.nil?
|
206
|
+
nil
|
207
|
+
else
|
208
|
+
@store.get_association(server_url, fetch("assoc_handle"))
|
209
|
+
end
|
210
|
+
|
211
|
+
if assoc.nil?
|
212
|
+
check_auth
|
213
|
+
elsif assoc.expires_in <= 0
|
214
|
+
raise ProtocolError, "Association with #{server_url} expired"
|
215
|
+
# XXX: It might be a good idea sometimes to re-start the
|
216
|
+
# authentication with a new association. Doing it
|
217
|
+
# automatically opens the possibility for
|
218
|
+
# denial-of-service by a server that just returns expired
|
219
|
+
# associations (or really short-lived associations)
|
220
|
+
elsif !assoc.check_message_signature(@message)
|
221
|
+
raise ProtocolError, "Bad signature in response from #{server_url}"
|
222
|
+
end
|
223
|
+
@endpoint = nil if set_endpoint_flag # Clear endpoint if we defined it.
|
224
|
+
end
|
225
|
+
|
226
|
+
def check_auth
|
227
|
+
Util.log("Using 'check_authentication' with #{server_url}")
|
228
|
+
begin
|
229
|
+
request = create_check_auth_request
|
230
|
+
rescue Message::KeyNotFound => e
|
231
|
+
raise ProtocolError, "Could not generate 'check_authentication' " \
|
232
|
+
"request: #{e.message}"
|
233
|
+
end
|
234
|
+
|
235
|
+
response = OpenID.make_kv_post(request, server_url)
|
236
|
+
|
237
|
+
process_check_auth_response(response)
|
238
|
+
end
|
239
|
+
|
240
|
+
def create_check_auth_request
|
241
|
+
signed_list = @message.get_arg(OPENID_NS, "signed", NO_DEFAULT).split(",")
|
242
|
+
|
243
|
+
# check that we got all the signed arguments
|
244
|
+
signed_list.each do |k|
|
245
|
+
@message.get_aliased_arg(k, NO_DEFAULT)
|
246
|
+
end
|
247
|
+
|
248
|
+
ca_message = @message.copy
|
249
|
+
ca_message.set_arg(OPENID_NS, "mode", "check_authentication")
|
250
|
+
|
251
|
+
ca_message
|
252
|
+
end
|
253
|
+
|
254
|
+
# Process the response message from a check_authentication
|
255
|
+
# request, invalidating associations if requested.
|
256
|
+
def process_check_auth_response(response)
|
257
|
+
is_valid = response.get_arg(OPENID_NS, "is_valid", "false")
|
258
|
+
|
259
|
+
invalidate_handle = response.get_arg(OPENID_NS, "invalidate_handle")
|
260
|
+
unless invalidate_handle.nil?
|
261
|
+
Util.log("Received 'invalidate_handle' from server #{server_url}")
|
262
|
+
if @store.nil?
|
263
|
+
Util.log('Unexpectedly got "invalidate_handle" without a store!')
|
264
|
+
else
|
265
|
+
@store.remove_association(server_url, invalidate_handle)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
return unless is_valid != "true"
|
270
|
+
|
271
|
+
raise ProtocolError, "Server #{server_url} responds that the " \
|
272
|
+
"'check_authentication' call is not valid"
|
273
|
+
end
|
274
|
+
|
275
|
+
def check_nonce
|
276
|
+
case openid_namespace
|
277
|
+
when OPENID1_NS, OPENID11_NS
|
278
|
+
nonce =
|
279
|
+
@message.get_arg(BARE_NS, Consumer.openid1_return_to_nonce_name)
|
280
|
+
|
281
|
+
# We generated the nonce, so it uses the empty string as the
|
282
|
+
# server URL
|
283
|
+
server_url = ""
|
284
|
+
when OPENID2_NS
|
285
|
+
nonce = @message.get_arg(OPENID2_NS, "response_nonce")
|
286
|
+
server_url = self.server_url
|
287
|
+
else
|
288
|
+
raise StandardError, "Not reached"
|
289
|
+
end
|
290
|
+
|
291
|
+
raise ProtocolError, "Nonce missing from response" if nonce.nil?
|
292
|
+
|
293
|
+
begin
|
294
|
+
time, extra = Nonce.split_nonce(nonce)
|
295
|
+
rescue ArgumentError
|
296
|
+
raise ProtocolError, "Malformed nonce: #{nonce.inspect}"
|
297
|
+
end
|
298
|
+
|
299
|
+
return unless !@store.nil? && !@store.use_nonce(server_url, time, extra)
|
300
|
+
|
301
|
+
raise ProtocolError, "Nonce already used or out of range: " \
|
302
|
+
"#{nonce.inspect}"
|
303
|
+
end
|
304
|
+
|
305
|
+
def verify_discovery_results
|
306
|
+
case openid_namespace
|
307
|
+
when OPENID1_NS, OPENID11_NS
|
308
|
+
verify_discovery_results_openid1
|
309
|
+
when OPENID2_NS
|
310
|
+
verify_discovery_results_openid2
|
311
|
+
else
|
312
|
+
raise StandardError, "Not reached: #{openid_namespace}"
|
313
|
+
end
|
314
|
+
rescue Message::KeyNotFound => e
|
315
|
+
raise ProtocolError, "Missing required field: #{e.message}"
|
316
|
+
end
|
317
|
+
|
318
|
+
def verify_discovery_results_openid2
|
319
|
+
to_match = OpenIDServiceEndpoint.new
|
320
|
+
to_match.type_uris = [OPENID_2_0_TYPE]
|
321
|
+
to_match.claimed_id = fetch("claimed_id", nil)
|
322
|
+
to_match.local_id = fetch("identity", nil)
|
323
|
+
to_match.server_url = fetch("op_endpoint")
|
324
|
+
|
325
|
+
if to_match.claimed_id.nil? && !to_match.local_id.nil?
|
326
|
+
raise ProtocolError, "openid.identity is present without " \
|
327
|
+
"openid.claimed_id"
|
328
|
+
elsif !to_match.claimed_id.nil? && to_match.local_id.nil?
|
329
|
+
raise ProtocolError, "openid.claimed_id is present without " \
|
330
|
+
"openid.identity"
|
331
|
+
|
332
|
+
# This is a response without identifiers, so there's really no
|
333
|
+
# checking that we can do, so return an endpoint that's for
|
334
|
+
# the specified `openid.op_endpoint'
|
335
|
+
elsif to_match.claimed_id.nil?
|
336
|
+
@endpoint =
|
337
|
+
OpenIDServiceEndpoint.from_op_endpoint_url(to_match.server_url)
|
338
|
+
return
|
339
|
+
end
|
340
|
+
|
341
|
+
if @endpoint.nil?
|
342
|
+
Util.log("No pre-discovered information supplied")
|
343
|
+
discover_and_verify(to_match.claimed_id, [to_match])
|
344
|
+
else
|
345
|
+
begin
|
346
|
+
verify_discovery_single(@endpoint, to_match)
|
347
|
+
rescue ProtocolError => e
|
348
|
+
Util.log("Error attempting to use stored discovery " \
|
349
|
+
"information: #{e.message}")
|
350
|
+
Util.log("Attempting discovery to verify endpoint")
|
351
|
+
discover_and_verify(to_match.claimed_id, [to_match])
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
return unless @endpoint.claimed_id != to_match.claimed_id
|
356
|
+
|
357
|
+
@endpoint = @endpoint.dup
|
358
|
+
@endpoint.claimed_id = to_match.claimed_id
|
359
|
+
end
|
360
|
+
|
361
|
+
def verify_discovery_results_openid1
|
362
|
+
claimed_id =
|
363
|
+
@message.get_arg(BARE_NS, Consumer.openid1_return_to_claimed_id_name)
|
364
|
+
|
365
|
+
if claimed_id.nil?
|
366
|
+
if @endpoint.nil?
|
367
|
+
raise ProtocolError, "When using OpenID 1, the claimed ID must " \
|
368
|
+
"be supplied, either by passing it through " \
|
369
|
+
"as a return_to parameter or by using a " \
|
370
|
+
"session, and supplied to the IdResHandler " \
|
371
|
+
"when it is constructed."
|
372
|
+
else
|
373
|
+
claimed_id = @endpoint.claimed_id
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
to_match = OpenIDServiceEndpoint.new
|
378
|
+
to_match.type_uris = [OPENID_1_1_TYPE]
|
379
|
+
to_match.local_id = fetch("identity")
|
380
|
+
# Restore delegate information from the initiation phase
|
381
|
+
to_match.claimed_id = claimed_id
|
382
|
+
|
383
|
+
to_match_1_0 = to_match.dup
|
384
|
+
to_match_1_0.type_uris = [OPENID_1_0_TYPE]
|
385
|
+
|
386
|
+
unless @endpoint.nil?
|
387
|
+
begin
|
388
|
+
begin
|
389
|
+
verify_discovery_single(@endpoint, to_match)
|
390
|
+
rescue TypeURIMismatch
|
391
|
+
verify_discovery_single(@endpoint, to_match_1_0)
|
392
|
+
end
|
393
|
+
rescue ProtocolError => e
|
394
|
+
Util.log("Error attempting to use stored discovery information: " +
|
395
|
+
e.message)
|
396
|
+
Util.log("Attempting discovery to verify endpoint")
|
397
|
+
else
|
398
|
+
return @endpoint
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
# Either no endpoint was supplied or OpenID 1.x verification
|
403
|
+
# of the information that's in the message failed on that
|
404
|
+
# endpoint.
|
405
|
+
discover_and_verify(to_match.claimed_id, [to_match, to_match_1_0])
|
406
|
+
end
|
407
|
+
|
408
|
+
# Given an endpoint object created from the information in an
|
409
|
+
# OpenID response, perform discovery and verify the discovery
|
410
|
+
# results, returning the matching endpoint that is the result of
|
411
|
+
# doing that discovery.
|
412
|
+
def discover_and_verify(claimed_id, to_match_endpoints)
|
413
|
+
Util.log("Performing discovery on #{claimed_id}")
|
414
|
+
_, services = OpenID.discover(claimed_id)
|
415
|
+
if services.length == 0
|
416
|
+
# XXX: this might want to be something other than
|
417
|
+
# ProtocolError. In Python, it's DiscoveryFailure
|
418
|
+
raise ProtocolError, "No OpenID information found at " \
|
419
|
+
"#{claimed_id}"
|
420
|
+
end
|
421
|
+
verify_discovered_services(claimed_id, services, to_match_endpoints)
|
422
|
+
end
|
423
|
+
|
424
|
+
def verify_discovered_services(claimed_id, services, to_match_endpoints)
|
425
|
+
# Search the services resulting from discovery to find one
|
426
|
+
# that matches the information from the assertion
|
427
|
+
failure_messages = []
|
428
|
+
for endpoint in services
|
429
|
+
for to_match_endpoint in to_match_endpoints
|
430
|
+
begin
|
431
|
+
verify_discovery_single(endpoint, to_match_endpoint)
|
432
|
+
rescue ProtocolError => e
|
433
|
+
failure_messages << e.message
|
434
|
+
else
|
435
|
+
# It matches, so discover verification has
|
436
|
+
# succeeded. Return this endpoint.
|
437
|
+
@endpoint = endpoint
|
438
|
+
return
|
439
|
+
end
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
Util.log("Discovery verification failure for #{claimed_id}")
|
444
|
+
failure_messages.each do |failure_message|
|
445
|
+
Util.log(" * Endpoint mismatch: " + failure_message)
|
446
|
+
end
|
447
|
+
|
448
|
+
# XXX: is DiscoveryFailure in Python OpenID
|
449
|
+
raise ProtocolError, "No matching endpoint found after " \
|
450
|
+
"discovering #{claimed_id}"
|
451
|
+
end
|
452
|
+
|
453
|
+
def verify_discovery_single(endpoint, to_match)
|
454
|
+
# Every type URI that's in the to_match endpoint has to be
|
455
|
+
# present in the discovered endpoint.
|
456
|
+
for type_uri in to_match.type_uris
|
457
|
+
raise TypeURIMismatch.new(type_uri, endpoint) unless endpoint.uses_extension(type_uri)
|
458
|
+
end
|
459
|
+
|
460
|
+
# Fragments do not influence discovery, so we can't compare a
|
461
|
+
# claimed identifier with a fragment to discovered information.
|
462
|
+
defragged_claimed_id =
|
463
|
+
case Yadis::XRI.identifier_scheme(to_match.claimed_id)
|
464
|
+
when :xri
|
465
|
+
to_match.claimed_id
|
466
|
+
when :uri
|
467
|
+
begin
|
468
|
+
parsed = URI.parse(to_match.claimed_id)
|
469
|
+
rescue URI::InvalidURIError
|
470
|
+
to_match.claimed_id
|
471
|
+
else
|
472
|
+
parsed.fragment = nil
|
473
|
+
parsed.to_s
|
474
|
+
end
|
475
|
+
else
|
476
|
+
raise StandardError, "Not reached"
|
477
|
+
end
|
478
|
+
|
479
|
+
if defragged_claimed_id != endpoint.claimed_id
|
480
|
+
raise ProtocolError, "Claimed ID does not match (different " \
|
481
|
+
"subjects!), Expected " \
|
482
|
+
"#{defragged_claimed_id}, got " \
|
483
|
+
"#{endpoint.claimed_id}"
|
484
|
+
end
|
485
|
+
|
486
|
+
if to_match.get_local_id != endpoint.get_local_id
|
487
|
+
raise ProtocolError, "local_id mismatch. Expected " \
|
488
|
+
"#{to_match.get_local_id}, got " \
|
489
|
+
"#{endpoint.get_local_id}"
|
490
|
+
end
|
491
|
+
|
492
|
+
# If the server URL is nil, this must be an OpenID 1
|
493
|
+
# response, because op_endpoint is a required parameter in
|
494
|
+
# OpenID 2. In that case, we don't actually care what the
|
495
|
+
# discovered server_url is, because signature checking or
|
496
|
+
# check_auth should take care of that check for us.
|
497
|
+
if to_match.server_url.nil?
|
498
|
+
if to_match.preferred_namespace != OPENID1_NS
|
499
|
+
raise StandardError,
|
500
|
+
"The code calling this must ensure that OpenID 2 " \
|
501
|
+
"responses have a non-none `openid.op_endpoint' and " \
|
502
|
+
"that it is set as the `server_url' attribute of the " \
|
503
|
+
"`to_match' endpoint."
|
504
|
+
end
|
505
|
+
elsif to_match.server_url != endpoint.server_url
|
506
|
+
raise ProtocolError, "OP Endpoint mismatch. Expected" \
|
507
|
+
"#{to_match.server_url}, got " \
|
508
|
+
"#{endpoint.server_url}"
|
509
|
+
end
|
510
|
+
end
|
511
|
+
end
|
512
|
+
end
|
513
|
+
end
|