Software /
code /
prosody-modules
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"); |