Comparison

mod_http_oauth2/mod_http_oauth2.lua @ 6309:342f88e8d522 draft

Merge update
author Trần H. Trung <xmpp:trần.h.trung@trung.fun>
date Sun, 15 Jun 2025 01:08:46 +0700
parent 6273:8ceedc336d0d
parent 6308:e1c54de06905
child 6344:eb834f754f57
comparison
equal deleted inserted replaced
6279:b92aea4e7ff4 6309:342f88e8d522
408 408
409 function grant_type_handlers.password(params, client) 409 function grant_type_handlers.password(params, client)
410 local request_username 410 local request_username
411 411
412 if expect_username_jid then 412 if expect_username_jid then
413 local request_jid = assert(params.username, oauth_error("invalid_request", "missing 'username' (JID)")); 413 local request_jid = params.username;
414 if not request_jid then
415 return oauth_error("invalid_request", "missing 'username' (JID)");
416 end
414 local _request_username, request_host = jid.prepped_split(request_jid); 417 local _request_username, request_host = jid.prepped_split(request_jid);
415 418
416 if not (_request_username and request_host) or request_host ~= module.host then 419 if not (_request_username and request_host) or request_host ~= module.host then
417 return oauth_error("invalid_request", "invalid JID"); 420 return oauth_error("invalid_request", "invalid JID");
418 end 421 end
419 422
420 request_username = _request_username 423 request_username = _request_username
421 else 424 else
422 request_username = assert(params.username, oauth_error("invalid_request", "missing 'username'")); 425 request_username = params.username;
423 end 426 if not request_username then
424 427 return oauth_error("invalid_request", "missing 'username'");
425 local request_password = assert(params.password, oauth_error("invalid_request", "missing 'password'")); 428 end
429 end
430
431 local request_password = params.password;
432 if not request_password then
433 return oauth_error("invalid_request", "missing 'password'");
434 end
435
436 local auth_event = {
437 session = {
438 type = "oauth2";
439 ip = "::";
440 username = request_username;
441 host = module.host;
442 log = module._log;
443 sasl_handler = { username = request_username; selected = "x-oauth2-password" };
444 client_id = client.client_name;
445 };
446 };
426 447
427 if not usermanager.test_password(request_username, module.host, request_password) then 448 if not usermanager.test_password(request_username, module.host, request_password) then
449 module:fire_event("authentication-failure", auth_event);
428 return oauth_error("invalid_grant", "incorrect credentials"); 450 return oauth_error("invalid_grant", "incorrect credentials");
429 end 451 end
452
453 module:fire_event("authentication-success", auth_event);
430 454
431 local granted_jid = jid.join(request_username, module.host); 455 local granted_jid = jid.join(request_username, module.host);
432 local granted_scopes, granted_role = filter_scopes(request_username, params.scope); 456 local granted_scopes, granted_role = filter_scopes(request_username, params.scope);
433 return json.encode(new_access_token(granted_jid, granted_role, granted_scopes, client)); 457 return json.encode(new_access_token(granted_jid, granted_role, granted_scopes, client));
434 end 458 end
550 end 574 end
551 575
552 return json.encode(new_access_token(code.granted_jid, code.granted_role, code.granted_scopes, client, code.id_token)); 576 return json.encode(new_access_token(code.granted_jid, code.granted_role, code.granted_scopes, client, code.id_token));
553 end 577 end
554 578
555 function grant_type_handlers.refresh_token(params) 579 function grant_type_handlers.refresh_token(params, client)
556 if not params.client_id then return oauth_error("invalid_request", "missing 'client_id'"); end
557 if not params.client_secret then return oauth_error("invalid_request", "missing 'client_secret'"); end
558 if not params.refresh_token then return oauth_error("invalid_request", "missing 'refresh_token'"); end 580 if not params.refresh_token then return oauth_error("invalid_request", "missing 'refresh_token'"); end
559
560 local client = check_client(params.client_id);
561 if not client then
562 return oauth_error("invalid_client", "incorrect credentials");
563 end
564
565 if not verify_client_secret(params.client_id, params.client_secret) then
566 module:log("debug", "client_secret mismatch");
567 return oauth_error("invalid_client", "incorrect credentials");
568 end
569 581
570 local refresh_token_info = tokens.get_token_info(params.refresh_token); 582 local refresh_token_info = tokens.get_token_info(params.refresh_token);
571 if not refresh_token_info or refresh_token_info.purpose ~= "oauth2-refresh" then 583 if not refresh_token_info or refresh_token_info.purpose ~= "oauth2-refresh" then
572 return oauth_error("invalid_grant", "invalid refresh token"); 584 return oauth_error("invalid_grant", "invalid refresh token");
573 end 585 end
596 refresh_token_info.token = params.refresh_token; 608 refresh_token_info.token = params.refresh_token;
597 609
598 return json.encode(new_access_token(refresh_token_info.jid, role, new_scopes, client, nil, refresh_token_info)); 610 return json.encode(new_access_token(refresh_token_info.jid, role, new_scopes, client, nil, refresh_token_info));
599 end 611 end
600 612
601 grant_type_handlers[device_uri] = function(params) 613 grant_type_handlers[device_uri] = function(params, client)
602 if not params.client_id then return oauth_error("invalid_request", "missing 'client_id'"); end
603 if not params.client_secret then return oauth_error("invalid_request", "missing 'client_secret'"); end
604 if not params.device_code then return oauth_error("invalid_request", "missing 'device_code'"); end 614 if not params.device_code then return oauth_error("invalid_request", "missing 'device_code'"); end
605
606 local client = check_client(params.client_id);
607 if not client then
608 return oauth_error("invalid_client", "incorrect credentials");
609 end
610
611 if not verify_client_secret(params.client_id, params.client_secret) then
612 module:log("debug", "client_secret mismatch");
613 return oauth_error("invalid_client", "incorrect credentials");
614 end
615 615
616 local code = codes:get("device_code:" .. params.client_id .. "#" .. params.device_code); 616 local code = codes:get("device_code:" .. params.client_id .. "#" .. params.device_code);
617 if type(code) ~= "table" or code_expired(code) then 617 if type(code) ~= "table" or code_expired(code) then
618 return oauth_error("expired_token"); 618 return oauth_error("expired_token");
619 elseif code.error then 619 elseif code.error then
745 745
746 if module:get_host_type() == "component" then 746 if module:get_host_type() == "component" then
747 local component_secret = assert(module:get_option_string("component_secret"), "'component_secret' is a required setting when loaded on a Component"); 747 local component_secret = assert(module:get_option_string("component_secret"), "'component_secret' is a required setting when loaded on a Component");
748 748
749 function grant_type_handlers.password(params) 749 function grant_type_handlers.password(params)
750 local request_jid = assert(params.username, oauth_error("invalid_request", "missing 'username' (JID)")); 750 local request_jid = params.username;
751 local request_password = assert(params.password, oauth_error("invalid_request", "missing 'password'")); 751 if not request_jid then
752 return oauth_error("invalid_request", "missing 'username' (JID)");
753 end
754 local request_password = params.password
755 if not request_password then
756 return oauth_error("invalid_request", "missing 'password'");
757 end
752 local request_username, request_host, request_resource = jid.prepped_split(request_jid); 758 local request_username, request_host, request_resource = jid.prepped_split(request_jid);
753 if params.scope then 759 if params.scope then
754 -- TODO shouldn't we support scopes / roles here? 760 -- TODO shouldn't we support scopes / roles here?
755 return oauth_error("invalid_scope", "unknown scope requested"); 761 return oauth_error("invalid_scope", "unknown scope requested");
756 end 762 end
779 -- appending the error information to the redirect_uri and sending the 785 -- appending the error information to the redirect_uri and sending the
780 -- redirect to the user-agent. In some cases we can't do this, e.g. if 786 -- redirect to the user-agent. In some cases we can't do this, e.g. if
781 -- the redirect_uri is missing or invalid. In those cases, we render an 787 -- the redirect_uri is missing or invalid. In those cases, we render an
782 -- error directly to the user-agent. 788 -- error directly to the user-agent.
783 local function error_response(request, redirect_uri, err) 789 local function error_response(request, redirect_uri, err)
784 if not redirect_uri or redirect_uri == oob_uri or redirect_uri == device_uri then 790 if not redirect_uri or redirect_uri == oob_uri then
785 return render_error(err); 791 return render_error(err);
786 end 792 end
787 local q = strict_formdecode(request.url.query); 793 local params = strict_formdecode(request.url.query);
794 if redirect_uri == device_uri then
795 local is_device, device_state = verify_device_token(params.state);
796 if is_device then
797 local device_code = b64url(hashes.hmac_sha256(verification_key, device_state.user_code));
798 local code = codes:get("device_code:" .. params.client_id .. "#" .. device_code);
799 if type(code) == "table" then
800 code.error = err;
801 code.expires = os.time() + 60;
802 codes:set("device_code:" .. params.client_id .. "#" .. device_code, code);
803 end
804 end
805 return render_error(err);
806 end
788 local redirect_query = url.parse(redirect_uri); 807 local redirect_query = url.parse(redirect_uri);
789 local sep = redirect_query.query and "&" or "?"; 808 local sep = redirect_query.query and "&" or "?";
790 redirect_uri = redirect_uri 809 redirect_uri = redirect_uri
791 .. sep .. http.formencode(err.extra.oauth2_response) 810 .. sep .. http.formencode(err.extra.oauth2_response)
792 .. "&" .. http.formencode({ state = q.state, iss = get_issuer() }); 811 .. "&" .. http.formencode({ state = params.state, iss = get_issuer() });
793 module:log("debug", "Sending error response to client via redirect to %s", redirect_uri); 812 module:log("debug", "Sending error response to client via redirect to %s", redirect_uri);
794 return { 813 return {
795 status_code = 303; 814 status_code = 303;
796 headers = { 815 headers = {
797 cache_control = "no-store"; 816 cache_control = "no-store";
840 else 859 else
841 module:log("debug", "Challenge method %q enabled", handler_type); 860 module:log("debug", "Challenge method %q enabled", handler_type);
842 end 861 end
843 end 862 end
844 863
864 local function array_contains(haystack, needle)
865 for _, item in ipairs(haystack) do
866 if item == needle then
867 return true
868 end
869 end
870 return false
871 end
872
845 function handle_token_grant(event) 873 function handle_token_grant(event)
846 local credentials = get_request_credentials(event.request); 874 local credentials = get_request_credentials(event.request);
847 875
848 event.response.headers.content_type = "application/json"; 876 event.response.headers.content_type = "application/json";
849 event.response.headers.cache_control = "no-store"; 877 event.response.headers.cache_control = "no-store";
872 return oauth_error("invalid_client", "incorrect credentials"); 900 return oauth_error("invalid_client", "incorrect credentials");
873 end 901 end
874 902
875 903
876 local grant_type = params.grant_type 904 local grant_type = params.grant_type
905 if not array_contains(client.grant_types or { "authorization_code" }, grant_type) then
906 return oauth_error("invalid_request", "'grant_type' not registered");
907 end
908
877 local grant_handler = grant_type_handlers[grant_type]; 909 local grant_handler = grant_type_handlers[grant_type];
878 if not grant_handler then 910 if not grant_handler then
879 return oauth_error("invalid_request", "No such grant type."); 911 return oauth_error("invalid_request", "'grant_type' not available");
880 end 912 end
913
881 return grant_handler(params, client); 914 return grant_handler(params, client);
882 end 915 end
883 916
884 local function handle_authorization_request(event) 917 local function handle_authorization_request(event)
885 local request = event.request; 918 local request = event.request;
907 if not redirect_uri then 940 if not redirect_uri then
908 return render_error(oauth_error("invalid_request", "Invalid 'redirect_uri' parameter")); 941 return render_error(oauth_error("invalid_request", "Invalid 'redirect_uri' parameter"));
909 end 942 end
910 -- From this point we know that redirect_uri is safe to use 943 -- From this point we know that redirect_uri is safe to use
911 944
912 local client_response_types = set.new(array(client.response_types or { "code" })); 945 local response_type = params.response_type;
913 client_response_types = set.intersection(client_response_types, allowed_response_type_handlers); 946 if not array_contains(client.response_types or { "code" }, response_type) then
914 if not client_response_types:contains(params.response_type) then 947 return error_response(request, redirect_uri, oauth_error("invalid_client", "'response_type' not registered"));
915 return error_response(request, redirect_uri, oauth_error("invalid_client", "'response_type' not allowed")); 948 end
949 if not allowed_response_type_handlers:contains(response_type) then
950 return error_response(request, redirect_uri, oauth_error("unsupported_response_type", "'response_type' not allowed"));
951 end
952 local response_handler = response_type_handlers[response_type];
953 if not response_handler then
954 return error_response(request, redirect_uri, oauth_error("unsupported_response_type"));
916 end 955 end
917 956
918 local requested_scopes = parse_scopes(params.scope or ""); 957 local requested_scopes = parse_scopes(params.scope or "");
919 if client.scope then 958 if client.scope then
920 local client_scopes = set.new(parse_scopes(client.scope)); 959 local client_scopes = set.new(parse_scopes(client.scope));
974 -- Render consent page 1013 -- Render consent page
975 return render_page(templates.consent, { state = auth_state; client = client; scopes = scopes+roles }, true); 1014 return render_page(templates.consent, { state = auth_state; client = client; scopes = scopes+roles }, true);
976 end 1015 end
977 elseif not auth_state.consent then 1016 elseif not auth_state.consent then
978 -- Notify client of rejection 1017 -- Notify client of rejection
979 if redirect_uri == device_uri then
980 local is_device, device_state = verify_device_token(params.state);
981 if is_device then
982 local device_code = b64url(hashes.hmac_sha256(verification_key, device_state.user_code));
983 local code = codes:get("device_code:" .. params.client_id .. "#" .. device_code);
984 code.error = oauth_error("access_denied");
985 code.expires = os.time() + 60;
986 codes:set("device_code:" .. params.client_id .. "#" .. device_code, code);
987 end
988 end
989 return error_response(request, redirect_uri, oauth_error("access_denied")); 1018 return error_response(request, redirect_uri, oauth_error("access_denied"));
990 end 1019 end
991 -- else auth_state.consent == true 1020 -- else auth_state.consent == true
992 1021
993 local granted_scopes = auth_state.scopes 1022 local granted_scopes = auth_state.scopes
1007 iss = get_issuer(); 1036 iss = get_issuer();
1008 sub = url.build({ scheme = "xmpp"; path = user_jid }); 1037 sub = url.build({ scheme = "xmpp"; path = user_jid });
1009 aud = params.client_id; 1038 aud = params.client_id;
1010 auth_time = auth_state.user.iat; 1039 auth_time = auth_state.user.iat;
1011 nonce = params.nonce; 1040 nonce = params.nonce;
1012 amr = auth_state.user.amr; 1041 amr = auth_state.user.amr; -- RFC 8176: Authentication Method Reference Values
1013 }); 1042 });
1014 local response_type = params.response_type;
1015 local response_handler = response_type_handlers[response_type];
1016 if not response_handler then
1017 return error_response(request, redirect_uri, oauth_error("unsupported_response_type"));
1018 end
1019 local ret = response_handler(client, params, user_jid, id_token); 1043 local ret = response_handler(client, params, user_jid, id_token);
1020 if errors.is_err(ret) then 1044 if errors.is_err(ret) then
1021 return error_response(request, redirect_uri, ret); 1045 return error_response(request, redirect_uri, ret);
1022 end 1046 end
1023 return ret; 1047 return ret;
1305 default = "web"; 1329 default = "web";
1306 }; 1330 };
1307 response_types = { 1331 response_types = {
1308 title = "Response Types"; 1332 title = "Response Types";
1309 type = "array"; 1333 type = "array";
1310 minItems = 1;
1311 uniqueItems = true; 1334 uniqueItems = true;
1312 items = { type = "string"; enum = { "code"; "token" } }; 1335 items = { type = "string"; enum = { "code"; "token" } };
1313 default = { "code" }; 1336 default = { "code" };
1314 }; 1337 };
1315 client_name = { 1338 client_name = {
1469 end 1492 end
1470 1493
1471 local grant_types = set.new(client_metadata.grant_types); 1494 local grant_types = set.new(client_metadata.grant_types);
1472 local response_types = set.new(client_metadata.response_types); 1495 local response_types = set.new(client_metadata.response_types);
1473 1496
1497 if not (grant_types - allowed_grant_type_handlers):empty() then
1498 return nil, oauth_error("invalid_client_metadata", "Disallowed 'grant_types' specified");
1499 elseif not (response_types - allowed_response_type_handlers):empty() then
1500 return nil, oauth_error("invalid_client_metadata", "Disallowed 'response_types' specified");
1501 end
1502
1474 if grant_types:contains("authorization_code") and not response_types:contains("code") then 1503 if grant_types:contains("authorization_code") and not response_types:contains("code") then
1475 return nil, oauth_error("invalid_client_metadata", "Inconsistency between 'grant_types' and 'response_types'"); 1504 return nil, oauth_error("invalid_client_metadata", "Inconsistency between 'grant_types' and 'response_types'");
1476 elseif grant_types:contains("implicit") and not response_types:contains("token") then 1505 elseif grant_types:contains("implicit") and not response_types:contains("token") then
1477 return nil, oauth_error("invalid_client_metadata", "Inconsistency between 'grant_types' and 'response_types'"); 1506 return nil, oauth_error("invalid_client_metadata", "Inconsistency between 'grant_types' and 'response_types'");
1478 end
1479
1480 if set.intersection(grant_types, allowed_grant_type_handlers):empty() then
1481 return nil, oauth_error("invalid_client_metadata", "No allowed 'grant_types' specified");
1482 elseif set.intersection(response_types, allowed_response_type_handlers):empty() then
1483 return nil, oauth_error("invalid_client_metadata", "No allowed 'response_types' specified");
1484 end 1507 end
1485 1508
1486 if client_metadata.token_endpoint_auth_method ~= "none" then 1509 if client_metadata.token_endpoint_auth_method ~= "none" then
1487 -- Ensure that each client_id JWT with a client_secret is unique. 1510 -- Ensure that each client_id JWT with a client_secret is unique.
1488 -- A short ID along with the issued at timestamp should be sufficient to 1511 -- A short ID along with the issued at timestamp should be sufficient to
1667 authorization_server_metadata = { 1690 authorization_server_metadata = {
1668 -- RFC 8414: OAuth 2.0 Authorization Server Metadata 1691 -- RFC 8414: OAuth 2.0 Authorization Server Metadata
1669 issuer = get_issuer(); 1692 issuer = get_issuer();
1670 authorization_endpoint = handle_authorization_request and module:http_url() .. "/authorize" or nil; 1693 authorization_endpoint = handle_authorization_request and module:http_url() .. "/authorize" or nil;
1671 token_endpoint = handle_token_grant and module:http_url() .. "/token" or nil; 1694 token_endpoint = handle_token_grant and module:http_url() .. "/token" or nil;
1695 jwks_uri = nil; -- REQUIRED in OpenID Discovery but not in OAuth 2.0 Metadata
1672 registration_endpoint = handle_register_request and module:http_url() .. "/register" or nil; 1696 registration_endpoint = handle_register_request and module:http_url() .. "/register" or nil;
1673 scopes_supported = usermanager.get_all_roles 1697 scopes_supported = usermanager.get_all_roles
1674 and array(it.keys(usermanager.get_all_roles(module.host))):push("xmpp"):append(array(openid_claims:items())); 1698 and array(it.keys(usermanager.get_all_roles(module.host))):push("xmpp"):append(array(openid_claims:items()));
1675 response_types_supported = array(it.keys(response_type_handlers)); 1699 response_types_supported = array(it.keys(response_type_handlers));
1676 token_endpoint_auth_methods_supported = array({ "client_secret_post"; "client_secret_basic" }); 1700 response_modes_supported = array(it.keys(response_type_handlers)):map(tmap { token = "fragment"; code = "query" });
1701 grant_types_supported = array(it.keys(grant_type_handlers));
1702 token_endpoint_auth_methods_supported = array({ "client_secret_basic"; "client_secret_post"; "none" });
1703 token_endpoint_auth_signing_alg_values_supported = nil;
1704 service_documentation = module:get_option_string("oauth2_service_documentation", "https://modules.prosody.im/mod_http_oauth2.html");
1705 ui_locales_supported = allowed_locales[1] and allowed_locales;
1677 op_policy_uri = module:get_option_string("oauth2_policy_url", nil); 1706 op_policy_uri = module:get_option_string("oauth2_policy_url", nil);
1678 op_tos_uri = module:get_option_string("oauth2_terms_url", nil); 1707 op_tos_uri = module:get_option_string("oauth2_terms_url", nil);
1679 revocation_endpoint = handle_revocation_request and module:http_url() .. "/revoke" or nil; 1708 revocation_endpoint = handle_revocation_request and module:http_url() .. "/revoke" or nil;
1680 revocation_endpoint_auth_methods_supported = array({ "client_secret_basic" }); 1709 revocation_endpoint_auth_methods_supported = array({ "client_secret_basic"; "client_secret_post"; "none" });
1681 device_authorization_endpoint = handle_device_authorization_request and module:http_url() .. "/device"; 1710 revocation_endpoint_auth_signing_alg_values_supported = nil;
1682 introspection_endpoint = handle_introspection_request and module:http_url() .. "/introspect"; 1711 introspection_endpoint = handle_introspection_request and module:http_url() .. "/introspect";
1683 introspection_endpoint_auth_methods_supported = nil; 1712 introspection_endpoint_auth_methods_supported = nil;
1713 introspection_endpoint_auth_signing_alg_values_supported = nil;
1684 code_challenge_methods_supported = array(it.keys(verifier_transforms)); 1714 code_challenge_methods_supported = array(it.keys(verifier_transforms));
1685 grant_types_supported = array(it.keys(grant_type_handlers)); 1715
1686 response_modes_supported = array(it.keys(response_type_handlers)):map(tmap { token = "fragment"; code = "query" }); 1716 -- RFC 8628: OAuth 2.0 Device Authorization Grant
1717 device_authorization_endpoint = handle_device_authorization_request and module:http_url() .. "/device";
1718
1719 -- RFC 9207: OAuth 2.0 Authorization Server Issuer Identification
1687 authorization_response_iss_parameter_supported = true; 1720 authorization_response_iss_parameter_supported = true;
1688 service_documentation = module:get_option_string("oauth2_service_documentation", "https://modules.prosody.im/mod_http_oauth2.html"); 1721
1689 ui_locales_supported = allowed_locales[1] and allowed_locales; 1722 -- OpenID Connect Discovery 1.0
1690
1691 -- OpenID
1692 userinfo_endpoint = handle_userinfo_request and module:http_url() .. "/userinfo" or nil; 1723 userinfo_endpoint = handle_userinfo_request and module:http_url() .. "/userinfo" or nil;
1693 jwks_uri = nil; -- REQUIRED in OpenID Discovery but not in OAuth 2.0 Metadata
1694 id_token_signing_alg_values_supported = { "HS256" }; -- The algorithm RS256 MUST be included, but we use HS256 and client_secret as shared key. 1724 id_token_signing_alg_values_supported = { "HS256" }; -- The algorithm RS256 MUST be included, but we use HS256 and client_secret as shared key.
1695 } 1725 }
1696 return authorization_server_metadata; 1726 return authorization_server_metadata;
1697 end 1727 end
1698 1728