grim/prosody_mod_auth_jetbrains_hub

Initial revision

2019-10-19, Gary Kramlich
18e22b4ce5f4
Parents
Children 297bf63a92af
Initial revision
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/README.md Sat Oct 19 02:04:03 2019 -0500
@@ -0,0 +1,67 @@
+---
+labels:
+ - Stage-Alpha
+...
+
+# Introduction
+
+This is an experimental authentication modules that connects Prosody to
+[JetBrains Hub](https://www.jetbrains.com/hub/).
+
+# Details
+
+When a user attempts to authenticate to Prosody, this module will ask Hub if
+the user is allowed and if their credentials are correct.
+
+# Prerequisites
+
+To get this up and running you're going to need to get Hub itself up and
+running. Documentation for installation can be found
+[here](https://www.jetbrains.com/help/hub/installation-and-upgrade.html).
+
+Once you Hub installation is up and running we need to create a new service
+in Hub that will allows us to query it. To start you'll need to access the
+services page of your Hub instance. If you Hub is running on `hub.example.com`
+you can find it at `https://hub.example.com/hub/services`.
+
+Once you're on the services page, go ahead and click `New service...`. You
+really only need to set a name for this service, but adding a `Home URL` will
+make it so people can access it from the Hub interface. But that's really only
+necessary if you have some HTTP setup in your Prosody install.
+
+Once you've create the service all we need to talk to it is the `ID` of the
+client and it's secret. When the service is created it has a random secret
+that we don't know, so go ahead and click the `Change...` button next to
+`Secret`. Please note, that this is the only time that Hub will show the
+secret to you, so please write it down.
+
+Now that the service is configured in Hub we can go ahead and configure the
+Prosody module to talk to it.
+
+# Configuration
+
+``` lua
+VirtualHost "example.com"
+authentication = "jetbrains_hub"
+hub_url = "https://hub.example.com/hub"
+hub_scopes = "0-0-0-0-0"
+hub_client_id = "Client ID"
+hub_client_secret = "Client Secret Key"
+```
+
+`hub_url` is the url to the root of your Hub installation. In the example
+above hub is running on HTTPS at `hub.example.com` with it's normal path of
+`/hub`.
+
+`hub_scopes` is the ID of the Hub service itself. In my experience this is
+always `0-0-0-0-0` but you can double check by going to
+`https://hub.example.com/hub/services/jetbrains-hub-service`. You want the
+value from the `ID` field.
+
+`hub_client_id` and `hub_client_secret` are the values for the service ID and
+secrete that were created in the prerequisites section.
+
+# Compatibility
+
+Requires Prosody trunk.
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_auth_jetbrains_hub.lua Sat Oct 19 02:04:03 2019 -0500
@@ -0,0 +1,149 @@
+-- Prosody IM
+-- Copyright (C) 2008-2013 Matthew Wild
+-- Copyright (C) 2008-2013 Waqas Hussain
+-- Copyright (C) 2014 Kim Alvefur
+-- Copyright (C) 2019 Gary Kramlich
+--
+-- 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 log = module._log;
+local host = module.host;
+
+local hub_url = module:get_option_string("jetbrains_hub_url", ""):gsub("$host", host);
+if hub_url == "" then error("jetbrains_hub_url required") end
+
+local hub_scopes = module:get_option_string("jetbrains_hub_scopes", "");
+if hub_scopes == "" then error("jetbrains_hub_scopes required") end
+
+local hub_client_id = module:get_option_string("jetbrains_hub_client_id", "");
+if hub_client_id == "" then error("jetbrains_hub_client_id required") end
+
+local hub_client_secret = module:get_option_string("jetbrains_hub_client_secret", "");
+if hub_client_secret == "" then error("jetbrains_hub_client_secret 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_auth(url, username, password)
+ module:log("debug", "async_http_auth()");
+ local http = require "net.http";
+ local wait, done = async.waiter();
+ local content, code, request, response;
+ local body = {
+ { name = "grant_type", value = "password" };
+ { name = "username", value = username };
+ { name = "password", value = password };
+ { name = "scope", value = hub_scopes };
+ }
+ local ex = {
+ headers = { Authorization = "Basic "..base64(hub_client_id..":"..hub_client_secret); };
+ body = http.formencode(body);
+ }
+ local function cb(content_, code_, request_, response_)
+ content, code, request, response = content_, code_, request_, response_;
+ done();
+ end
+ http.request(url, ex, cb);
+ wait();
+ if code >= 200 and code <= 299 then
+ module:log("debug", "HTTP auth provider confirmed valid password");
+ return true;
+ else
+ module:log("debug", "HTTP auth provider returned status code %d", code);
+ end
+ return nil, "Auth failed. Invalid username or password.";
+end
+
+local function sync_http_auth(url,username, password)
+ 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 = {
+ { name = "grant_type", value = "password" };
+ { name = "username", value = username };
+ { name = "password", value = password };
+ { name = "scope", value = hub_scopes };
+ }
+ local _, code, headers, status = request{
+ url = url,
+ headers = { Authorization = "Basic "..base64(hub_client_id..":"..hub_client_secret); },
+ body = http.formencode(body)
+ };
+ if type(code) == "number" and code >= 200 and code <= 299 then
+ module:log("debug", "HTTP auth provider confirmed valid password");
+ return true;
+ else
+ module:log("debug", "HTTP auth provider returned status code: "..code);
+ end
+ return nil, "Auth failed. Invalid username or password.";
+end
+
+function provider.test_password(username, password)
+ local url = hub_url .. "/api/rest/oauth2/token"
+ log("debug", "Testing password for user %s at host %s with URL %s", username, host, url);
+ if (have_async) then
+ return async_http_auth(url, username, password);
+ else
+ return sync_http_auth(url, username, password);
+ end
+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
+
+function provider.get_sasl_handler()
+ return new_sasl(host, {
+ plain_test = function(sasl, username, password, realm)
+ return provider.test_password(username, password), true;
+ end
+ });
+end
+
+module:provides("auth", provider);