# HG changeset patch # User JC Brand # Date 1528909789 0 # Node ID 73ada978dabc58e3056284379aa313ed75752cf4 # Parent 8298b06e6603c9d9a35d85b409c36fe640f1213b mod_sasl_oauthbearer and mod_auth_oauthbearer Two new modules for logging in with OAuth tokens. diff -r 8298b06e6603 -r 73ada978dabc mod_auth_oauthbearer/README.markdown --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_auth_oauthbearer/README.markdown Wed Jun 13 17:09:49 2018 +0000 @@ -0,0 +1,24 @@ +--- +labels: +- 'Type-Auth' +summary: OAuth authentication +... + +Introduction +============ + +This is an authentication module for the SASL OAUTHBEARER mechanism, as provided by `mod_sasl_oauthbearer`. + +Configuration +============= + +Per VirtualHost, you'll need to supply your OAuth client Id, secret and the URL which +Prosody must call in order to verify the OAuth token it receives from the XMPP client. + +For example, for Github: + + oauth_client_id = "13f8e9cc8928b3409822" + oauth_client_secret = "983161fd3ah608ea7ef35382668aad1927463978" + oauth_url = "https://api.github.com/applications/{{oauth_client_id}}/tokens/{{password}}"; + + authentication = "oauthbearer" diff -r 8298b06e6603 -r 73ada978dabc mod_auth_oauthbearer/mod_auth_oauthbearer.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_auth_oauthbearer/mod_auth_oauthbearer.lua Wed Jun 13 17:09:49 2018 +0000 @@ -0,0 +1,90 @@ +local host = module.host; +local log = module._log; +local new_sasl = require "util.sasl".new; +local base64 = require "util.encodings".base64.encode; + +local provider = {}; + +local oauth_client_id = module:get_option_string("oauth_client_id", ""); +local oauth_client_secret = module:get_option_string("oauth_client_secret", ""); +local oauth_url = module:get_option_string("oauth_url", ""); + +if oauth_client_id == "" then error("oauth_client_id required") end +if oauth_client_secret == "" then error("oauth_client_secret required") end +if oauth_url == "" then error("oauth_url required") end + +-- 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 + +local function interp(s, tab) + -- String interpolation, so that we can make the oauth_url configurable + -- e.g. oauth_url = "https://api.github.com/applications/{{oauth_client_id}}/tokens/{{password}}"; + -- + -- See: http://lua-users.org/wiki/StringInterpolation + return (s:gsub('(%b{})', function(w) return tab[w:sub(3, -3)] or w end)) +end + +function provider.test_password(sasl, username, password, realm) + log("debug", "Testing signed OAuth2 for user %s at realm %s", username, realm); + -- TODO: determine, based on the "realm" which OAuth provider to verify with. + module:log("debug", "sync_http_auth()"); + local https = require "ssl.https"; + local url = interp(oauth_url, {oauth_client_id = oauth_client_id, password = password}); + + module:log("debug", "The URL is: "..url); + local _, code, headers, status = https.request{ + url = url, + headers = { + Authorization = "Basic "..base64(oauth_client_id..":"..oauth_client_secret); + } + }; + if type(code) == "number" and code >= 200 and code <= 299 then + module:log("debug", "OAuth provider confirmed valid password"); + return 'johnny', true; + else + module:log("warn", "OAuth provider returned status code: "..code); + end + module:log("warn", "OAuth failed. Invalid username or password."); + return nil, false; +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() + local supported_mechanisms = {}; + supported_mechanisms["OAUTHBEARER"] = true; + + return new_sasl(host, { + oauthbearer = function(sasl, username, password, realm) + return provider.test_password(sasl, username, password, realm); + end, + mechanisms = supported_mechanisms + }); +end + +module:provides("auth", provider); diff -r 8298b06e6603 -r 73ada978dabc mod_sasl_oauthbearer/README.markdown --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_sasl_oauthbearer/README.markdown Wed Jun 13 17:09:49 2018 +0000 @@ -0,0 +1,12 @@ +--- +labels: +- 'Type-Auth' +summary: SASL OAuthBearer Mechanism +... + +Introduction +============ + +This module adds a new SASL mechanism OAUTHBEARER, as defined in [RFC-7628](https://tools.ietf.org/html/rfc7628). + +It's intended to be used together with the `mod_auth_oauthbearer.lua` module. diff -r 8298b06e6603 -r 73ada978dabc mod_sasl_oauthbearer/mod_sasl_oauthbearer.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_sasl_oauthbearer/mod_sasl_oauthbearer.lua Wed Jun 13 17:09:49 2018 +0000 @@ -0,0 +1,50 @@ +local s_match = string.match; +local registerMechanism = require "util.sasl".registerMechanism; +local saslprep = require "util.encodings".stringprep.saslprep; +local nodeprep = require "util.encodings".stringprep.nodeprep; +local log = require "util.logger".init("sasl"); +local _ENV = nil; + + +local function oauthbearer(self, message) + if not message then + return "failure", "malformed-request"; + end + + local authorization, password = s_match(message, "^n,a=([^,]*),\1auth=Bearer ([^\1]+)"); + if not authorization then + return "failure", "malformed-request"; + end + + local authentication = s_match(authorization, "(.-)@.*"); + + -- SASLprep password and authentication + authentication = saslprep(authentication); + password = saslprep(password); + + if (not password) or (password == "") or (not authentication) or (authentication == "") then + log("debug", "Username or password violates SASLprep."); + return "failure", "malformed-request", "Invalid username or password."; + end + + local _nodeprep = self.profile.nodeprep; + if _nodeprep ~= false then + authentication = (_nodeprep or nodeprep)(authentication); + if not authentication or authentication == "" then + return "failure", "malformed-request", "Invalid username or password." + end + end + + local correct, state = false, false; + correct, state = self.profile.oauthbearer(self, authentication, password, self.realm); + + self.username = authentication + if state == false then + return "failure", "account-disabled"; + elseif state == nil or not correct then + return "failure", "not-authorized", "Unable to authorize you with the authentication credentials you've sent."; + end + return "success"; +end + +registerMechanism("OAUTHBEARER", {"oauthbearer"}, oauthbearer);