Comparison

mod_http_oauth2/mod_http_oauth2.lua @ 5856:75dee6127829 draft

Merge upstream
author Trần H. Trung <xmpp:trần.h.trung@trung.fun>
date Tue, 06 Feb 2024 18:32:01 +0700
parent 5853:b109773ce6fe
child 5882:761142ee0ff2
comparison
equal deleted inserted replaced
5664:52db2da66680 5856:75dee6127829
109 local registration_algo = module:get_option_string("oauth2_registration_algorithm", "HS256"); 109 local registration_algo = module:get_option_string("oauth2_registration_algorithm", "HS256");
110 local registration_ttl = module:get_option("oauth2_registration_ttl", nil); 110 local registration_ttl = module:get_option("oauth2_registration_ttl", nil);
111 local registration_options = module:get_option("oauth2_registration_options", 111 local registration_options = module:get_option("oauth2_registration_options",
112 { default_ttl = registration_ttl; accept_expired = not registration_ttl }); 112 { default_ttl = registration_ttl; accept_expired = not registration_ttl });
113 113
114 local pkce_required = module:get_option_boolean("oauth2_require_code_challenge", false); 114 local pkce_required = module:get_option_boolean("oauth2_require_code_challenge", true);
115 local respect_prompt = module:get_option_boolean("oauth2_respect_oidc_prompt", false);
115 116
116 local verification_key; 117 local verification_key;
117 local sign_client, verify_client; 118 local sign_client, verify_client;
118 if registration_key then 119 if registration_key then
119 -- Tie it to the host if global 120 -- Tie it to the host if global
135 end 136 end
136 137
137 client.client_hash = b64url(hashes.sha256(client_id)); 138 client.client_hash = b64url(hashes.sha256(client_id));
138 return client; 139 return client;
139 end 140 end
141
142 local purpose_map = { ["oauth2-refresh"] = "refresh_token"; ["oauth"] = "access_token" };
140 143
141 -- scope : string | array | set 144 -- scope : string | array | set
142 -- 145 --
143 -- at each step, allow the same or a subset of scopes 146 -- at each step, allow the same or a subset of scopes
144 -- (all ( client ( grant ( token ) ) )) 147 -- (all ( client ( grant ( token ) ) ))
210 213
211 local function code_expired(code) --> boolean, true: has expired, false: still valid 214 local function code_expired(code) --> boolean, true: has expired, false: still valid
212 return code_expires_in(code) < 0; 215 return code_expires_in(code) < 0;
213 end 216 end
214 217
218 -- LRU cache for short-term storage of authorization codes and device codes
215 local codes = cache.new(10000, function (_, code) 219 local codes = cache.new(10000, function (_, code)
220 -- If the cache is full and the oldest item hasn't expired yet then we
221 -- might be under some kind of DoS attack, so might as well reject further
222 -- entries for a bit.
216 return code_expired(code) 223 return code_expired(code)
217 end); 224 end);
218 225
219 -- Clear out unredeemed codes so they don't linger in memory. 226 -- Clear out unredeemed codes so they don't linger in memory.
220 module:daily("Clear expired authorization codes", function() 227 module:daily("Clear expired authorization codes", function()
228 -- The tail should be the least recently touched item, and most likely to
229 -- have expired already, so check and remove that one until encountering
230 -- one that has not expired.
221 local k, code = codes:tail(); 231 local k, code = codes:tail();
222 while code and code_expired(code) do 232 while code and code_expired(code) do
223 codes:set(k, nil); 233 codes:set(k, nil);
224 k, code = codes:tail(); 234 k, code = codes:tail();
225 end 235 end
240 local function oauth_error(err_name, err_desc) 250 local function oauth_error(err_name, err_desc)
241 return errors.new({ 251 return errors.new({
242 type = "modify"; 252 type = "modify";
243 condition = "bad-request"; 253 condition = "bad-request";
244 code = err_name == "invalid_client" and 401 or 400; 254 code = err_name == "invalid_client" and 401 or 400;
245 text = err_desc and (err_name..": "..err_desc) or err_name; 255 text = err_desc or err_name:gsub("^.", string.upper):gsub("_", " ");
246 extra = { oauth2_response = { error = err_name, error_description = err_desc } }; 256 extra = { oauth2_response = { error = err_name, error_description = err_desc } };
247 }); 257 });
248 end 258 end
249 259
250 -- client_id / client_metadata are pretty large, filter out a subset of 260 -- client_id / client_metadata are pretty large, filter out a subset of
270 end 280 end
271 281
272 local grant = refresh_token_info and refresh_token_info.grant; 282 local grant = refresh_token_info and refresh_token_info.grant;
273 if not grant then 283 if not grant then
274 -- No existing grant, create one 284 -- No existing grant, create one
275 grant = tokens.create_grant(token_jid, token_jid, default_refresh_ttl, token_data); 285 grant = tokens.create_grant(token_jid, token_jid, nil, token_data);
276 end 286 end
277 287
278 if refresh_token_info then 288 if refresh_token_info then
279 -- out with the old refresh tokens 289 -- out with the old refresh tokens
280 local ok, err = tokens.revoke_token(refresh_token_info.token); 290 local ok, err = tokens.revoke_token(refresh_token_info.token);
282 module:log("error", "Could not revoke refresh token: %s", err); 292 module:log("error", "Could not revoke refresh token: %s", err);
283 return 500; 293 return 500;
284 end 294 end
285 end 295 end
286 -- in with the new refresh token 296 -- in with the new refresh token
287 local refresh_token = refresh_token_info ~= false and tokens.create_token(token_jid, grant.id, nil, nil, "oauth2-refresh"); 297 local refresh_token = refresh_token_info ~= false and tokens.create_token(token_jid, grant.id, nil, default_refresh_ttl, "oauth2-refresh");
288 298
289 if role == "xmpp" then 299 if role == "xmpp" then
290 -- Special scope meaning the users default role. 300 -- Special scope meaning the users default role.
291 local user_default_role = usermanager.get_user_role(jid.node(token_jid), module.host); 301 local user_default_role = usermanager.get_user_role(jid.node(token_jid), module.host);
292 role = user_default_role and user_default_role.name; 302 role = user_default_role and user_default_role.name;
388 if not request_host or request_host ~= module.host then 398 if not request_host or request_host ~= module.host then
389 return oauth_error("invalid_request", "invalid JID"); 399 return oauth_error("invalid_request", "invalid JID");
390 end 400 end
391 local granted_scopes, granted_role = filter_scopes(request_username, params.scope); 401 local granted_scopes, granted_role = filter_scopes(request_username, params.scope);
392 402
393 if pkce_required and not params.code_challenge then 403 local redirect_uri = get_redirect_uri(client, params.redirect_uri);
404
405 if pkce_required and not params.code_challenge and redirect_uri ~= device_uri and redirect_uri ~= oob_uri then
394 return oauth_error("invalid_request", "PKCE required"); 406 return oauth_error("invalid_request", "PKCE required");
395 end 407 end
396 408
397 local prefix = "authorization_code:"; 409 local prefix = "authorization_code:";
398 local code = id.medium(); 410 local code = id.medium();
399 if params.redirect_uri == device_uri then 411 if redirect_uri == device_uri then
400 local is_device, device_state = verify_device_token(params.state); 412 local is_device, device_state = verify_device_token(params.state);
401 if is_device then 413 if is_device then
402 -- reconstruct the device_code 414 -- reconstruct the device_code
403 prefix = "device_code:"; 415 prefix = "device_code:";
404 code = b64url(hashes.hmac_sha256(verification_key, device_state.user_code)); 416 code = b64url(hashes.hmac_sha256(verification_key, device_state.user_code));
417 }); 429 });
418 if not ok then 430 if not ok then
419 return oauth_error("temporarily_unavailable"); 431 return oauth_error("temporarily_unavailable");
420 end 432 end
421 433
422 local redirect_uri = get_redirect_uri(client, params.redirect_uri);
423 if redirect_uri == oob_uri then 434 if redirect_uri == oob_uri then
424 return render_page(templates.oob, { client = client; authorization_code = code }, true); 435 return render_page(templates.oob, { client = client; authorization_code = code }, true);
425 elseif redirect_uri == device_uri then 436 elseif redirect_uri == device_uri then
426 return render_page(templates.device, { client = client }, true); 437 return render_page(templates.device, { client = client }, true);
427 elseif not redirect_uri then 438 elseif not redirect_uri then
628 639
629 if not form.user_token then 640 if not form.user_token then
630 -- First step: login 641 -- First step: login
631 local username = encodings.stringprep.nodeprep(form.username); 642 local username = encodings.stringprep.nodeprep(form.username);
632 local password = encodings.stringprep.saslprep(form.password); 643 local password = encodings.stringprep.saslprep(form.password);
644 -- Many things hooked to authentication-{success,failure} don't expect
645 -- non-XMPP sessions so here's something close enough...
646 local auth_event = {
647 session = {
648 type = "http";
649 ip = request.ip;
650 conn = request.conn;
651 username = username;
652 host = module.host;
653 log = request.log;
654 sasl_handler = { username = username; selected = "x-www-form" };
655 client_id = request.headers.user_agent;
656 };
657 };
633 if not (username and password) or not usermanager.test_password(username, module.host, password) then 658 if not (username and password) or not usermanager.test_password(username, module.host, password) then
659 module:fire_event("authentication-failure", auth_event);
634 return { 660 return {
635 error = "Invalid username/password"; 661 error = "Invalid username/password";
636 }; 662 };
637 end 663 end
664 module:fire_event("authentication-success", auth_event);
638 return { 665 return {
639 user = { 666 user = {
640 username = username; 667 username = username;
641 host = module.host; 668 host = module.host;
642 token = new_user_token({ username = username; host = module.host; auth_time = os.time() }); 669 token = new_user_token({ username = username; host = module.host; amr = { "pwd" } });
643 }; 670 };
644 }; 671 };
645 elseif form.user_token and form.consent then 672 elseif form.user_token and form.consent then
646 -- Second step: consent 673 -- Second step: consent
647 local ok, user = verify_user_token(form.user_token); 674 local ok, user = verify_user_token(form.user_token);
727 -- appending the error information to the redirect_uri and sending the 754 -- appending the error information to the redirect_uri and sending the
728 -- redirect to the user-agent. In some cases we can't do this, e.g. if 755 -- redirect to the user-agent. In some cases we can't do this, e.g. if
729 -- the redirect_uri is missing or invalid. In those cases, we render an 756 -- the redirect_uri is missing or invalid. In those cases, we render an
730 -- error directly to the user-agent. 757 -- error directly to the user-agent.
731 local function error_response(request, redirect_uri, err) 758 local function error_response(request, redirect_uri, err)
732 if not redirect_uri or redirect_uri == oob_uri then 759 if not redirect_uri or redirect_uri == oob_uri or redirect_uri == device_uri then
733 return render_error(err); 760 return render_error(err);
734 end 761 end
735 local q = strict_formdecode(request.url.query); 762 local q = strict_formdecode(request.url.query);
736 local redirect_query = url.parse(redirect_uri); 763 local redirect_query = url.parse(redirect_uri);
737 local sep = redirect_query.query and "&" or "?"; 764 local sep = redirect_query.query and "&" or "?";
738 redirect_uri = redirect_uri 765 redirect_uri = redirect_uri
739 .. sep .. http.formencode(err.extra.oauth2_response) 766 .. sep .. http.formencode(err.extra.oauth2_response)
740 .. "&" .. http.formencode({ state = q.state, iss = get_issuer() }); 767 .. "&" .. http.formencode({ state = q.state, iss = get_issuer() });
741 module:log("warn", "Sending error response to client via redirect to %s", redirect_uri); 768 module:log("debug", "Sending error response to client via redirect to %s", redirect_uri);
742 return { 769 return {
743 status_code = 303; 770 status_code = 303;
744 headers = { 771 headers = {
745 cache_control = "no-store"; 772 cache_control = "no-store";
746 pragma = "no-cache"; 773 pragma = "no-cache";
749 }; 776 };
750 end 777 end
751 778
752 local allowed_grant_type_handlers = module:get_option_set("allowed_oauth2_grant_types", { 779 local allowed_grant_type_handlers = module:get_option_set("allowed_oauth2_grant_types", {
753 "authorization_code"; 780 "authorization_code";
754 "password"; -- TODO Disable. The resource owner password credentials grant [RFC6749] MUST NOT be used.
755 "refresh_token"; 781 "refresh_token";
756 device_uri; 782 device_uri;
757 }) 783 })
758 if allowed_grant_type_handlers:contains("device_code") then 784 if allowed_grant_type_handlers:contains("device_code") then
759 -- expand short form because that URI is long 785 -- expand short form because that URI is long
779 else 805 else
780 module:log("debug", "Response type %q enabled", handler_type); 806 module:log("debug", "Response type %q enabled", handler_type);
781 end 807 end
782 end 808 end
783 809
784 local allowed_challenge_methods = module:get_option_set("allowed_oauth2_code_challenge_methods", { "plain"; "S256" }) 810 local allowed_challenge_methods = module:get_option_set("allowed_oauth2_code_challenge_methods", { "S256" })
785 for handler_type in pairs(verifier_transforms) do 811 for handler_type in pairs(verifier_transforms) do
786 if not allowed_challenge_methods:contains(handler_type) then 812 if not allowed_challenge_methods:contains(handler_type) then
787 module:log("debug", "Challenge method %q disabled", handler_type); 813 module:log("debug", "Challenge method %q disabled", handler_type);
788 verifier_transforms[handler_type] = nil; 814 verifier_transforms[handler_type] = nil;
789 else 815 else
857 return client_scopes:contains(scope); 883 return client_scopes:contains(scope);
858 end); 884 end);
859 end 885 end
860 886
861 -- The 'prompt' parameter from OpenID Core 887 -- The 'prompt' parameter from OpenID Core
862 local prompt = set.new(parse_scopes(params.prompt or "select_account login consent")); 888 local prompt = set.new(parse_scopes(respect_prompt and params.prompt or "select_account login consent"));
863 if prompt:contains("none") then
864 -- Client wants no interaction, only confirmation of prior login and
865 -- consent, but this is not implemented.
866 return error_response(request, redirect_uri, oauth_error("interaction_required"));
867 elseif not prompt:contains("select_account") then
868 -- TODO If the login page is split into account selection followed by login
869 -- (e.g. password), and then the account selection could be skipped iff the
870 -- 'login_hint' parameter is present.
871 return error_response(request, redirect_uri, oauth_error("account_selection_required"));
872 elseif not prompt:contains("login") then
873 -- Currently no cookies or such are used, so login is required every time.
874 return error_response(request, redirect_uri, oauth_error("login_required"));
875 elseif not prompt:contains("consent") then
876 -- Are there any circumstances when consent would be implied or assumed?
877 return error_response(request, redirect_uri, oauth_error("consent_required"));
878 end
879 889
880 local auth_state = get_auth_state(request); 890 local auth_state = get_auth_state(request);
881 if not auth_state.user then 891 if not auth_state.user then
892 if not prompt:contains("login") then
893 -- Currently no cookies or such are used, so login is required every time.
894 return error_response(request, redirect_uri, oauth_error("login_required"));
895 end
896
882 -- Render login page 897 -- Render login page
883 local extra = {}; 898 local extra = {};
884 if params.login_hint then 899 if params.login_hint then
885 extra.username_hint = (jid.prepped_split(params.login_hint)); 900 extra.username_hint = (jid.prepped_split(params.login_hint) or encodings.stringprep.nodeprep(params.login_hint));
901 elseif not prompt:contains("select_account") then
902 -- TODO If the login page is split into account selection followed by login
903 -- (e.g. password), and then the account selection could be skipped iff the
904 -- 'login_hint' parameter is present.
905 return error_response(request, redirect_uri, oauth_error("account_selection_required"));
886 end 906 end
887 return render_page(templates.login, { state = auth_state; client = client; extra = extra }); 907 return render_page(templates.login, { state = auth_state; client = client; extra = extra });
888 elseif auth_state.consent == nil then 908 elseif auth_state.consent == nil then
889 -- Render consent page
890 local scopes, roles = split_scopes(requested_scopes); 909 local scopes, roles = split_scopes(requested_scopes);
891 roles = user_assumable_roles(auth_state.user.username, roles); 910 roles = user_assumable_roles(auth_state.user.username, roles);
892 return render_page(templates.consent, { state = auth_state; client = client; scopes = scopes+roles }, true); 911
912 if not prompt:contains("consent") then
913 local grants = tokens.get_user_grants(auth_state.user.username);
914 local matching_grant;
915 if grants then
916 for grant_id, grant in pairs(grants) do
917 if grant.data and grant.data.oauth2_client and grant.data.oauth2_client.hash == client.client_hash then
918 if set.new(parse_scopes(grant.data.oauth2_scopes)) == set.new(scopes+roles) then
919 matching_grant = grant_id;
920 break
921 end
922 end
923 end
924 end
925
926 if not matching_grant then
927 return error_response(request, redirect_uri, oauth_error("consent_required"));
928 else
929 -- Consent for these scopes already granted to this exact client, continue
930 auth_state.scopes = scopes + roles;
931 auth_state.consent = "granted";
932 end
933
934 else
935 -- Render consent page
936 return render_page(templates.consent, { state = auth_state; client = client; scopes = scopes+roles }, true);
937 end
893 elseif not auth_state.consent then 938 elseif not auth_state.consent then
894 -- Notify client of rejection 939 -- Notify client of rejection
895 if redirect_uri == device_uri then 940 if redirect_uri == device_uri then
896 local is_device, device_state = verify_device_token(params.state); 941 local is_device, device_state = verify_device_token(params.state);
897 if is_device then 942 if is_device then
921 local id_token_signer = jwt.new_signer("HS256", client_secret); 966 local id_token_signer = jwt.new_signer("HS256", client_secret);
922 local id_token = id_token_signer({ 967 local id_token = id_token_signer({
923 iss = get_issuer(); 968 iss = get_issuer();
924 sub = url.build({ scheme = "xmpp"; path = user_jid }); 969 sub = url.build({ scheme = "xmpp"; path = user_jid });
925 aud = params.client_id; 970 aud = params.client_id;
926 auth_time = auth_state.user.auth_time; 971 auth_time = auth_state.user.iat;
927 nonce = params.nonce; 972 nonce = params.nonce;
973 amr = auth_state.user.amr;
928 }); 974 });
929 local response_type = params.response_type; 975 local response_type = params.response_type;
930 local response_handler = response_type_handlers[response_type]; 976 local response_handler = response_type_handlers[response_type];
931 if not response_handler then 977 if not response_handler then
932 return error_response(request, redirect_uri, oauth_error("unsupported_response_type")); 978 return error_response(request, redirect_uri, oauth_error("unsupported_response_type"));
1044 }); 1090 });
1045 }; 1091 };
1046 } 1092 }
1047 end 1093 end
1048 1094
1095 local function handle_introspection_request(event)
1096 local request = event.request;
1097 local credentials = get_request_credentials(request);
1098 if not credentials or credentials.type ~= "basic" then
1099 event.response.headers.www_authenticate = string.format("Basic realm=%q", module.host.."/"..module.name);
1100 return 401;
1101 end
1102 -- OAuth "client" credentials
1103 if not verify_client_secret(credentials.username, credentials.password) then
1104 return 401;
1105 end
1106
1107 local client = check_client(credentials.username);
1108 if not client then
1109 return 401;
1110 end
1111
1112 local form_data = http.formdecode(request.body or "=");
1113 local token = form_data.token;
1114 if not token then
1115 return 400;
1116 end
1117
1118 local token_info = tokens.get_token_info(form_data.token);
1119 if not token_info then
1120 return { headers = { content_type = "application/json" }; body = json.encode { active = false } };
1121 end
1122 local token_client = token_info.grant.data.oauth2_client;
1123 if not token_client or token_client.hash ~= client.client_hash then
1124 return 403;
1125 end
1126
1127 return {
1128 headers = { content_type = "application/json" };
1129 body = json.encode {
1130 active = true;
1131 client_id = credentials.username; -- We don't really know for sure
1132 username = jid.node(token_info.jid);
1133 scope = token_info.grant.data.oauth2_scopes;
1134 token_type = purpose_map[token_info.purpose];
1135 exp = token.expires;
1136 iat = token.created;
1137 sub = url.build({ scheme = "xmpp"; path = token_info.jid });
1138 aud = credentials.username;
1139 iss = get_issuer();
1140 jti = token_info.id;
1141 };
1142 };
1143 end
1144
1145 -- RFC 7009 says that the authorization server should validate that only the client that a token was issued to should be able to revoke it. However
1146 -- this would prevent someone who comes across a leaked token from doing the responsible thing and revoking it, so this is not enforced by default.
1049 local strict_auth_revoke = module:get_option_boolean("oauth2_require_auth_revoke", false); 1147 local strict_auth_revoke = module:get_option_boolean("oauth2_require_auth_revoke", false);
1050 1148
1051 local function handle_revocation_request(event) 1149 local function handle_revocation_request(event)
1052 local request, response = event.request, event.response; 1150 local request, response = event.request, event.response;
1053 response.headers.cache_control = "no-store"; 1151 response.headers.cache_control = "no-store";
1054 response.headers.pragma = "no-cache"; 1152 response.headers.pragma = "no-cache";
1055 if request.headers.authorization then 1153 local credentials = get_request_credentials(request);
1056 local credentials = get_request_credentials(request); 1154 if credentials then
1057 if not credentials or credentials.type ~= "basic" then 1155 if credentials.type ~= "basic" then
1058 response.headers.www_authenticate = string.format("Basic realm=%q", module.host.."/"..module.name); 1156 response.headers.www_authenticate = string.format("Basic realm=%q", module.host.."/"..module.name);
1059 return 401; 1157 return 401;
1060 end 1158 end
1061 -- OAuth "client" credentials 1159 -- OAuth "client" credentials
1062 if not verify_client_secret(credentials.username, credentials.password) then 1160 if not verify_client_secret(credentials.username, credentials.password) then
1072 local form_data = strict_formdecode(event.request.body); 1170 local form_data = strict_formdecode(event.request.body);
1073 if not form_data or not form_data.token then 1171 if not form_data or not form_data.token then
1074 response.headers.accept = "application/x-www-form-urlencoded"; 1172 response.headers.accept = "application/x-www-form-urlencoded";
1075 return 415; 1173 return 415;
1076 end 1174 end
1175
1176 if credentials then
1177 local client = check_client(credentials.username);
1178 if not client then
1179 return 401;
1180 end
1181 local token_info = tokens.get_token_info(form_data.token);
1182 if not token_info then
1183 return 404;
1184 end
1185 local token_client = token_info.grant.data.oauth2_client;
1186 if not token_client or token_client.hash ~= client.client_hash then
1187 return 403;
1188 end
1189 end
1190
1077 local ok, err = tokens.revoke_token(form_data.token); 1191 local ok, err = tokens.revoke_token(form_data.token);
1078 if not ok then 1192 if not ok then
1079 module:log("warn", "Unable to revoke token: %s", tostring(err)); 1193 module:log("warn", "Unable to revoke token: %s", tostring(err));
1080 return 500; 1194 return 500;
1081 end 1195 end
1082 return 200; 1196 return 200;
1083 end 1197 end
1084 1198
1085 local registration_schema = { 1199 local registration_schema = {
1086 title = "OAuth 2.0 Dynamic Client Registration Protocol"; 1200 title = "OAuth 2.0 Dynamic Client Registration Protocol";
1201 description = "This endpoint allows dynamically registering an OAuth 2.0 client.";
1087 type = "object"; 1202 type = "object";
1088 required = { 1203 required = {
1089 -- These are shown to users in the template 1204 -- These are shown to users in the template
1090 "client_name"; 1205 "client_name";
1091 "client_uri"; 1206 "client_uri";
1096 redirect_uris = { 1211 redirect_uris = {
1097 title = "List of Redirect URIs"; 1212 title = "List of Redirect URIs";
1098 type = "array"; 1213 type = "array";
1099 minItems = 1; 1214 minItems = 1;
1100 uniqueItems = true; 1215 uniqueItems = true;
1101 items = { title = "Redirect URI"; type = "string"; format = "uri" }; 1216 items = {
1217 title = "Redirect URI";
1218 type = "string";
1219 format = "uri";
1220 examples = {
1221 "https://app.example.com/redirect";
1222 "http://localhost:8080/redirect";
1223 "com.example.app:/redirect";
1224 oob_uri;
1225 device_uri;
1226 };
1227 };
1102 }; 1228 };
1103 token_endpoint_auth_method = { 1229 token_endpoint_auth_method = {
1104 title = "Token Endpoint Authentication Method"; 1230 title = "Token Endpoint Authentication Method";
1231 description = "Authentication method the client intends to use. Recommended is `client_secret_basic`. \z
1232 `none` is only allowed for use with the insecure Implicit flow.";
1105 type = "string"; 1233 type = "string";
1106 enum = { "none"; "client_secret_post"; "client_secret_basic" }; 1234 enum = { "none"; "client_secret_post"; "client_secret_basic" };
1107 default = "client_secret_basic"; 1235 default = "client_secret_basic";
1108 }; 1236 };
1109 grant_types = { 1237 grant_types = {
1110 title = "Grant Types"; 1238 title = "Grant Types";
1239 description = "List of grant types the client intends to use.";
1111 type = "array"; 1240 type = "array";
1112 minItems = 1; 1241 minItems = 1;
1113 uniqueItems = true; 1242 uniqueItems = true;
1114 items = { 1243 items = {
1115 type = "string"; 1244 type = "string";
1127 default = { "authorization_code" }; 1256 default = { "authorization_code" };
1128 }; 1257 };
1129 application_type = { 1258 application_type = {
1130 title = "Application Type"; 1259 title = "Application Type";
1131 description = "Determines which kinds of redirect URIs the client may register. \z 1260 description = "Determines which kinds of redirect URIs the client may register. \z
1132 The value 'web' limits the client to https:// URLs with the same hostname as in 'client_uri' \z 1261 The value `web` limits the client to `https://` URLs with the same hostname as \z
1133 while the value 'native' allows either loopback http:// URLs or application specific URIs."; 1262 in `client_uri` while the value `native` allows either loopback URLs like \z
1263 `http://localhost:8080/` or application specific URIs like `com.example.app:/redirect`.";
1134 type = "string"; 1264 type = "string";
1135 enum = { "native"; "web" }; 1265 enum = { "native"; "web" };
1136 default = "web"; 1266 default = "web";
1137 }; 1267 };
1138 response_types = { 1268 response_types = {
1148 description = "Human-readable name of the client, presented to the user in the consent dialog."; 1278 description = "Human-readable name of the client, presented to the user in the consent dialog.";
1149 type = "string"; 1279 type = "string";
1150 }; 1280 };
1151 client_uri = { 1281 client_uri = {
1152 title = "Client URL"; 1282 title = "Client URL";
1153 description = "Should be an link to a page with information about the client."; 1283 description = "Should be an link to a page with information about the client. \z
1284 The hostname in this URL must be the same as in every other '_uri' property.";
1154 type = "string"; 1285 type = "string";
1155 format = "uri"; 1286 format = "uri";
1156 pattern = "^https:"; 1287 pattern = "^https:";
1288 examples = { "https://app.example.com/" };
1157 }; 1289 };
1158 logo_uri = { 1290 logo_uri = {
1159 title = "Logo URL"; 1291 title = "Logo URL";
1160 description = "URL to the clients logotype (not currently used)."; 1292 description = "URL to the clients logotype (not currently used).";
1161 type = "string"; 1293 type = "string";
1162 format = "uri"; 1294 format = "uri";
1163 pattern = "^https:"; 1295 pattern = "^https:";
1296 examples = { "https://app.example.com/appicon.png" };
1164 }; 1297 };
1165 scope = { 1298 scope = {
1166 title = "Scopes"; 1299 title = "Scopes";
1167 description = "Space-separated list of scopes the client promises to restrict itself to."; 1300 description = "Space-separated list of scopes the client promises to restrict itself to.";
1168 type = "string"; 1301 type = "string";
1302 examples = { "openid xmpp" };
1169 }; 1303 };
1170 contacts = { 1304 contacts = {
1171 title = "Contact Addresses"; 1305 title = "Contact Addresses";
1172 description = "Addresses, typically email or URLs where the client developers can be contacted."; 1306 description = "Addresses, typically email or URLs where the client developers can be contacted.";
1173 type = "array"; 1307 type = "array";
1175 items = { type = "string"; format = "email" }; 1309 items = { type = "string"; format = "email" };
1176 }; 1310 };
1177 tos_uri = { 1311 tos_uri = {
1178 title = "Terms of Service URL"; 1312 title = "Terms of Service URL";
1179 description = "Link to Terms of Service for the client, presented to the user in the consent dialog. \z 1313 description = "Link to Terms of Service for the client, presented to the user in the consent dialog. \z
1180 MUST be a https:// URL with hostname matching that of 'client_uri'."; 1314 MUST be a `https://` URL with hostname matching that of `client_uri`.";
1181 type = "string"; 1315 type = "string";
1182 format = "uri"; 1316 format = "uri";
1183 pattern = "^https:"; 1317 pattern = "^https:";
1318 examples = { "https://app.example.com/tos.html" };
1184 }; 1319 };
1185 policy_uri = { 1320 policy_uri = {
1186 title = "Privacy Policy URL"; 1321 title = "Privacy Policy URL";
1187 description = "Link to a Privacy Policy for the client. MUST be a https:// URL with hostname matching that of 'client_uri'."; 1322 description = "Link to a Privacy Policy for the client. MUST be a `https://` URL with hostname matching that of `client_uri`.";
1188 type = "string"; 1323 type = "string";
1189 format = "uri"; 1324 format = "uri";
1190 pattern = "^https:"; 1325 pattern = "^https:";
1326 examples = { "https://app.example.com/policy.pdf" };
1191 }; 1327 };
1192 software_id = { 1328 software_id = {
1193 title = "Software ID"; 1329 title = "Software ID";
1194 description = "Unique identifier for the client software, common for all instances. Typically an UUID."; 1330 description = "Unique identifier for the client software, common for all instances. Typically an UUID.";
1195 type = "string"; 1331 type = "string";
1198 software_version = { 1334 software_version = {
1199 title = "Software Version"; 1335 title = "Software Version";
1200 description = "Version of the client software being registered. \z 1336 description = "Version of the client software being registered. \z
1201 E.g. to allow revoking all related tokens in the event of a security incident."; 1337 E.g. to allow revoking all related tokens in the event of a security incident.";
1202 type = "string"; 1338 type = "string";
1203 example = "2.3.1"; 1339 examples = { "2.3.1" };
1204 }; 1340 };
1205 }; 1341 };
1206 } 1342 }
1207 1343
1208 -- Limit per-locale fields to allowed locales, partly to keep size of client_id 1344 -- Limit per-locale fields to allowed locales, partly to keep size of client_id
1219 end 1355 end
1220 end 1356 end
1221 1357
1222 local function redirect_uri_allowed(redirect_uri, client_uri, app_type) 1358 local function redirect_uri_allowed(redirect_uri, client_uri, app_type)
1223 local uri = url.parse(redirect_uri); 1359 local uri = url.parse(redirect_uri);
1360 if not uri then
1361 return false;
1362 end
1224 if not uri.scheme then 1363 if not uri.scheme then
1225 return false; -- no relative URLs 1364 return false; -- no relative URLs
1226 end 1365 end
1227 if app_type == "native" then 1366 if app_type == "native" then
1228 return uri.scheme == "http" and loopbacks:contains(uri.host) or redirect_uri == oob_uri or uri.scheme:find(".", 1, true) ~= nil; 1367 return uri.scheme == "http" and loopbacks:contains(uri.host) or redirect_uri == oob_uri or uri.scheme:find(".", 1, true) ~= nil;
1230 return uri.scheme == "https" and uri.host == client_uri.host; 1369 return uri.scheme == "https" and uri.host == client_uri.host;
1231 end 1370 end
1232 end 1371 end
1233 1372
1234 function create_client(client_metadata) 1373 function create_client(client_metadata)
1235 if not schema.validate(registration_schema, client_metadata) then 1374 local valid, validation_errors = schema.validate(registration_schema, client_metadata);
1236 return nil, oauth_error("invalid_request", "Failed schema validation."); 1375 if not valid then
1376 return nil, errors.new({
1377 type = "modify";
1378 condition = "bad-request";
1379 code = 400;
1380 text = "Failed schema validation.";
1381 extra = {
1382 oauth2_response = {
1383 error = "invalid_request";
1384 error_description = "Client registration data failed schema validation."; -- TODO Generate from validation_errors?
1385 -- JSON Schema Output Format
1386 -- https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-01#name-basic
1387 valid = false;
1388 errors = validation_errors;
1389 };
1390 };
1391 });
1237 end 1392 end
1238 1393
1239 local client_uri = url.parse(client_metadata.client_uri); 1394 local client_uri = url.parse(client_metadata.client_uri);
1240 if not client_uri or client_uri.scheme ~= "https" or loopbacks:contains(client_uri.host) then 1395 if not client_uri or client_uri.scheme ~= "https" or loopbacks:contains(client_uri.host) then
1241 return nil, oauth_error("invalid_client_metadata", "Missing, invalid or insecure client_uri"); 1396 return nil, oauth_error("invalid_client_metadata", "Missing, invalid or insecure client_uri");
1286 if set.intersection(grant_types, allowed_grant_type_handlers):empty() then 1441 if set.intersection(grant_types, allowed_grant_type_handlers):empty() then
1287 return nil, oauth_error("invalid_client_metadata", "No allowed 'grant_types' specified"); 1442 return nil, oauth_error("invalid_client_metadata", "No allowed 'grant_types' specified");
1288 elseif set.intersection(response_types, allowed_response_type_handlers):empty() then 1443 elseif set.intersection(response_types, allowed_response_type_handlers):empty() then
1289 return nil, oauth_error("invalid_client_metadata", "No allowed 'response_types' specified"); 1444 return nil, oauth_error("invalid_client_metadata", "No allowed 'response_types' specified");
1290 end 1445 end
1291
1292 -- Do we want to keep everything?
1293 local client_id = sign_client(client_metadata);
1294
1295 client_metadata.client_id = client_id;
1296 client_metadata.client_id_issued_at = os.time();
1297 1446
1298 if client_metadata.token_endpoint_auth_method ~= "none" then 1447 if client_metadata.token_endpoint_auth_method ~= "none" then
1299 -- Ensure that each client_id JWT with a client_secret is unique. 1448 -- Ensure that each client_id JWT with a client_secret is unique.
1300 -- A short ID along with the issued at timestamp should be sufficient to 1449 -- A short ID along with the issued at timestamp should be sufficient to
1301 -- rule out brute force attacks. 1450 -- rule out brute force attacks.
1302 -- Not needed for public clients without a secret, but those are expected 1451 -- Not needed for public clients without a secret, but those are expected
1303 -- to be uncommon since they can only do the insecure implicit flow. 1452 -- to be uncommon since they can only do the insecure implicit flow.
1304 client_metadata.nonce = id.short(); 1453 client_metadata.nonce = id.short();
1305 1454 end
1306 local client_secret = make_client_secret(client_id, client_metadata); 1455
1456 -- Do we want to keep everything?
1457 local client_id = sign_client(client_metadata);
1458
1459 client_metadata.client_id = client_id;
1460 client_metadata.client_id_issued_at = os.time();
1461
1462 if client_metadata.token_endpoint_auth_method ~= "none" then
1463 local client_secret = make_client_secret(client_id);
1307 client_metadata.client_secret = client_secret; 1464 client_metadata.client_secret = client_secret;
1308 client_metadata.client_secret_expires_at = 0; 1465 client_metadata.client_secret_expires_at = 0;
1309 1466
1310 if not registration_options.accept_expired then 1467 if not registration_options.accept_expired then
1311 client_metadata.client_secret_expires_at = client_metadata.client_id_issued_at + (registration_options.default_ttl or 3600); 1468 client_metadata.client_secret_expires_at = client_metadata.client_id_issued_at + (registration_options.default_ttl or 3600);
1422 -- Step 4 is later repeated using the refresh token to get new access tokens. 1579 -- Step 4 is later repeated using the refresh token to get new access tokens.
1423 1580
1424 -- Step 5. Revoke token (access or refresh) 1581 -- Step 5. Revoke token (access or refresh)
1425 ["POST /revoke"] = handle_revocation_request; 1582 ["POST /revoke"] = handle_revocation_request;
1426 1583
1584 -- Get info about a token
1585 ["POST /introspect"] = handle_introspection_request;
1586
1427 -- OpenID 1587 -- OpenID
1428 ["GET /userinfo"] = handle_userinfo_request; 1588 ["GET /userinfo"] = handle_userinfo_request;
1429 1589
1430 -- Optional static content for templates 1590 -- Optional static content for templates
1431 ["GET /style.css"] = templates.css and { 1591 ["GET /style.css"] = templates.css and {
1432 headers = { 1592 headers = {
1433 ["Content-Type"] = "text/css"; 1593 ["Content-Type"] = "text/css";
1434 }; 1594 };
1435 body = render_html(templates.css, module:get_option("oauth2_template_style")); 1595 body = templates.css;
1436 } or nil; 1596 } or nil;
1437 ["GET /script.js"] = templates.js and { 1597 ["GET /script.js"] = templates.js and {
1438 headers = { 1598 headers = {
1439 ["Content-Type"] = "text/javascript"; 1599 ["Content-Type"] = "text/javascript";
1440 }; 1600 };
1443 1603
1444 -- Some convenient fallback handlers 1604 -- Some convenient fallback handlers
1445 ["GET /register"] = { headers = { content_type = "application/schema+json" }; body = json.encode(registration_schema) }; 1605 ["GET /register"] = { headers = { content_type = "application/schema+json" }; body = json.encode(registration_schema) };
1446 ["GET /token"] = function() return 405; end; 1606 ["GET /token"] = function() return 405; end;
1447 ["GET /revoke"] = function() return 405; end; 1607 ["GET /revoke"] = function() return 405; end;
1608 ["GET /introspect"] = function() return 405; end;
1448 }; 1609 };
1449 }); 1610 });
1450 1611
1451 local http_server = require "net.http.server"; 1612 local http_server = require "net.http.server";
1452 1613
1479 op_policy_uri = module:get_option_string("oauth2_policy_url", nil); 1640 op_policy_uri = module:get_option_string("oauth2_policy_url", nil);
1480 op_tos_uri = module:get_option_string("oauth2_terms_url", nil); 1641 op_tos_uri = module:get_option_string("oauth2_terms_url", nil);
1481 revocation_endpoint = handle_revocation_request and module:http_url() .. "/revoke" or nil; 1642 revocation_endpoint = handle_revocation_request and module:http_url() .. "/revoke" or nil;
1482 revocation_endpoint_auth_methods_supported = array({ "client_secret_basic" }); 1643 revocation_endpoint_auth_methods_supported = array({ "client_secret_basic" });
1483 device_authorization_endpoint = handle_device_authorization_request and module:http_url() .. "/device"; 1644 device_authorization_endpoint = handle_device_authorization_request and module:http_url() .. "/device";
1645 introspection_endpoint = handle_introspection_request and module:http_url() .. "/introspect";
1646 introspection_endpoint_auth_methods_supported = nil;
1484 code_challenge_methods_supported = array(it.keys(verifier_transforms)); 1647 code_challenge_methods_supported = array(it.keys(verifier_transforms));
1485 grant_types_supported = array(it.keys(grant_type_handlers)); 1648 grant_types_supported = array(it.keys(grant_type_handlers));
1486 response_modes_supported = array(it.keys(response_type_handlers)):map(tmap { token = "fragment"; code = "query" }); 1649 response_modes_supported = array(it.keys(response_type_handlers)):map(tmap { token = "fragment"; code = "query" });
1487 authorization_response_iss_parameter_supported = true; 1650 authorization_response_iss_parameter_supported = true;
1488 service_documentation = module:get_option_string("oauth2_service_documentation", "https://modules.prosody.im/mod_http_oauth2.html"); 1651 service_documentation = module:get_option_string("oauth2_service_documentation", "https://modules.prosody.im/mod_http_oauth2.html");