comparison mod_http_oauth2/mod_http_oauth2.lua @ 5383:df11a2cbc7b7

mod_http_oauth2: Implement RFC 7628 Proof Key for Code Exchange Likely to become mandatory in OAuth 2.1. Backwards compatible since the default 'plain' verifier would compare nil with nil if the relevant parameters are left out.
author Kim Alvefur <zash@zash.se>
date Sat, 29 Apr 2023 13:09:46 +0200
parents 12498c0d705f
children b40f29ec391a
comparison
equal deleted inserted replaced
5382:12498c0d705f 5383:df11a2cbc7b7
15 local jwt = require"util.jwt"; 15 local jwt = require"util.jwt";
16 local it = require "util.iterators"; 16 local it = require "util.iterators";
17 local array = require "util.array"; 17 local array = require "util.array";
18 local st = require "util.stanza"; 18 local st = require "util.stanza";
19 19
20 local function b64url(s)
21 return (s:gsub("[+/=]", { ["+"] = "-", ["/"] = "_", ["="] = "" }))
22 end
23
20 local function read_file(base_path, fn, required) 24 local function read_file(base_path, fn, required)
21 local f, err = io.open(base_path .. "/" .. fn); 25 local f, err = io.open(base_path .. "/" .. fn);
22 if not f then 26 if not f then
23 module:log(required and "error" or "debug", "Unable to load template file: %s", err); 27 module:log(required and "error" or "debug", "Unable to load template file: %s", err);
24 if required then 28 if required then
67 -- Used to derive client_secret from client_id, set to enable stateless dynamic registration. 71 -- Used to derive client_secret from client_id, set to enable stateless dynamic registration.
68 local registration_key = module:get_option_string("oauth2_registration_key"); 72 local registration_key = module:get_option_string("oauth2_registration_key");
69 local registration_algo = module:get_option_string("oauth2_registration_algorithm", "HS256"); 73 local registration_algo = module:get_option_string("oauth2_registration_algorithm", "HS256");
70 local registration_options = module:get_option("oauth2_registration_options", { default_ttl = 60 * 60 * 24 * 90 }); 74 local registration_options = module:get_option("oauth2_registration_options", { default_ttl = 60 * 60 * 24 * 90 });
71 75
76 local pkce_required = module:get_option_boolean("oauth2_require_code_challenge", false);
77
72 local verification_key; 78 local verification_key;
73 local jwt_sign, jwt_verify; 79 local jwt_sign, jwt_verify;
74 if registration_key then 80 if registration_key then
75 -- Tie it to the host if global 81 -- Tie it to the host if global
76 verification_key = hashes.hmac_sha256(registration_key, module.host); 82 verification_key = hashes.hmac_sha256(registration_key, module.host);
209 end 215 end
210 end 216 end
211 217
212 local grant_type_handlers = {}; 218 local grant_type_handlers = {};
213 local response_type_handlers = {}; 219 local response_type_handlers = {};
220 local verifier_transforms = {};
214 221
215 function grant_type_handlers.password(params) 222 function grant_type_handlers.password(params)
216 local request_jid = assert(params.username, oauth_error("invalid_request", "missing 'username' (JID)")); 223 local request_jid = assert(params.username, oauth_error("invalid_request", "missing 'username' (JID)"));
217 local request_password = assert(params.password, oauth_error("invalid_request", "missing 'password'")); 224 local request_password = assert(params.password, oauth_error("invalid_request", "missing 'password'"));
218 local request_username, request_host, request_resource = jid.prepped_split(request_jid); 225 local request_username, request_host, request_resource = jid.prepped_split(request_jid);
233 local request_username, request_host = jid.split(granted_jid); 240 local request_username, request_host = jid.split(granted_jid);
234 if not request_host or request_host ~= module.host then 241 if not request_host or request_host ~= module.host then
235 return oauth_error("invalid_request", "invalid JID"); 242 return oauth_error("invalid_request", "invalid JID");
236 end 243 end
237 local granted_scopes, granted_role = filter_scopes(request_username, params.scope); 244 local granted_scopes, granted_role = filter_scopes(request_username, params.scope);
245
246 if pkce_required and not params.code_challenge then
247 return oauth_error("invalid_request", "PKCE required");
248 end
238 249
239 local code = id.medium(); 250 local code = id.medium();
240 local ok = codes:set(params.client_id .. "#" .. code, { 251 local ok = codes:set(params.client_id .. "#" .. code, {
241 expires = os.time() + 600; 252 expires = os.time() + 600;
242 granted_jid = granted_jid; 253 granted_jid = granted_jid;
243 granted_scopes = granted_scopes; 254 granted_scopes = granted_scopes;
244 granted_role = granted_role; 255 granted_role = granted_role;
256 challenge = params.code_challenge;
257 challenge_method = params.code_challenge_method;
245 id_token = id_token; 258 id_token = id_token;
246 }); 259 });
247 if not ok then 260 if not ok then
248 return {status_code = 429}; 261 return {status_code = 429};
249 end 262 end
338 if not code or type(code) ~= "table" or code_expired(code) then 351 if not code or type(code) ~= "table" or code_expired(code) then
339 module:log("debug", "authorization_code invalid or expired: %q", code); 352 module:log("debug", "authorization_code invalid or expired: %q", code);
340 return oauth_error("invalid_client", "incorrect credentials"); 353 return oauth_error("invalid_client", "incorrect credentials");
341 end 354 end
342 355
356 -- TODO Decide if the code should be removed or not when PKCE fails
357 local transform = verifier_transforms[code.challenge_method or "plain"];
358 if not transform then
359 return oauth_error("invalid_request", "unknown challenge transform method");
360 elseif transform(params.code_verifier) ~= code.challenge then
361 return oauth_error("invalid_grant", "incorrect credentials");
362 end
363
343 return json.encode(new_access_token(code.granted_jid, code.granted_role, code.granted_scopes, client, code.id_token)); 364 return json.encode(new_access_token(code.granted_jid, code.granted_role, code.granted_scopes, client, code.id_token));
344 end 365 end
345 366
346 function grant_type_handlers.refresh_token(params) 367 function grant_type_handlers.refresh_token(params)
347 if not params.client_id then return oauth_error("invalid_request", "missing 'client_id'"); end 368 if not params.client_id then return oauth_error("invalid_request", "missing 'client_id'"); end
367 refresh_token_info.token = params.refresh_token; 388 refresh_token_info.token = params.refresh_token;
368 389
369 return json.encode(new_access_token( 390 return json.encode(new_access_token(
370 refresh_token_info.jid, refresh_token_info.role, refresh_token_info.grant.data.oauth2_scopes, client, nil, refresh_token_info 391 refresh_token_info.jid, refresh_token_info.role, refresh_token_info.grant.data.oauth2_scopes, client, nil, refresh_token_info
371 )); 392 ));
393 end
394
395 -- RFC 7636 Proof Key for Code Exchange by OAuth Public Clients
396
397 function verifier_transforms.plain(code_verifier)
398 -- code_challenge = code_verifier
399 return code_verifier;
400 end
401
402 function verifier_transforms.S256(code_verifier)
403 -- code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
404 return code_verifier and b64url(hashes.SHA256(code_verifier));
372 end 405 end
373 406
374 -- Used to issue/verify short-lived tokens for the authorization process below 407 -- Used to issue/verify short-lived tokens for the authorization process below
375 local new_user_token, verify_user_token = jwt.init("HS256", random.bytes(32), nil, { default_ttl = 600 }); 408 local new_user_token, verify_user_token = jwt.init("HS256", random.bytes(32), nil, { default_ttl = 600 });
376 409
901 token_endpoint = handle_token_grant and module:http_url() .. "/token" or nil; 934 token_endpoint = handle_token_grant and module:http_url() .. "/token" or nil;
902 jwks_uri = nil; -- TODO? 935 jwks_uri = nil; -- TODO?
903 registration_endpoint = handle_register_request and module:http_url() .. "/register" or nil; 936 registration_endpoint = handle_register_request and module:http_url() .. "/register" or nil;
904 scopes_supported = usermanager.get_all_roles and array(it.keys(usermanager.get_all_roles(module.host))):append(array(openid_claims:items())); 937 scopes_supported = usermanager.get_all_roles and array(it.keys(usermanager.get_all_roles(module.host))):append(array(openid_claims:items()));
905 response_types_supported = array(it.keys(response_type_handlers)); 938 response_types_supported = array(it.keys(response_type_handlers));
939 code_challenge_methods_supported = array(it.keys(verifier_transforms));
906 authorization_response_iss_parameter_supported = true; 940 authorization_response_iss_parameter_supported = true;
907 941
908 -- OpenID 942 -- OpenID
909 userinfo_endpoint = handle_register_request and module:http_url() .. "/userinfo" or nil; 943 userinfo_endpoint = handle_register_request and module:http_url() .. "/userinfo" or nil;
910 }; 944 };