changeset 3037:bae7b0a002ef

mod_auth_http_cookie: Possibly temporary fork of mod_http_auth_async that adds cookie auth support
author Matthew Wild <mwild1@gmail.com>
date Thu, 24 May 2018 13:25:13 +0100
parents f7ebf8fcd602
children 48cbf6a3f112
files mod_auth_http_cookie/README.markdown mod_auth_http_cookie/mod_auth_http_cookie.lua
diffstat 2 files changed, 255 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /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
--- /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 "<unknown>", 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);