Software / code / prosody-modules
Annotate
mod_openid/mod_openid.lua @ 2735:b5fae17e4403
mod_csi_battery_saver: correctly handle encrypted message stanzas
Thanks to michel
| author | tmolitor <thilo@eightysoft.de> |
|---|---|
| date | Sat, 12 Aug 2017 20:45:11 +0200 |
| parent | 1343:7dbde05b48a9 |
| rev | line source |
|---|---|
| 3 | 1 local usermanager = require "core.usermanager" |
| 2 local httpserver = require "net.httpserver" | |
| 3 local jidutil = require "util.jid" | |
| 4 local hmac = require "hmac" | |
| 5 | |
| 6 local base64 = require "util.encodings".base64 | |
| 7 | |
| 8 local humane = require "util.serialization".serialize | |
| 9 | |
| 10 -- Configuration | |
| 11 local base = "openid" | |
| 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 :(" }; | |
| 14 | |
| 15 local associations = {} | |
| 16 | |
| 17 local function genkey(length) | |
| 18 -- FIXME not cryptographically secure | |
| 19 str = {} | |
|
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
3
diff
changeset
|
20 |
| 3 | 21 for i = 1,length do |
| 22 local rand = math.random(33, 126) | |
| 23 table.insert(str, string.char(rand)) | |
| 24 end | |
| 25 | |
| 26 return table.concat(str) | |
| 27 end | |
| 28 | |
| 29 local function tokvstring(dict) | |
| 30 -- key-value encoding for a dictionary [#4.1.3] | |
| 31 local str = "" | |
|
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
3
diff
changeset
|
32 |
| 3 | 33 for k,v in pairs(dict) do |
| 34 str = str..k..":"..v.."\n" | |
| 35 end | |
| 36 | |
| 37 return str | |
| 38 end | |
| 39 | |
| 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() | |
| 47 return handle | |
| 48 end | |
| 49 | |
| 50 local function split(str, sep) | |
| 51 local splits = {} | |
| 52 str:gsub("([^.."..sep.."]*)"..sep, function(c) table.insert(splits, c) end) | |
| 53 return splits | |
| 54 end | |
| 55 | |
| 56 local function sign(response, key) | |
| 57 local fields = {} | |
| 58 | |
| 59 for _,field in pairs(split(response["openid.signed"],",")) do | |
| 60 fields[field] = response["openid."..field] | |
| 61 end | |
| 62 | |
| 63 -- [#10.1] | |
| 64 return base64.encode(hmac.sha256(key, tokvstring(fields))) | |
| 65 end | |
| 66 | |
| 67 local function urlencode(s) | |
| 68 return (string.gsub(s, "%W", | |
| 69 function(str) | |
| 70 return string.format("%%%02X", string.byte(str)) | |
| 71 end)) | |
| 72 end | |
| 73 | |
| 74 local function urldecode(s) | |
| 75 return(string.gsub(string.gsub(s, "+", " "), "%%(%x%x)", | |
| 76 function(str) | |
| 77 return string.char(tonumber(str,16)) | |
| 78 end)) | |
| 79 end | |
| 80 | |
| 81 local function utctime() | |
| 82 local now = os.time() | |
| 83 local diff = os.difftime(now, os.time(os.date("!*t", now))) | |
| 84 return now-diff | |
| 85 end | |
| 86 | |
| 87 local function nonce() | |
| 88 -- generate a response nonce [#10.1] | |
| 89 local random = "" | |
| 90 for i=0,10 do | |
| 91 random = random..string.char(math.random(33,126)) | |
| 92 end | |
|
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
3
diff
changeset
|
93 |
| 3 | 94 local timestamp = os.date("%Y-%m-%dT%H:%M:%SZ", utctime()) |
| 95 | |
| 96 return timestamp..random | |
| 97 end | |
| 98 | |
| 99 local function query_params(query) | |
| 100 if type(query) == "string" and #query > 0 then | |
| 101 if query:match("=") then | |
| 102 local params = {} | |
| 103 for k, v in query:gmatch("&?([^=%?]+)=([^&%?]+)&?") do | |
| 104 if k and v then | |
| 105 params[urldecode(k)] = urldecode(v) | |
| 106 end | |
| 107 end | |
| 108 return params | |
| 109 else | |
| 110 return urldecode(query) | |
| 111 end | |
| 112 end | |
| 113 end | |
| 114 | |
| 115 local function split_host_port(combined) | |
| 116 local host = combined | |
| 117 local port = "" | |
| 118 local cpos = string.find(combined, ":") | |
| 119 if cpos ~= nil then | |
| 120 host = string.sub(combined, 0, cpos-1) | |
| 121 port = string.sub(combined, cpos+1) | |
| 122 end | |
| 123 | |
| 124 return host, port | |
| 125 end | |
| 126 | |
| 127 local function toquerystring(dict) | |
| 128 -- query string encoding for a dictionary [#4.1.3] | |
| 129 local str = "" | |
| 130 | |
| 131 for k,v in pairs(dict) do | |
| 132 str = str..urlencode(k).."="..urlencode(v).."&" | |
| 133 end | |
| 134 | |
| 135 return string.sub(str, 0, -1) | |
| 136 end | |
| 137 | |
| 138 local function match_realm(url, realm) | |
| 139 -- FIXME do actual match [#9.2] | |
| 140 return true | |
| 141 end | |
| 142 | |
| 143 local function handle_endpoint(method, body, request) | |
| 144 module:log("debug", "Request at OpenID provider endpoint") | |
|
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
3
diff
changeset
|
145 |
| 3 | 146 local params = nil |
| 147 | |
| 148 if method == "GET" then | |
| 149 params = query_params(request.url.query) | |
| 150 elseif method == "POST" then | |
| 151 params = query_params(body) | |
| 152 else | |
| 153 -- TODO error | |
| 154 return response_404 | |
| 155 end | |
|
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
3
diff
changeset
|
156 |
| 3 | 157 module:log("debug", "Request Parameters:\n"..humane(params)) |
| 158 | |
| 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 | |
| 164 | |
| 165 -- Error response [#8.2.4] | |
| 166 local openidresponse = { | |
| 167 ["ns"] = openidns, | |
| 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", | |
| 172 } | |
| 173 | |
| 174 local kvresponse = tokvstring(openidresponse) | |
| 175 module:log("debug", "OpenID Response:\n"..kvresponse) | |
| 176 return { | |
| 177 headers = { | |
| 178 ["Content-Type"] = "text/plain" | |
| 179 }, | |
| 180 body = kvresponse | |
| 181 } | |
| 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"] | |
| 188 else | |
| 189 -- neither was sent, error [#9.1] | |
| 190 -- FIXME return proper error | |
| 191 return response_404 | |
| 192 end | |
| 193 end | |
| 194 | |
| 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 | |
| 199 return response_404 | |
| 200 end | |
| 201 | |
| 202 -- Verify the return url [#9.2.1] | |
| 203 -- TODO implement return url verification | |
| 204 end | |
|
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
3
diff
changeset
|
205 |
| 3 | 206 if params["openid.claimed_id"] and params["openid.identity"] then |
| 207 -- asserting an identifier [#9.1] | |
| 208 | |
| 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"] | |
| 212 end | |
| 213 | |
| 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"/> | |
| 228 </form> | |
| 229 ]], params["openid.claimed_id"], params["openid.realm"], params["openid.return_to"], base, params["openid.return_to"]) | |
| 230 | |
| 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"> | |
| 235 <head> | |
| 236 <meta http-equiv="Content-type" content="text/html;charset=UTF-8" /> | |
| 237 %s | |
| 238 </head> | |
| 239 <body> | |
| 240 %s | |
| 241 </body> | |
| 242 </html> | |
| 243 ]], head, body) | |
| 244 elseif params["openid.mode"] == "checkid_immediate" then | |
| 245 -- Check ID Immediate mode [#9.3] | |
| 246 -- TODO implement check id immediate | |
| 247 end | |
| 248 else | |
| 249 -- not asserting an identifier [#9.1] | |
| 250 -- used for extensions | |
| 251 -- TODO implement common extensions | |
| 252 end | |
| 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"]) | |
| 260 | |
| 261 local is_valid = "false" | |
| 262 if sig == params["openid.sig"] then | |
| 263 is_valid = "true" | |
| 264 end | |
| 265 | |
| 266 module:log("debug", "Signature is: "..is_valid) | |
|
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
3
diff
changeset
|
267 |
| 3 | 268 openidresponse = { |
| 269 ns = openidns, | |
|
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
3
diff
changeset
|
270 is_valid = is_valid, |
| 3 | 271 } |
| 272 | |
| 273 -- Delete this association | |
| 274 associations[params["openid.assoc_handle"]] = nil | |
| 275 return { | |
| 276 headers = { | |
| 277 ["Content-Type"] = "text/plain" | |
| 278 }, | |
| 279 body = tokvstring(openidresponse), | |
| 280 } | |
| 281 else | |
| 282 module:log("debug", "No valid association") | |
| 283 -- TODO return error | |
| 284 -- Invalidate the handle [#11.4.2.2] | |
| 285 end | |
| 286 else | |
| 287 -- Some other mode | |
| 288 -- TODO error | |
| 289 end | |
| 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") | |
| 295 if valid then | |
| 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 = "" | |
| 303 if port == '' then | |
| 304 endpointurl = string.format("http://%s/%s", host, base) | |
| 305 else | |
| 306 endpointurl = string.format("http://%s:%s/%s", host, port, base) | |
| 307 end | |
|
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
3
diff
changeset
|
308 |
| 3 | 309 local nonce = nonce() |
| 310 local key = genkey(32) | |
| 311 local assoc_handle = newassoc(key) | |
| 312 | |
| 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, | |
| 324 } | |
| 325 | |
| 326 openidresponse["openid.sig"] = sign(openidresponse, key) | |
| 327 | |
| 328 queryresponse = toquerystring(openidresponse) | |
| 329 | |
| 330 redirecturl = params["openid.return_to"] | |
| 331 -- add the parameters to the return_to | |
| 332 if redirecturl:match("?") then | |
| 333 redirecturl = redirecturl.."&" | |
| 334 else | |
| 335 redirecturl = redirecturl.."?" | |
| 336 end | |
| 337 | |
| 338 redirecturl = redirecturl..queryresponse | |
| 339 | |
| 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) | |
| 343 return { | |
| 344 status = "303 See Other", | |
| 345 headers = { | |
| 346 Location = redirecturl, | |
| 347 }, | |
| 348 body = "Redirecting to: "..redirecturl -- TODO Include a note with a hyperlink to redirect | |
| 349 } | |
| 350 else | |
| 351 -- TODO Do something useful is there is no return_to | |
| 352 end | |
| 353 else | |
| 354 module:log("debug", "Authentication Failed: "..params["jid"]) | |
| 355 -- TODO let them try again | |
| 356 end | |
| 357 else | |
| 358 -- Not an Open ID request, do something useful | |
| 359 -- TODO | |
| 360 end | |
| 361 | |
| 362 return response_404 | |
| 363 end | |
| 364 | |
| 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) | |
| 368 | |
| 369 local user_name = "" | |
| 370 local user_domain = "" | |
| 371 local apos = string.find(id, "@") | |
| 372 if apos == nil then | |
| 373 user_name = id | |
| 374 user_domain = host | |
| 375 else | |
| 376 user_name = string.sub(id, 0, apos-1) | |
| 377 user_domain = string.sub(id, apos+1) | |
| 378 end | |
| 379 | |
| 380 user, domain = jidutil.split(id) | |
| 381 | |
| 382 local exists = usermanager.user_exists(user_name, user_domain) | |
|
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
3
diff
changeset
|
383 |
| 3 | 384 if not exists then |
|
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
3
diff
changeset
|
385 return response_404 |
| 3 | 386 end |
|
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
3
diff
changeset
|
387 |
| 3 | 388 local endpointurl = "" |
| 389 if port == '' then | |
| 390 endpointurl = string.format("http://%s/%s", host, base) | |
| 391 else | |
| 392 endpointurl = string.format("http://%s:%s/%s", host, port, base) | |
| 393 end | |
| 394 | |
| 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) | |
|
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
3
diff
changeset
|
398 |
| 3 | 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/>' | |
|
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
3
diff
changeset
|
406 |
| 3 | 407 local body = string.format('<p>%s</p>', content) |
|
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
3
diff
changeset
|
408 |
| 3 | 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"> | |
| 413 <head> | |
| 414 <meta http-equiv="Content-type" content="text/html;charset=UTF-8" /> | |
| 415 %s | |
| 416 </head> | |
| 417 <body> | |
| 418 %s | |
| 419 </body> | |
| 420 </html> | |
| 421 ]], head, body) | |
| 422 return data; | |
| 423 end | |
| 424 | |
| 425 local function handle_request(method, body, request) | |
| 426 module:log("debug", "Received request") | |
| 427 | |
| 428 -- Make sure the host is enabled | |
| 429 local host = split_host_port(request.headers.host) | |
| 430 if not hosts[host] then | |
| 431 return response_404 | |
| 432 end | |
| 433 | |
| 434 if request.url.path == "/"..base then | |
| 435 -- OpenID Provider Endpoint | |
| 436 return handle_endpoint(method, body, request) | |
| 437 else | |
| 438 local id = request.url.path:match("^/"..base.."/(.+)$") | |
| 439 if id then | |
| 440 -- OpenID Identifier | |
| 441 return handle_identifier(method, body, request, id) | |
| 442 else | |
| 443 return response_404 | |
| 444 end | |
| 445 end | |
| 446 end | |
| 447 | |
| 448 httpserver.new{ port = 5280, base = base, handler = handle_request, ssl = false} |