rev |
line source |
jbe/bsw@20
|
1 --[[--
|
jbe/bsw@20
|
2 discovery_data, -- table containing "claimed_identifier", "op_endpoint" and "op_local_identifier"
|
jbe/bsw@20
|
3 errmsg, -- error message in case of failure
|
jbe/bsw@20
|
4 errcode = -- error code in case of failure (TODO: not implemented yet)
|
jbe/bsw@20
|
5 auth.openid.discover{
|
jbe/bsw@20
|
6 user_supplied_identifier = user_supplied_identifier, -- string given by user
|
jbe/bsw@20
|
7 https_as_default = https_as_default, -- default to https
|
jbe/bsw@20
|
8 curl_options = curl_options -- options passed to "curl" binary, when performing discovery
|
jbe/bsw@20
|
9 }
|
jbe/bsw@20
|
10
|
jbe/bsw@20
|
11 --]]--
|
jbe/bsw@20
|
12
|
jbe/bsw@20
|
13 -- helper function
|
jbe/bsw@20
|
14 local function decode_entities(str)
|
jbe/bsw@20
|
15 local str = str
|
jbe/bsw@20
|
16 str = string.gsub(value, "<", '<')
|
jbe/bsw@20
|
17 str = string.gsub(value, ">", '>')
|
jbe/bsw@20
|
18 str = string.gsub(value, """, '"')
|
jbe/bsw@20
|
19 str = string.gsub(value, "&", '&')
|
jbe/bsw@20
|
20 return str
|
jbe/bsw@20
|
21 end
|
jbe/bsw@20
|
22
|
jbe/bsw@20
|
23 -- helper function
|
jbe/bsw@20
|
24 local function get_tag_value(
|
jbe/bsw@20
|
25 str, -- HTML document or document snippet
|
jbe/bsw@20
|
26 match_tag, -- tag name
|
jbe/bsw@20
|
27 match_key, -- attribute key to match
|
jbe/bsw@20
|
28 match_value, -- attribute value to match
|
jbe/bsw@20
|
29 result_key -- attribute key of value to return
|
jbe/bsw@20
|
30 )
|
jbe/bsw@20
|
31 -- NOTE: The following parameters are case insensitive
|
jbe/bsw@20
|
32 local match_tag = string.lower(match_tag)
|
jbe/bsw@20
|
33 local match_key = string.lower(match_key)
|
jbe/bsw@20
|
34 local match_value = string.lower(match_value)
|
jbe/bsw@20
|
35 local result_key = string.lower(result_key)
|
jbe/bsw@20
|
36 for tag, attributes in
|
jbe/bsw@20
|
37 string.gmatch(str, "<([0-9A-Za-z_-]+) ([^>]*)>")
|
jbe/bsw@20
|
38 do
|
jbe/bsw@20
|
39 local tag = string.lower(tag)
|
jbe/bsw@20
|
40 if tag == match_tag then
|
jbe/bsw@20
|
41 local matching = false
|
jbe/bsw@20
|
42 for key, value in
|
jbe/bsw@20
|
43 string.gmatch(attributes, '([0-9A-Za-z_-]+)="([^"<>]*)"')
|
jbe/bsw@20
|
44 do
|
jbe/bsw@20
|
45 local key = string.lower(key)
|
jbe/bsw@20
|
46 local value = decode_entities(value)
|
jbe/bsw@20
|
47 if key == match_key then
|
jbe/bsw@20
|
48 -- NOTE: match_key must only match one key of space seperated list
|
jbe/bsw@20
|
49 for value in string.gmatch(value, "[^ ]+") do
|
jbe/bsw@20
|
50 if string.lower(value) == match_value then
|
jbe/bsw@20
|
51 matching = true
|
jbe/bsw@20
|
52 break
|
jbe/bsw@20
|
53 end
|
jbe/bsw@20
|
54 end
|
jbe/bsw@20
|
55 end
|
jbe/bsw@20
|
56 if key == result_key then
|
jbe/bsw@20
|
57 result_value = value
|
jbe/bsw@20
|
58 end
|
jbe/bsw@20
|
59 end
|
jbe/bsw@20
|
60 if matching then
|
jbe/bsw@20
|
61 return result_value
|
jbe/bsw@20
|
62 end
|
jbe/bsw@20
|
63 end
|
jbe/bsw@20
|
64 end
|
jbe/bsw@20
|
65 return nil
|
jbe/bsw@20
|
66 end
|
jbe/bsw@20
|
67
|
jbe/bsw@20
|
68 -- helper function
|
jbe/bsw@20
|
69 local function tag_contents(str, match_tag)
|
jbe/bsw@20
|
70 local pos = 0
|
jbe/bsw@20
|
71 local tagpos, closing, tag
|
jbe/bsw@20
|
72 local function next_tag()
|
jbe/bsw@20
|
73 local prefix
|
jbe/bsw@20
|
74 tagpos, prefix, tag, pos = string.match(
|
jbe/bsw@20
|
75 str,
|
jbe/bsw@20
|
76 "()<(/?)([0-9A-Za-z:_-]+)[^>]*>()",
|
jbe/bsw@20
|
77 pos
|
jbe/bsw@20
|
78 )
|
jbe/bsw@20
|
79 closing = (prefix == "/")
|
jbe/bsw@20
|
80 end
|
jbe/bsw@20
|
81 return function()
|
jbe/bsw@20
|
82 repeat
|
jbe/bsw@20
|
83 next_tag()
|
jbe/bsw@20
|
84 if not tagpos then return nil end
|
jbe/bsw@20
|
85 local stripped_tag
|
jbe/bsw@20
|
86 if string.find(tag, ":") then
|
jbe/bsw@20
|
87 stripped_tag = string.match(tag, ":([^:]*)$")
|
jbe/bsw@20
|
88 else
|
jbe/bsw@20
|
89 stripped_tag = tag
|
jbe/bsw@20
|
90 end
|
jbe/bsw@20
|
91 until stripped_tag == match_tag and not closing
|
jbe/bsw@20
|
92 local content_start = pos
|
jbe/bsw@20
|
93 local used_tag = tag
|
jbe/bsw@20
|
94 local counter = 0
|
jbe/bsw@20
|
95 while true do
|
jbe/bsw@20
|
96 repeat
|
jbe/bsw@20
|
97 next_tag()
|
jbe/bsw@20
|
98 if not tagpos then return nil end
|
jbe/bsw@20
|
99 until tag == used_tag
|
jbe/bsw@20
|
100 if closing then
|
jbe/bsw@20
|
101 if counter > 0 then
|
jbe/bsw@20
|
102 counter = counter - 1
|
jbe/bsw@20
|
103 else
|
jbe/bsw@20
|
104 return string.sub(str, content_start, tagpos-1)
|
jbe/bsw@20
|
105 end
|
jbe/bsw@20
|
106 else
|
jbe/bsw@20
|
107 counter = counter + 1
|
jbe/bsw@20
|
108 end
|
jbe/bsw@20
|
109 end
|
jbe/bsw@20
|
110 local content = string.sub(rest, 1, startpos-1)
|
jbe/bsw@20
|
111 str = string.sub(rest, endpos+1)
|
jbe/bsw@20
|
112 return content
|
jbe/bsw@20
|
113 end
|
jbe/bsw@20
|
114 end
|
jbe/bsw@20
|
115
|
jbe/bsw@20
|
116 local function strip(str)
|
jbe/bsw@20
|
117 local str = str
|
jbe/bsw@20
|
118 string.gsub(str, "^[ \t\r\n]+", "")
|
jbe/bsw@20
|
119 string.gsub(str, "[ \t\r\n]+$", "")
|
jbe/bsw@20
|
120 return str
|
jbe/bsw@20
|
121 end
|
jbe/bsw@20
|
122
|
jbe/bsw@20
|
123 function auth.openid.discover(args)
|
jbe/bsw@20
|
124 local url = string.match(args.user_supplied_identifier, "[^#]*")
|
jbe/bsw@20
|
125 -- NOTE: XRIs are not supported
|
jbe/bsw@20
|
126 if
|
jbe/bsw@20
|
127 string.find(url, "^[Xx][Rr][Ii]://") or
|
jbe/bsw@20
|
128 string.find(url, "^[=@+$!(]")
|
jbe/bsw@20
|
129 then
|
jbe/bsw@20
|
130 return nil, "XRI identifiers are not supported."
|
jbe/bsw@20
|
131 end
|
jbe/bsw@20
|
132 -- Prepend http:// or https://, if neccessary:
|
jbe/bsw@20
|
133 if not string.find(url, "://") then
|
jbe/bsw@20
|
134 if args.default_to_https then
|
jbe/bsw@20
|
135 url = "https://" .. url
|
jbe/bsw@20
|
136 else
|
jbe/bsw@20
|
137 url = "http://" .. url
|
jbe/bsw@20
|
138 end
|
jbe/bsw@20
|
139 end
|
jbe/bsw@20
|
140 -- Either an xrds_document or an html_document will be fetched
|
jbe/bsw@20
|
141 local xrds_document, html_document
|
jbe/bsw@20
|
142 -- Repeat max 10 times to avoid endless redirection loops
|
jbe/bsw@20
|
143 local redirects = 0
|
jbe/bsw@20
|
144 while true do
|
jbe/bsw@20
|
145 local status, headers, body = auth.openid._curl(url, args.curl_options)
|
jbe/bsw@20
|
146 if not status then
|
jbe/bsw@20
|
147 return nil, "Error while locating XRDS or HTML file for discovery."
|
jbe/bsw@20
|
148 end
|
jbe/bsw@20
|
149 -- Check, if we are redirected:
|
jbe/bsw@20
|
150 local location = string.match(
|
jbe/bsw@20
|
151 headers,
|
jbe/bsw@20
|
152 "\r?\n[Ll][Oo][Cc][Aa][Tt][Ii][Oo][Nn]:[ \t]*([^\r\n]+)"
|
jbe/bsw@20
|
153 )
|
jbe/bsw@20
|
154 if location then
|
jbe/bsw@20
|
155 -- If we are redirected too often, then return an error.
|
jbe/bsw@20
|
156 if redirects >= 10 then
|
jbe/bsw@20
|
157 return nil, "Too many redirects."
|
jbe/bsw@20
|
158 end
|
jbe/bsw@20
|
159 -- Otherwise follow the redirection by changing the variable "url"
|
jbe/bsw@20
|
160 -- and by incrementing the redirect counter.
|
jbe/bsw@20
|
161 url = location
|
jbe/bsw@20
|
162 redirects = redirects + 1
|
jbe/bsw@20
|
163 else
|
jbe/bsw@20
|
164 -- Check, if there is an X-XRDS-Location header
|
jbe/bsw@20
|
165 -- pointing to an XRDS document:
|
jbe/bsw@20
|
166 local xrds_location = string.match(
|
jbe/bsw@20
|
167 headers,
|
jbe/bsw@20
|
168 "\r?\n[Xx]%-[Xx][Rr][Dd][Ss]%-[Ll][Oo][Cc][Aa][Tt][Ii][Oo][Nn]:[ \t]*([^\r\n]+)"
|
jbe/bsw@20
|
169 )
|
jbe/bsw@20
|
170 -- If there is no X-XRDS-Location header, there might be an
|
jbe/bsw@20
|
171 -- http-equiv meta tag serving the same purpose:
|
jbe/bsw@20
|
172 if not xrds_location and status == 200 then
|
jbe/bsw@20
|
173 xrds_location = get_tag_value(body, "meta", "http-equiv", "X-XRDS-Location", "content")
|
jbe/bsw@20
|
174 end
|
jbe/bsw@20
|
175 if xrds_location then
|
jbe/bsw@20
|
176 -- If we know the XRDS-Location, we can fetch the XRDS document
|
jbe/bsw@20
|
177 -- from that location:
|
jbe/bsw@20
|
178 local status, headers, body = auth.openid._curl(xrds_location, args.curl_options)
|
jbe/bsw@20
|
179 if not status then
|
jbe/bsw@20
|
180 return nil, "XRDS document could not be loaded."
|
jbe/bsw@20
|
181 end
|
jbe/bsw@20
|
182 if status ~= 200 then
|
jbe/bsw@20
|
183 return nil, "XRDS document not found where expected."
|
jbe/bsw@20
|
184 end
|
jbe/bsw@20
|
185 xrds_document = body
|
jbe/bsw@20
|
186 break
|
jbe/bsw@20
|
187 elseif
|
jbe/bsw@20
|
188 -- If the Content-Type header is set accordingly, then we already
|
jbe/bsw@20
|
189 -- should have received an XRDS document:
|
jbe/bsw@20
|
190 string.find(
|
jbe/bsw@20
|
191 headers,
|
jbe/bsw@20
|
192 "\r?\n[Cc][Oo][Nn][Tt][Ee][Nn][Tt]%-[Tt][Yy][Pp][Ee]:[ \t]*application/xrds%+xml\r?\n"
|
jbe/bsw@20
|
193 )
|
jbe/bsw@20
|
194 then
|
jbe/bsw@20
|
195 if status ~= 200 then
|
jbe/bsw@20
|
196 return nil, "XRDS document announced but not found."
|
jbe/bsw@20
|
197 end
|
jbe/bsw@20
|
198 xrds_document = body
|
jbe/bsw@20
|
199 break
|
jbe/bsw@20
|
200 else
|
jbe/bsw@20
|
201 -- Otherwise we should have received an HTML document:
|
jbe/bsw@20
|
202 if status ~= 200 then
|
jbe/bsw@20
|
203 return nil, "No XRDS or HTML document found for discovery."
|
jbe/bsw@20
|
204 end
|
jbe/bsw@20
|
205 html_document = body
|
jbe/bsw@20
|
206 break;
|
jbe/bsw@20
|
207 end
|
jbe/bsw@20
|
208 end
|
jbe/bsw@20
|
209 end
|
jbe/bsw@20
|
210 local claimed_identifier -- OpenID identifier the user claims to own
|
jbe/bsw@20
|
211 local op_endpoint -- OpenID provider endpoint URL
|
jbe/bsw@20
|
212 local op_local_identifier -- optional user identifier, local to the OpenID provider
|
jbe/bsw@20
|
213 if xrds_document then
|
jbe/bsw@20
|
214 -- If we got an XRDS document, we look for a matching <Service> entry:
|
jbe/bsw@20
|
215 for content in tag_contents(xrds_document, "Service") do
|
jbe/bsw@20
|
216 local service_uri, service_localid
|
jbe/bsw@20
|
217 for content in tag_contents(content, "URI") do
|
jbe/bsw@20
|
218 if not string.find(content, "[<>]") then
|
jbe/bsw@20
|
219 service_uri = strip(content)
|
jbe/bsw@20
|
220 break
|
jbe/bsw@20
|
221 end
|
jbe/bsw@20
|
222 end
|
jbe/bsw@20
|
223 for content in tag_contents(content, "LocalID") do
|
jbe/bsw@20
|
224 if not string.find(content, "[<>]") then
|
jbe/bsw@20
|
225 service_localid = strip(content)
|
jbe/bsw@20
|
226 break
|
jbe/bsw@20
|
227 end
|
jbe/bsw@20
|
228 end
|
jbe/bsw@20
|
229 for content in tag_contents(content, "Type") do
|
jbe/bsw@20
|
230 if not string.find(content, "[<>]") then
|
jbe/bsw@20
|
231 local content = strip(content)
|
jbe/bsw@20
|
232 if content == "http://specs.openid.net/auth/2.0/server" then
|
jbe/bsw@20
|
233 -- The user entered a provider identifier, thus claimed_identifier
|
jbe/bsw@20
|
234 -- and op_local_identifier will be set to nil.
|
jbe/bsw@20
|
235 op_endpoint = service_uri
|
jbe/bsw@20
|
236 break
|
jbe/bsw@20
|
237 elseif content == "http://specs.openid.net/auth/2.0/signon" then
|
jbe/bsw@20
|
238 -- The user entered his/her own identifier.
|
jbe/bsw@20
|
239 claimed_identifier = url
|
jbe/bsw@20
|
240 op_endpoint = service_uri
|
jbe/bsw@20
|
241 op_local_identifier = service_localid
|
jbe/bsw@20
|
242 break
|
jbe/bsw@20
|
243 end
|
jbe/bsw@20
|
244 end
|
jbe/bsw@20
|
245 end
|
jbe/bsw@20
|
246 end
|
jbe/bsw@20
|
247 elseif html_document then
|
jbe/bsw@20
|
248 -- If we got an HTML document, we look for matching <link .../> tags:
|
jbe/bsw@20
|
249 claimed_identifier = url
|
jbe/bsw@20
|
250 op_endpoint = get_tag_value(
|
jbe/bsw@20
|
251 html_document,
|
jbe/bsw@20
|
252 "link", "rel", "openid2.provider", "href"
|
jbe/bsw@20
|
253 )
|
jbe/bsw@20
|
254 op_local_identifier = get_tag_value(
|
jbe/bsw@20
|
255 html_document,
|
jbe/bsw@20
|
256 "link", "rel", "openid2.local_id", "href"
|
jbe/bsw@20
|
257 )
|
jbe/bsw@20
|
258 else
|
jbe/bsw@20
|
259 error("Assertion failed") -- should not happen
|
jbe/bsw@20
|
260 end
|
jbe/bsw@20
|
261 if not op_endpoint then
|
jbe/bsw@20
|
262 return nil, "No OpenID endpoint found."
|
jbe/bsw@20
|
263 end
|
jbe/bsw@20
|
264 if claimed_identifier then
|
jbe/bsw@20
|
265 claimed_identifier = auth.openid._normalize_url(claimed_identifier)
|
jbe/bsw@20
|
266 if not claimed_identifier then
|
jbe/bsw@20
|
267 return nil, "Claimed identifier could not be normalized."
|
jbe/bsw@20
|
268 end
|
jbe/bsw@20
|
269 end
|
jbe/bsw@20
|
270 return {
|
jbe/bsw@20
|
271 claimed_identifier = claimed_identifier,
|
jbe/bsw@20
|
272 op_endpoint = op_endpoint,
|
jbe/bsw@20
|
273 op_local_identifier = op_local_identifier
|
jbe/bsw@20
|
274 }
|
jbe/bsw@20
|
275 end
|