Mercurial > prosody-modules
diff mod_openid/mod_openid.lua @ 3:723fd785815f
mod_openid: Initial commit
author | Dwayne Bent <dbb.0@liqd.org> |
---|---|
date | Sun, 13 Sep 2009 10:40:21 -0400 |
parents | |
children | 7dbde05b48a9 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_openid/mod_openid.lua Sun Sep 13 10:40:21 2009 -0400 @@ -0,0 +1,448 @@ +local usermanager = require "core.usermanager" +local httpserver = require "net.httpserver" +local jidutil = require "util.jid" +local hmac = require "hmac" + +local base64 = require "util.encodings".base64 + +local humane = require "util.serialization".serialize + +-- Configuration +local base = "openid" +local openidns = "http://specs.openid.net/auth/2.0" -- [#4.1.2] +local response_404 = { status = "404 Not Found", body = "<h1>Page Not Found</h1>Sorry, we couldn't find what you were looking for :(" }; + +local associations = {} + +local function genkey(length) + -- FIXME not cryptographically secure + str = {} + + for i = 1,length do + local rand = math.random(33, 126) + table.insert(str, string.char(rand)) + end + + return table.concat(str) +end + +local function tokvstring(dict) + -- key-value encoding for a dictionary [#4.1.3] + local str = "" + + for k,v in pairs(dict) do + str = str..k..":"..v.."\n" + end + + return str +end + +local function newassoc(key, shared) + -- TODO don't use genkey here + local handle = genkey(16) + associations[handle] = {} + associations[handle]["key"] = key + associations[handle]["shared"] = shared + associations[handle]["time"] = os.time() + return handle +end + +local function split(str, sep) + local splits = {} + str:gsub("([^.."..sep.."]*)"..sep, function(c) table.insert(splits, c) end) + return splits +end + +local function sign(response, key) + local fields = {} + + for _,field in pairs(split(response["openid.signed"],",")) do + fields[field] = response["openid."..field] + end + + -- [#10.1] + return base64.encode(hmac.sha256(key, tokvstring(fields))) +end + +local function urlencode(s) + return (string.gsub(s, "%W", + function(str) + return string.format("%%%02X", string.byte(str)) + end)) +end + +local function urldecode(s) + return(string.gsub(string.gsub(s, "+", " "), "%%(%x%x)", + function(str) + return string.char(tonumber(str,16)) + end)) +end + +local function utctime() + local now = os.time() + local diff = os.difftime(now, os.time(os.date("!*t", now))) + return now-diff +end + +local function nonce() + -- generate a response nonce [#10.1] + local random = "" + for i=0,10 do + random = random..string.char(math.random(33,126)) + end + + local timestamp = os.date("%Y-%m-%dT%H:%M:%SZ", utctime()) + + return timestamp..random +end + +local function query_params(query) + if type(query) == "string" and #query > 0 then + if query:match("=") then + local params = {} + for k, v in query:gmatch("&?([^=%?]+)=([^&%?]+)&?") do + if k and v then + params[urldecode(k)] = urldecode(v) + end + end + return params + else + return urldecode(query) + end + end +end + +local function split_host_port(combined) + local host = combined + local port = "" + local cpos = string.find(combined, ":") + if cpos ~= nil then + host = string.sub(combined, 0, cpos-1) + port = string.sub(combined, cpos+1) + end + + return host, port +end + +local function toquerystring(dict) + -- query string encoding for a dictionary [#4.1.3] + local str = "" + + for k,v in pairs(dict) do + str = str..urlencode(k).."="..urlencode(v).."&" + end + + return string.sub(str, 0, -1) +end + +local function match_realm(url, realm) + -- FIXME do actual match [#9.2] + return true +end + +local function handle_endpoint(method, body, request) + module:log("debug", "Request at OpenID provider endpoint") + + local params = nil + + if method == "GET" then + params = query_params(request.url.query) + elseif method == "POST" then + params = query_params(body) + else + -- TODO error + return response_404 + end + + module:log("debug", "Request Parameters:\n"..humane(params)) + + if params["openid.ns"] == openidns then + -- OpenID 2.0 request [#5.1.1] + if params["openid.mode"] == "associate" then + -- Associate mode [#8] + -- TODO implement association + + -- Error response [#8.2.4] + local openidresponse = { + ["ns"] = openidns, + ["session_type"] = params["openid.session_type"], + ["assoc_type"] = params["openid.assoc_type"], + ["error"] = "Association not supported... yet", + ["error_code"] = "unsupported-type", + } + + local kvresponse = tokvstring(openidresponse) + module:log("debug", "OpenID Response:\n"..kvresponse) + return { + headers = { + ["Content-Type"] = "text/plain" + }, + body = kvresponse + } + elseif params["openid.mode"] == "checkid_setup" or params["openid.mode"] == "checkid_immediate" then + -- Requesting authentication [#9] + if not params["openid.realm"] then + -- set realm to default value of return_to [#9.1] + if params["openid.return_to"] then + params["openid.realm"] = params["openid.return_to"] + else + -- neither was sent, error [#9.1] + -- FIXME return proper error + return response_404 + end + end + + if params["openid.return_to"] then + -- Assure that the return_to url matches the realm [#9.2] + if not match_realm(params["openid.return_to"], params["openid.realm"]) then + -- FIXME return proper error + return response_404 + end + + -- Verify the return url [#9.2.1] + -- TODO implement return url verification + end + + if params["openid.claimed_id"] and params["openid.identity"] then + -- asserting an identifier [#9.1] + + if params["openid.identity"] == "http://specs.openid.net/auth/2.0/identifier_select" then + -- automatically select an identity [#9.1] + params["openid.identity"] = params["openid.claimed_id"] + end + + if params["openid.mode"] == "checkid_setup" then + -- Check ID Setup mode + -- TODO implement: NEXT STEP + local head = "<title>Prosody OpenID : Login</title>" + local body = string.format([[ +<p>Open ID Authentication<p> +<p>Identifier: <tt>%s</tt></p> +<p>Realm: <tt>%s</tt></p> +<p>Return: <tt>%s</tt></p> +<form method="POST" action="%s"> + Jabber ID: <input type="text" name="jid"/><br/> + Password: <input type="password" name="password"/><br/> + <input type="hidden" name="openid.return_to" value="%s"/> + <input type="submit" value="Authenticate"/> +</form> + ]], params["openid.claimed_id"], params["openid.realm"], params["openid.return_to"], base, params["openid.return_to"]) + + return string.format([[ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" +"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> +<head> +<meta http-equiv="Content-type" content="text/html;charset=UTF-8" /> +%s +</head> +<body> +%s +</body> +</html> + ]], head, body) + elseif params["openid.mode"] == "checkid_immediate" then + -- Check ID Immediate mode [#9.3] + -- TODO implement check id immediate + end + else + -- not asserting an identifier [#9.1] + -- used for extensions + -- TODO implement common extensions + end + elseif params["openid.mode"] == "check_authentication" then + module:log("debug", "OpenID Check Authentication Mode") + local assoc = associations[params["openid.assoc_handle"]] + module:log("debug", "Checking Association Handle: "..params["openid.assoc_handle"]) + if assoc and not assoc["shared"] then + module:log("debug", "Found valid association") + local sig = sign(params, assoc["key"]) + + local is_valid = "false" + if sig == params["openid.sig"] then + is_valid = "true" + end + + module:log("debug", "Signature is: "..is_valid) + + openidresponse = { + ns = openidns, + is_valid = is_valid, + } + + -- Delete this association + associations[params["openid.assoc_handle"]] = nil + return { + headers = { + ["Content-Type"] = "text/plain" + }, + body = tokvstring(openidresponse), + } + else + module:log("debug", "No valid association") + -- TODO return error + -- Invalidate the handle [#11.4.2.2] + end + else + -- Some other mode + -- TODO error + end + elseif params["password"] then + -- User is authenticating + local user, domain = jidutil.split(params["jid"]) + module:log("debug", "Authenticating "..params["jid"].." ("..user..","..domain..") with password: "..params["password"]) + local valid = usermanager.validate_credentials(domain, user, params["password"], "PLAIN") + if valid then + module:log("debug", "Authentication Succeeded: "..params["jid"]) + if params["openid.return_to"] ~= "" then + -- TODO redirect the user to return_to with the openid response + -- included, need to handle the case if its a GET, that there are + -- existing query parameters on the return_to URL [#10.1] + local host, port = split_host_port(request.headers.host) + local endpointurl = "" + if port == '' then + endpointurl = string.format("http://%s/%s", host, base) + else + endpointurl = string.format("http://%s:%s/%s", host, port, base) + end + + local nonce = nonce() + local key = genkey(32) + local assoc_handle = newassoc(key) + + local openidresponse = { + ["openid.ns"] = openidns, + ["openid.mode"] = "id_res", + ["openid.op_endpoint"] = endpointurl, + ["openid.claimed_id"] = endpointurl.."/"..user, + ["openid.identity"] = endpointurl.."/"..user, + ["openid.return_to"] = params["openid.return_to"], + ["openid.response_nonce"] = nonce, + ["openid.assoc_handle"] = assoc_handle, + ["openid.signed"] = "op_endpoint,identity,claimed_id,return_to,assoc_handle,response_nonce", -- FIXME + ["openid.sig"] = nil, + } + + openidresponse["openid.sig"] = sign(openidresponse, key) + + queryresponse = toquerystring(openidresponse) + + redirecturl = params["openid.return_to"] + -- add the parameters to the return_to + if redirecturl:match("?") then + redirecturl = redirecturl.."&" + else + redirecturl = redirecturl.."?" + end + + redirecturl = redirecturl..queryresponse + + module:log("debug", "Open ID Positive Assertion Response Table:\n"..humane(openidresponse)) + module:log("debug", "Open ID Positive Assertion Response URL:\n"..queryresponse) + module:log("debug", "Redirecting User to:\n"..redirecturl) + return { + status = "303 See Other", + headers = { + Location = redirecturl, + }, + body = "Redirecting to: "..redirecturl -- TODO Include a note with a hyperlink to redirect + } + else + -- TODO Do something useful is there is no return_to + end + else + module:log("debug", "Authentication Failed: "..params["jid"]) + -- TODO let them try again + end + else + -- Not an Open ID request, do something useful + -- TODO + end + + return response_404 +end + +local function handle_identifier(method, body, request, id) + module:log("debug", "Request at OpenID identifier") + local host, port = split_host_port(request.headers.host) + + local user_name = "" + local user_domain = "" + local apos = string.find(id, "@") + if apos == nil then + user_name = id + user_domain = host + else + user_name = string.sub(id, 0, apos-1) + user_domain = string.sub(id, apos+1) + end + + user, domain = jidutil.split(id) + + local exists = usermanager.user_exists(user_name, user_domain) + + if not exists then + return response_404 + end + + local endpointurl = "" + if port == '' then + endpointurl = string.format("http://%s/%s", host, base) + else + endpointurl = string.format("http://%s:%s/%s", host, port, base) + end + + local head = string.format("<title>Prosody OpenID : %s@%s</title>", user_name, user_domain) + -- OpenID HTML discovery [#7.3] + head = head .. string.format('<link rel="openid2.provider" href="%s" />', endpointurl) + + local content = 'request.url.path: ' .. request.url.path .. '<br/>' + content = content .. 'host+port: ' .. request.headers.host .. '<br/>' + content = content .. 'host: ' .. tostring(host) .. '<br/>' + content = content .. 'port: ' .. tostring(port) .. '<br/>' + content = content .. 'user_name: ' .. user_name .. '<br/>' + content = content .. 'user_domain: ' .. user_domain .. '<br/>' + content = content .. 'exists: ' .. tostring(exists) .. '<br/>' + + local body = string.format('<p>%s</p>', content) + + local data = string.format([[ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" +"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> +<head> +<meta http-equiv="Content-type" content="text/html;charset=UTF-8" /> +%s +</head> +<body> +%s +</body> +</html> + ]], head, body) + return data; +end + +local function handle_request(method, body, request) + module:log("debug", "Received request") + + -- Make sure the host is enabled + local host = split_host_port(request.headers.host) + if not hosts[host] then + return response_404 + end + + if request.url.path == "/"..base then + -- OpenID Provider Endpoint + return handle_endpoint(method, body, request) + else + local id = request.url.path:match("^/"..base.."/(.+)$") + if id then + -- OpenID Identifier + return handle_identifier(method, body, request, id) + else + return response_404 + end + end +end + +httpserver.new{ port = 5280, base = base, handler = handle_request, ssl = false}