ruby-openid2 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|