# HG changeset patch # User Matthew Wild # Date 1527164713 -3600 # Node ID bae7b0a002efa980602ccb9fe49beee082f8978b # Parent f7ebf8fcd602ab584e745022a31f51fc513651ee mod_auth_http_cookie: Possibly temporary fork of mod_http_auth_async that adds cookie auth support diff -r f7ebf8fcd602 -r bae7b0a002ef mod_auth_http_cookie/README.markdown --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_auth_http_cookie/README.markdown Thu May 24 13:25:13 2018 +0100 @@ -0,0 +1,64 @@ +--- +labels: +- Stage-Alpha +... + +Introduction +============ + +This is an experimental authentication module that does an asynchronous +HTTP call to verify username and password. + +This is a (possibly temporary) fork of mod_http_auth_async that adds +support for authentication using a cookie and SASL EXTERNAL. + +Details +======= + +When a user attempts to authenticate to Prosody, this module takes the +username and password and does a HTTP GET request with [Basic +authentication][rfc7617] to the configured `http_auth_url`. + +Configuration +============= + +``` lua +VirtualHost "example.com" + authentication = "http_auth_cookie" + http_auth_url = "http://example.com/auth" + http_cookie_auth_url = "https://example.com/testcookie.php?user=$user" +``` + +Cookie Authentication +===================== + +It is possible to link authentication to an existing web application. This +has the benefit that the user logging into the web application in their +browser will automatically log them into their XMPP account. + +There are some prerequisites for this to work: + + - The BOSH or Websocket requests must include the application's cookie in + the headers sent to Prosody. This typically means the web chat code needs + to be served from the same domain as the web application. + + - The web application must have a URL that returns 200 OK when called with + a valid cookie, and returns a different status code if the cookie is invalid + or not currently logged in. + + - The XMPP username for the user must be passed to Prosody by the client, or + returned in the 200 response from the web application. + +Set `http_cookie_auth_url` to the web application URL that is used to check the +cookie. You may use the variables `$host` for the XMPP host and `$user` for the +XMPP username. + +If the `$user` variable is included in the URL, the client must provide the username +via the "authzid" in the SASL EXTERNAL authentication mechanism. + +If the `$user` variable is *not* included in the URL, Prosody expects the web application's response to be the username instead, as UTF-8 text/plain. + +Compatibility +============= + +Requires Prosody trunk diff -r f7ebf8fcd602 -r bae7b0a002ef mod_auth_http_cookie/mod_auth_http_cookie.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_auth_http_cookie/mod_auth_http_cookie.lua Thu May 24 13:25:13 2018 +0100 @@ -0,0 +1,191 @@ +-- Prosody IM +-- Copyright (C) 2008-2013 Matthew Wild +-- Copyright (C) 2008-2013 Waqas Hussain +-- Copyright (C) 2014 Kim Alvefur +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local new_sasl = require "util.sasl".new; +local base64 = require "util.encodings".base64.encode; +local have_async, async = pcall(require, "util.async"); + +local nodeprep = require "util.encodings".stringprep.nodeprep; + +local log = module._log; +local host = module.host; + +local password_auth_url = module:get_option_string("http_auth_url", ""):gsub("$host", host); + +local cookie_auth_url = module:get_option_string("http_cookie_auth_url"); +if cookie_auth_url then + cookie_auth_url = cookie_auth_url:gsub("$host", host); +end + +local external_needs_authzid = cookie_auth_url and cookie_auth_url:match("$user"); + +if password_auth_url == "" and not cookie_auth_url then error("http_auth_url or http_cookie_auth_url required") end + + +local provider = {}; + +-- globals required by socket.http +if rawget(_G, "PROXY") == nil then + rawset(_G, "PROXY", false) +end +if rawget(_G, "base_parsed") == nil then + rawset(_G, "base_parsed", false) +end +if not have_async then -- FINE! Set your globals then + prosody.unlock_globals() + require "ltn12" + require "socket" + require "socket.http" + require "ssl.https" + prosody.lock_globals() +end + +local function async_http_request(url, headers) + module:log("debug", "async_http_auth()"); + local http = require "net.http"; + local wait, done = async.waiter(); + local content, code, request, response; + local ex = { + headers = headers; + } + local function cb(content_, code_, request_, response_) + content, code, request, response = content_, code_, request_, response_; + done(); + end + http.request(url, ex, cb); + wait(); + log("debug", "response code %s", tostring(code)); + if code >= 200 and code <= 299 then + return true, content; + end + return nil; +end + +local function sync_http_request(url, headers) + module:log("debug", "sync_http_auth()"); + require "ltn12"; + local http = require "socket.http"; + local https = require "ssl.https"; + local request; + if string.sub(url, 1, string.len('https')) == 'https' then + request = https.request; + else + request = http.request; + end + local body_chunks = {}; + local _, code, headers, status = request{ + url = url, + headers = headers; + sink = ltn12.sink.table(body_chunks); + }; + log("debug", "response code %s %s", type(code), tostring(code)); + if type(code) == "number" and code >= 200 and code <= 299 then + log("debug", "success") + return true, table.concat(body_chunks); + end + return nil; +end + +local http_request = have_async and async_http_request or sync_http_request; + +function http_test_password(username, password) + local url = password_auth_url:gsub("$user", username):gsub("$password", password); + log("debug", "Testing password for user %s at host %s with URL %s", username, host, url); + local ok = (http_request(url, { Authorization = "Basic "..base64(username..":"..password); })); + if not ok then + return nil, "not authorized"; + end + return true; +end + +function http_test_cookie(cookie, username) + local url = external_needs_authzid and cookie_auth_url:gsub("$user", username) or cookie_auth_url; + log("debug", "Testing cookie auth for user %s at host %s with URL %s", username or "", host, url); + local ok, resp = http_request(url, { Cookie = cookie; }); + if not ok then + return nil, "not authorized"; + end + + return external_needs_authzid or resp; +end + +function provider.test_password(username, password) + return http_test_password(username, password); +end + +function provider.users() + return function() + return nil; + end +end + +function provider.set_password(username, password) + return nil, "Changing passwords not supported"; +end + +function provider.user_exists(username) + return true; +end + +function provider.create_user(username, password) + return nil, "User creation not supported"; +end + +function provider.delete_user(username) + return nil , "User deletion not supported"; +end + +local function get_session_cookies(session) + local response = session.conn._http_open_response; + local request = response and response.request; + if request then + return request.headers.cookie; + end +end + +function provider.get_sasl_handler(session) + local cookie = cookie_auth_url and get_session_cookies(session); + log("debug", "Request cookie: %s", cookie); + return new_sasl(host, { + plain_test = function(sasl, username, password, realm) + return provider.test_password(username, password), true; + end; + external = cookie and function (authzid) + if external_needs_authzid then + -- Authorize the username provided by the client, using request cookie + if authzid ~= "" then + module:log("warn", "Client requested authzid, but cookie auth URL does not contain $user variable"); + return nil; + end + local success = http_test_cookie(cookie); + if not success then + return nil; + end + return nodeprep(authzid), true; + else + -- Authorize client using request cookie, username comes from auth server + if authzid == "" then + module:log("warn", "Client did not provide authzid, but cookie auth URL contains $user variable"); + return nil; + end + local unprepped_username = http_test_cookie(cookie, nodeprep(authzid)); + local username = nodeprep(unprepped_username); + if not username then + if unprepped_username then + log("warn", "Username supplied by cookie_auth_url is not valid for XMPP"); + end + return nil; + end + return username, true; + end; + end; + }); +end + +module:provides("auth", provider);