util.sasl: Add SASL OAUTHBEARER mechanism (RFC 7628)
authorMatthew Wild <mwild1@gmail.com>
Wed, 01 Mar 2023 12:55:00 +0000
changeset 12915 ab1164eda011
parent 12914 5c90862e39aa
child 12916 44a78985471f
util.sasl: Add SASL OAUTHBEARER mechanism (RFC 7628)
util/sasl.lua
util/sasl/oauthbearer.lua
--- a/util/sasl.lua	Thu Feb 23 18:39:02 2023 +0100
+++ b/util/sasl.lua	Wed Mar 01 12:55:00 2023 +0000
@@ -133,10 +133,11 @@
 end
 
 -- load the mechanisms
-require "util.sasl.plain"     .init(registerMechanism);
-require "util.sasl.anonymous" .init(registerMechanism);
-require "util.sasl.scram"     .init(registerMechanism);
-require "util.sasl.external"  .init(registerMechanism);
+require "util.sasl.plain"       .init(registerMechanism);
+require "util.sasl.anonymous"   .init(registerMechanism);
+require "util.sasl.oauthbearer" .init(registerMechanism);
+require "util.sasl.scram"       .init(registerMechanism);
+require "util.sasl.external"    .init(registerMechanism);
 
 return {
 	registerMechanism = registerMechanism;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/sasl/oauthbearer.lua	Wed Mar 01 12:55:00 2023 +0000
@@ -0,0 +1,83 @@
+local saslprep = require "util.encodings".stringprep.saslprep;
+local nodeprep = require "util.encodings".stringprep.nodeprep;
+local jid = require "util.jid";
+local json = require "util.json";
+local log = require "util.logger".init("sasl");
+local _ENV = nil;
+
+
+local function oauthbearer(self, message)
+	if not message then
+		return "failure", "malformed-request";
+	end
+
+	if message == "\001" then
+		return "failure", "not-authorized";
+	end
+
+	local gs2_authzid, kvpairs = message:match("n,a=([^,]+),(.+)$");
+	if not gs2_authzid then
+		return "failure", "malformed-request";
+	end
+
+	local auth_header;
+	for k, v in kvpairs:gmatch("([a-zA-Z]+)=([\033-\126 \009\r\n]*)\001") do
+		if k == "auth" then
+			auth_header = v;
+			break;
+		end
+	end
+
+	if not auth_header then
+		return "failure", "malformed-request";
+	end
+
+	local username = jid.prepped_split(gs2_authzid);
+
+	-- SASLprep username
+	username = saslprep(username);
+
+	if not username or username == "" then
+		log("debug", "Username violates SASLprep.");
+		return "failure", "malformed-request", "Invalid username.";
+	end
+
+	local _nodeprep = self.profile.nodeprep;
+	if _nodeprep ~= false then
+		username = (_nodeprep or nodeprep)(username);
+		if not username or username == "" then
+			return "failure", "malformed-request", "Invalid username or password."
+		end
+	end
+
+	self.username = username;
+
+	local token = auth_header:match("^Bearer (.+)$");
+
+	local correct, state, token_info = self.profile.oauthbearer(self, username, token, self.realm);
+
+	if state == false then
+		return "failure", "account-disabled";
+	elseif state == nil or not correct then
+		-- For token-level errors, RFC 7628 demands use of a JSON-encoded
+		-- challenge response upon failure. We relay additional info from
+		-- the auth backend if available.
+		return "challenge", json.encode({
+			status = token_info and token_info.status or "invalid_token";
+			scope = token_info and token_info.scope or nil;
+			["openid-configuration"] = token_info and token_info.oidc_discovery_url or nil;
+		});
+	end
+
+	self.resource = token_info.resource;
+	self.role = token_info.role;
+	return "success";
+end
+
+local function init(registerMechanism)
+	registerMechanism("OAUTHBEARER", {"oauthbearer"}, oauthbearer);
+end
+
+return {
+	init = init;
+}