mod_http_oauth2/mod_http_oauth2.lua
changeset 5387 df11a2cbc7b7
parent 5386 12498c0d705f
child 5388 b40f29ec391a
--- a/mod_http_oauth2/mod_http_oauth2.lua	Sat Apr 29 11:26:04 2023 +0200
+++ b/mod_http_oauth2/mod_http_oauth2.lua	Sat Apr 29 13:09:46 2023 +0200
@@ -17,6 +17,10 @@
 local array = require "util.array";
 local st = require "util.stanza";
 
+local function b64url(s)
+	return (s:gsub("[+/=]", { ["+"] = "-", ["/"] = "_", ["="] = "" }))
+end
+
 local function read_file(base_path, fn, required)
 	local f, err = io.open(base_path .. "/" .. fn);
 	if not f then
@@ -69,6 +73,8 @@
 local registration_algo = module:get_option_string("oauth2_registration_algorithm", "HS256");
 local registration_options = module:get_option("oauth2_registration_options", { default_ttl = 60 * 60 * 24 * 90 });
 
+local pkce_required = module:get_option_boolean("oauth2_require_code_challenge", false);
+
 local verification_key;
 local jwt_sign, jwt_verify;
 if registration_key then
@@ -211,6 +217,7 @@
 
 local grant_type_handlers = {};
 local response_type_handlers = {};
+local verifier_transforms = {};
 
 function grant_type_handlers.password(params)
 	local request_jid = assert(params.username, oauth_error("invalid_request", "missing 'username' (JID)"));
@@ -236,12 +243,18 @@
 	end
 	local granted_scopes, granted_role = filter_scopes(request_username, params.scope);
 
+	if pkce_required and not params.code_challenge then
+		return oauth_error("invalid_request", "PKCE required");
+	end
+
 	local code = id.medium();
 	local ok = codes:set(params.client_id .. "#" .. code, {
 		expires = os.time() + 600;
 		granted_jid = granted_jid;
 		granted_scopes = granted_scopes;
 		granted_role = granted_role;
+		challenge = params.code_challenge;
+		challenge_method = params.code_challenge_method;
 		id_token = id_token;
 	});
 	if not ok then
@@ -340,6 +353,14 @@
 		return oauth_error("invalid_client", "incorrect credentials");
 	end
 
+	-- TODO Decide if the code should be removed or not when PKCE fails
+	local transform = verifier_transforms[code.challenge_method or "plain"];
+	if not transform then
+		return oauth_error("invalid_request", "unknown challenge transform method");
+	elseif transform(params.code_verifier) ~= code.challenge then
+		return oauth_error("invalid_grant", "incorrect credentials");
+	end
+
 	return json.encode(new_access_token(code.granted_jid, code.granted_role, code.granted_scopes, client, code.id_token));
 end
 
@@ -371,6 +392,18 @@
 	));
 end
 
+-- RFC 7636 Proof Key for Code Exchange by OAuth Public Clients
+
+function verifier_transforms.plain(code_verifier)
+	-- code_challenge = code_verifier
+	return code_verifier;
+end
+
+function verifier_transforms.S256(code_verifier)
+	-- code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
+	return code_verifier and b64url(hashes.SHA256(code_verifier));
+end
+
 -- Used to issue/verify short-lived tokens for the authorization process below
 local new_user_token, verify_user_token = jwt.init("HS256", random.bytes(32), nil, { default_ttl = 600 });
 
@@ -903,6 +936,7 @@
 				registration_endpoint = handle_register_request and module:http_url() .. "/register" or nil;
 				scopes_supported = usermanager.get_all_roles and array(it.keys(usermanager.get_all_roles(module.host))):append(array(openid_claims:items()));
 				response_types_supported = array(it.keys(response_type_handlers));
+				code_challenge_methods_supported = array(it.keys(verifier_transforms));
 				authorization_response_iss_parameter_supported = true;
 
 				-- OpenID