changeset 4157:93b12bfd7aa8

mod_auth_http: Yet another module to authenticate against a HTTP service
author Matthew Wild <mwild1@gmail.com>
date Wed, 30 Sep 2020 13:20:11 +0100
parents b79904446d9e
children df1e0465ff81
files mod_auth_http/README.markdown mod_auth_http/mod_auth_http.lua
diffstat 2 files changed, 249 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_auth_http/README.markdown	Wed Sep 30 13:20:11 2020 +0100
@@ -0,0 +1,127 @@
+---
+labels:
+- Stage-Alpha
+summary: "Authenticate users against an external HTTP API"
+...
+
+# Overview
+
+This authentication module allows Prosody to authenticate users against
+an external HTTP service.
+
+# Configuration
+
+``` lua
+VirtualHost "example.com"
+  authentication = "http"
+  http_auth_url = "http://example.com/auth"
+```
+
+If the API requires Prosody to authenticate, you can provide static
+credentials using HTTP Basic authentication, like so:
+
+```
+http_auth_credentials = "prosody:secret-password"
+```
+
+# Developers
+
+This 
+
+## Protocol
+
+Prosody will make a HTTP request to the configured API URL with an
+appended `/METHOD` where `METHOD` is one of the methods described below.
+
+GET methods must expect a series of URL-encoded query parameters, while
+POST requests will receive an URL-encoded form (i.e.
+`application/x-www-form-urlencoded`).
+
+## Parameters
+
+user
+: The username, e.g. `stephanie` for the JID `stephanie@example.com`.
+
+server
+: The host part of the user's JID, e.g. `example.com` for the JID
+  `stephanie@example.com`.
+
+pass
+: For methods that verify or set a user's password, the password will
+  be supplied in this parameter, otherwise it is not set.
+
+## Methods
+
+The only mandatory methods that the service must implement are `check_password`
+and `user_exists`. Unsupported methods should return a HTTP status code
+of `501 Not Implemented`, but other error codes will also be handled by
+Prosody.
+
+### register
+
+**HTTP method:**
+: POST
+
+**Success codes:**
+: 201
+
+**Error codes:**
+: 409 (user exists)
+
+### check_password
+
+**HTTP method:**
+: GET
+
+**Success codes:**
+: 200
+
+**Response:**
+: A text string of `true` if the user exists, or `false` otherwise.
+
+### user_exists
+
+**HTTP method:**
+: GET
+
+**Success codes:**
+: 200
+
+**Response:**
+: A text string of `true` if the user exists, or `false` otherwise.
+
+### set_password
+
+**HTTP method:**
+: POST
+
+**Success codes:**
+: 200, 201, or 204
+
+### remove_user
+
+**HTTP method:**
+: POST
+
+**Success codes:**
+: 200, 201 or 204
+
+## Examples
+
+With the following configuration:
+
+```
+authentication = "http"
+http_auth_url = "https://auth.example.net/api"
+
+If a user connects and tries to log in to Prosody as "romeo@example.net"
+with the password "iheartjuliet", Prosody would make the following HTTP
+request:
+
+```
+https://auth.example.net/api/check_password?user=romeo&server=example.net&pass=iheartjuliet
+```
+
+# Compatibility
+
+Requires Prosody 0.11.0 or later.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_auth_http/mod_auth_http.lua	Wed Sep 30 13:20:11 2020 +0100
@@ -0,0 +1,122 @@
+-- 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 http = require "net.http";
+
+if not have_async then
+	error("Your version of Prosody does not support async and is incompatible");
+end
+
+local host = module.host;
+
+local api_base = module:get_option_string("http_auth_url",  ""):gsub("$host", host);
+if api_base == "" then error("http_auth_url required") end
+api_base = api_base:gsub("/$", "");
+
+local auth_creds = module:get_option_string("http_auth_credentials");
+
+local method_types = {
+	-- Unlisted methods default to GET
+	register = "POST";
+	set_password = "POST";
+	remove_user = "POST";
+};
+
+local provider = {};
+
+local function make_request(method_name, params)
+	local wait, done = async.waiter();
+
+	local method_type = method_types[method_name] or "GET";
+
+	params.server = params.server or host;
+	local encoded_params = http.formencode(params);
+
+	local url;
+	local ex = {
+		method = method_type;
+		headers = { Authorization = auth_creds and ("Basic "..base64(auth_creds)) or nil; };
+	}
+	if method_type == "POST" then
+		url = api_base.."/"..method_name;
+		ex.headers["Content-Type"] = "application/x/www-form-urlencoded";
+		ex.body = encoded_params;
+	else
+		url = api_base.."/"..method_name.."?"..encoded_params;
+	end
+
+	local content, code;
+	local function cb(content_, code_)
+		content, code = content_, code_;
+		done();
+	end
+	http.request(url, ex, cb);
+	wait();
+	return code, content;
+end
+
+function provider.test_password(username, password)
+	local code, body = make_request("check_password", { user = username, pass = password });
+	if code == 200 and body == "true" then
+		return true;
+	end
+	return false;
+end
+
+function provider.users()
+	return function()
+		return nil;
+	end
+end
+
+function provider.set_password(username, password)
+	local code = make_request("set_password", { user = username, pass = password });
+	if code == 200 or code == 201 or code == 204 then
+		return true;
+	end
+	return false;
+end
+
+function provider.user_exists(username)
+	local code, body = make_request("user_exists", { user = username });
+	if code == 200 and body == "true" then
+		return true;
+	end
+	return false;
+end
+
+function provider.create_user(username, password)
+	local code = make_request("register", { user = username, pass = password });
+	if code == 201 then
+		return true;
+	end
+	return false;
+end
+
+function provider.delete_user(username)
+	local code = make_request("remove_user", { user = username });
+	if code == 200 or code == 201 or code == 204 then
+		return true;
+	end
+	return false;
+end
+
+function provider.get_sasl_handler()
+	return new_sasl(host, {
+		--luacheck: ignore 212/sasl 212/realm
+		plain_test = function(sasl, username, password, realm)
+			return provider.test_password(username, password), true;
+		end;
+	});
+end
+
+module:provides("auth", provider);