1 local usermanager
= require
"core.usermanager"
2 local httpserver
= require
"net.httpserver"
3 local jidutil
= require
"util.jid"
4 local hmac
= require
"hmac"
6 local base64
= require
"util.encodings".base64
8 local humane
= require
"util.serialization".serialize
12 local openidns
= "http://specs.openid.net/auth/2.0" -- [#4.1.2]
13 local response_404
= { status
= "404 Not Found", body
= "<h1>Page Not Found</h1>Sorry, we couldn't find what you were looking for :(" };
15 local associations
= {}
17 local function genkey(length
)
18 -- FIXME not cryptographically secure
22 local rand
= math
.random(33, 126)
23 table.insert(str
, string.char(rand
))
26 return table.concat(str
)
29 local function tokvstring(dict
)
30 -- key-value encoding for a dictionary [#4.1.3]
33 for k
,v
in pairs(dict
) do
34 str
= str
..k
..":"..v
.."\n"
40 local function newassoc(key
, shared
)
41 -- TODO don't use genkey here
42 local handle
= genkey(16)
43 associations
[handle
] = {}
44 associations
[handle
]["key"] = key
45 associations
[handle
]["shared"] = shared
46 associations
[handle
]["time"] = os
.time()
50 local function split(str
, sep
)
52 str
:gsub("([^.."..sep
.."]*)"..sep
, function(c
) table.insert(splits
, c
) end)
56 local function sign(response
, key
)
59 for _
,field
in pairs(split(response
["openid.signed"],",")) do
60 fields
[field
] = response
["openid."..field
]
64 return base64
.encode(hmac
.sha256(key
, tokvstring(fields
)))
67 local function urlencode(s
)
68 return (string.gsub(s
, "%W",
70 return string.format("%%%02X", string.byte(str
))
74 local function urldecode(s
)
75 return(string.gsub(string.gsub(s
, "+", " "), "%%(%x%x)",
77 return string.char(tonumber(str
,16))
81 local function utctime()
83 local diff
= os
.difftime(now
, os
.time(os
.date("!*t", now
)))
87 local function nonce()
88 -- generate a response nonce [#10.1]
91 random = random..string.char(math
.random(33,126))
94 local timestamp
= os
.date("%Y-%m-%dT%H:%M:%SZ", utctime())
96 return timestamp
..random
99 local function query_params(query
)
100 if type(query
) == "string" and #query
> 0 then
101 if query
:match("=") then
103 for k
, v
in query
:gmatch("&?([^=%?]+)=([^&%?]+)&?") do
105 params
[urldecode(k
)] = urldecode(v
)
110 return urldecode(query
)
115 local function split_host_port(combined
)
116 local host
= combined
118 local cpos
= string.find(combined
, ":")
120 host
= string.sub(combined
, 0, cpos
-1)
121 port
= string.sub(combined
, cpos
+1)
127 local function toquerystring(dict
)
128 -- query string encoding for a dictionary [#4.1.3]
131 for k
,v
in pairs(dict
) do
132 str
= str
..urlencode(k
).."="..urlencode(v
).."&"
135 return string.sub(str
, 0, -1)
138 local function match_realm(url
, realm
)
139 -- FIXME do actual match [#9.2]
143 local function handle_endpoint(method
, body
, request
)
144 module
:log("debug", "Request at OpenID provider endpoint")
148 if method
== "GET" then
149 params
= query_params(request
.url
.query
)
150 elseif method
== "POST" then
151 params
= query_params(body
)
157 module
:log("debug", "Request Parameters:\n"..humane(params
))
159 if params
["openid.ns"] == openidns
then
160 -- OpenID 2.0 request [#5.1.1]
161 if params
["openid.mode"] == "associate" then
162 -- Associate mode [#8]
163 -- TODO implement association
165 -- Error response [#8.2.4]
166 local openidresponse
= {
168 ["session_type"] = params
["openid.session_type"],
169 ["assoc_type"] = params
["openid.assoc_type"],
170 ["error"] = "Association not supported... yet",
171 ["error_code"] = "unsupported-type",
174 local kvresponse
= tokvstring(openidresponse
)
175 module
:log("debug", "OpenID Response:\n"..kvresponse
)
178 ["Content-Type"] = "text/plain"
182 elseif params
["openid.mode"] == "checkid_setup" or params
["openid.mode"] == "checkid_immediate" then
183 -- Requesting authentication [#9]
184 if not params
["openid.realm"] then
185 -- set realm to default value of return_to [#9.1]
186 if params
["openid.return_to"] then
187 params
["openid.realm"] = params
["openid.return_to"]
189 -- neither was sent, error [#9.1]
190 -- FIXME return proper error
195 if params
["openid.return_to"] then
196 -- Assure that the return_to url matches the realm [#9.2]
197 if not match_realm(params
["openid.return_to"], params
["openid.realm"]) then
198 -- FIXME return proper error
202 -- Verify the return url [#9.2.1]
203 -- TODO implement return url verification
206 if params
["openid.claimed_id"] and params
["openid.identity"] then
207 -- asserting an identifier [#9.1]
209 if params
["openid.identity"] == "http://specs.openid.net/auth/2.0/identifier_select" then
210 -- automatically select an identity [#9.1]
211 params
["openid.identity"] = params
["openid.claimed_id"]
214 if params
["openid.mode"] == "checkid_setup" then
215 -- Check ID Setup mode
216 -- TODO implement: NEXT STEP
217 local head
= "<title>Prosody OpenID : Login</title>"
218 local body
= string.format([[
219 <p>Open ID Authentication<p>
220 <p>Identifier: <tt>%s</tt></p>
221 <p>Realm: <tt>%s</tt></p>
222 <p>Return: <tt>%s</tt></p>
223 <form method="POST" action="%s">
224 Jabber ID: <input type="text" name="jid"/><br/>
225 Password: <input type="password" name="password"/><br/>
226 <input type="hidden" name="openid.return_to" value="%s"/>
227 <input type="submit" value="Authenticate"/>
229 ]], params
["openid.claimed_id"], params
["openid.realm"], params
["openid.return_to"], base
, params
["openid.return_to"])
231 return string.format([[
232 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
233 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
234 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
236 <meta http-equiv="Content-type" content="text/html;charset=UTF-8" />
244 elseif params
["openid.mode"] == "checkid_immediate" then
245 -- Check ID Immediate mode [#9.3]
246 -- TODO implement check id immediate
249 -- not asserting an identifier [#9.1]
250 -- used for extensions
251 -- TODO implement common extensions
253 elseif params
["openid.mode"] == "check_authentication" then
254 module
:log("debug", "OpenID Check Authentication Mode")
255 local assoc
= associations
[params
["openid.assoc_handle"]]
256 module
:log("debug", "Checking Association Handle: "..params
["openid.assoc_handle"])
257 if assoc
and not assoc
["shared"] then
258 module
:log("debug", "Found valid association")
259 local sig
= sign(params
, assoc
["key"])
261 local is_valid
= "false"
262 if sig
== params
["openid.sig"] then
266 module
:log("debug", "Signature is: "..is_valid
)
273 -- Delete this association
274 associations
[params
["openid.assoc_handle"]]
= nil
277 ["Content-Type"] = "text/plain"
279 body
= tokvstring(openidresponse
),
282 module
:log("debug", "No valid association")
284 -- Invalidate the handle [#11.4.2.2]
290 elseif params
["password"] then
291 -- User is authenticating
292 local user
, domain
= jidutil
.split(params
["jid"])
293 module
:log("debug", "Authenticating "..params
["jid"].." ("..user
..","..domain
..") with password: "..params
["password"])
294 local valid
= usermanager
.validate_credentials(domain
, user
, params
["password"], "PLAIN")
296 module
:log("debug", "Authentication Succeeded: "..params
["jid"])
297 if params
["openid.return_to"] ~= "" then
298 -- TODO redirect the user to return_to with the openid response
299 -- included, need to handle the case if its a GET, that there are
300 -- existing query parameters on the return_to URL [#10.1]
301 local host
, port
= split_host_port(request
.headers
.host
)
302 local endpointurl
= ""
304 endpointurl
= string.format("http://%s/%s", host
, base
)
306 endpointurl
= string.format("http://%s:%s/%s", host
, port
, base
)
309 local nonce
= nonce()
310 local key
= genkey(32)
311 local assoc_handle
= newassoc(key
)
313 local openidresponse
= {
314 ["openid.ns"] = openidns
,
315 ["openid.mode"] = "id_res",
316 ["openid.op_endpoint"] = endpointurl
,
317 ["openid.claimed_id"] = endpointurl
.."/"..user
,
318 ["openid.identity"] = endpointurl
.."/"..user
,
319 ["openid.return_to"] = params
["openid.return_to"],
320 ["openid.response_nonce"] = nonce
,
321 ["openid.assoc_handle"] = assoc_handle
,
322 ["openid.signed"] = "op_endpoint,identity,claimed_id,return_to,assoc_handle,response_nonce", -- FIXME
323 ["openid.sig"] = nil,
326 openidresponse
["openid.sig"] = sign(openidresponse
, key
)
328 queryresponse
= toquerystring(openidresponse
)
330 redirecturl
= params
["openid.return_to"]
331 -- add the parameters to the return_to
332 if redirecturl
:match("?") then
333 redirecturl
= redirecturl
.."&"
335 redirecturl
= redirecturl
.."?"
338 redirecturl
= redirecturl
..queryresponse
340 module
:log("debug", "Open ID Positive Assertion Response Table:\n"..humane(openidresponse
))
341 module
:log("debug", "Open ID Positive Assertion Response URL:\n"..queryresponse
)
342 module
:log("debug", "Redirecting User to:\n"..redirecturl
)
344 status
= "303 See Other",
346 Location
= redirecturl
,
348 body
= "Redirecting to: "..redirecturl
-- TODO Include a note with a hyperlink to redirect
351 -- TODO Do something useful is there is no return_to
354 module
:log("debug", "Authentication Failed: "..params
["jid"])
355 -- TODO let them try again
358 -- Not an Open ID request, do something useful
365 local function handle_identifier(method
, body
, request
, id
)
366 module
:log("debug", "Request at OpenID identifier")
367 local host
, port
= split_host_port(request
.headers
.host
)
370 local user_domain
= ""
371 local apos
= string.find(id
, "@")
376 user_name
= string.sub(id
, 0, apos
-1)
377 user_domain
= string.sub(id
, apos
+1)
380 user
, domain
= jidutil
.split(id
)
382 local exists
= usermanager
.user_exists(user_name
, user_domain
)
388 local endpointurl
= ""
390 endpointurl
= string.format("http://%s/%s", host
, base
)
392 endpointurl
= string.format("http://%s:%s/%s", host
, port
, base
)
395 local head
= string.format("<title>Prosody OpenID : %s@%s</title>", user_name
, user_domain
)
396 -- OpenID HTML discovery [#7.3]
397 head
= head
.. string.format('<link rel="openid2.provider" href="%s" />', endpointurl
)
399 local content
= 'request.url.path: ' .. request
.url
.path
.. '<br/>'
400 content
= content
.. 'host+port: ' .. request
.headers
.host
.. '<br/>'
401 content
= content
.. 'host: ' .. tostring(host
) .. '<br/>'
402 content
= content
.. 'port: ' .. tostring(port
) .. '<br/>'
403 content
= content
.. 'user_name: ' .. user_name
.. '<br/>'
404 content
= content
.. 'user_domain: ' .. user_domain
.. '<br/>'
405 content
= content
.. 'exists: ' .. tostring(exists
) .. '<br/>'
407 local body
= string.format('<p>%s</p>', content
)
409 local data
= string.format([[
410 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
411 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
412 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
414 <meta http-equiv="Content-type" content="text/html;charset=UTF-8" />
425 local function handle_request(method
, body
, request
)
426 module
:log("debug", "Received request")
428 -- Make sure the host is enabled
429 local host
= split_host_port(request
.headers
.host
)
430 if not hosts
[host
] then
434 if request
.url
.path
== "/"..base
then
435 -- OpenID Provider Endpoint
436 return handle_endpoint(method
, body
, request
)
438 local id
= request
.url
.path
:match("^/"..base
.."/(.+)$")
441 return handle_identifier(method
, body
, request
, id
)
448 httpserver
.new
{ port
= 5280, base
= base
, handler
= handle_request
, ssl
= false}