Comparison

mod_http_oauth2/mod_http_oauth2.lua @ 5596:7040d0772758

mod_http_oauth2: Implement RFC 8628 Device Authorization Grant Meant for devices without easy access to a web browser, such as refrigerators and toasters, which definitely need to be running OAuth-enabled XMPP clients! Could be used for CLI tools that might have trouble running a http server needed for the authorization code flow.
author Kim Alvefur <zash@zash.se>
date Mon, 10 Jul 2023 07:16:54 +0200
parent 5580:feadbd481285
child 5605:b496ebc12aed
comparison
equal deleted inserted replaced
5592:59acf7f540c1 5596:7040d0772758
66 local template_path = module:get_option_path("oauth2_template_path", "html"); 66 local template_path = module:get_option_path("oauth2_template_path", "html");
67 local templates = { 67 local templates = {
68 login = read_file(template_path, "login.html", true); 68 login = read_file(template_path, "login.html", true);
69 consent = read_file(template_path, "consent.html", true); 69 consent = read_file(template_path, "consent.html", true);
70 oob = read_file(template_path, "oob.html", true); 70 oob = read_file(template_path, "oob.html", true);
71 device = read_file(template_path, "device.html", true);
71 error = read_file(template_path, "error.html", true); 72 error = read_file(template_path, "error.html", true);
72 css = read_file(template_path, "style.css"); 73 css = read_file(template_path, "style.css");
73 js = read_file(template_path, "script.js"); 74 js = read_file(template_path, "script.js");
74 }; 75 };
75 76
118 -- Tie it to the host if global 119 -- Tie it to the host if global
119 verification_key = hashes.hmac_sha256(registration_key, module.host); 120 verification_key = hashes.hmac_sha256(registration_key, module.host);
120 sign_client, verify_client = jwt.init(registration_algo, registration_key, registration_key, registration_options); 121 sign_client, verify_client = jwt.init(registration_algo, registration_key, registration_key, registration_options);
121 end 122 end
122 123
124 local new_device_token, verify_device_token = jwt.init("HS256", random.bytes(32), nil, { default_ttl = 600 });
125
123 -- verify and prepare client structure 126 -- verify and prepare client structure
124 local function check_client(client_id) 127 local function check_client(client_id)
125 if not verify_client then 128 if not verify_client then
126 return nil, "client-registration-not-enabled"; 129 return nil, "client-registration-not-enabled";
127 end 130 end
229 232
230 -- Non-standard special redirect URI that has the AS show the authorization 233 -- Non-standard special redirect URI that has the AS show the authorization
231 -- code to the user for them to copy-paste into the client, which can then 234 -- code to the user for them to copy-paste into the client, which can then
232 -- continue as if it received it via redirect. 235 -- continue as if it received it via redirect.
233 local oob_uri = "urn:ietf:wg:oauth:2.0:oob"; 236 local oob_uri = "urn:ietf:wg:oauth:2.0:oob";
237 local device_uri = "urn:ietf:params:oauth:grant-type:device_code";
234 238
235 local loopbacks = set.new({ "localhost", "127.0.0.1", "::1" }); 239 local loopbacks = set.new({ "localhost", "127.0.0.1", "::1" });
236 240
237 local function oauth_error(err_name, err_desc) 241 local function oauth_error(err_name, err_desc)
238 return errors.new({ 242 return errors.new({
315 return; 319 return;
316 end 320 end
317 -- When only a single URI is registered, that's the default 321 -- When only a single URI is registered, that's the default
318 return client.redirect_uris[1]; 322 return client.redirect_uris[1];
319 end 323 end
324 if query_redirect_uri == device_uri and client.grant_types then
325 for _, grant_type in ipairs(client.grant_types) do
326 if grant_type == device_uri then
327 return query_redirect_uri;
328 end
329 end
330 -- Tried to use device authorization flow without registering it.
331 return;
332 end
320 -- Verify the client-provided URI matches one previously registered 333 -- Verify the client-provided URI matches one previously registered
321 for _, redirect_uri in ipairs(client.redirect_uris) do 334 for _, redirect_uri in ipairs(client.redirect_uris) do
322 if query_redirect_uri == redirect_uri then 335 if query_redirect_uri == redirect_uri then
323 return redirect_uri 336 return redirect_uri
324 end 337 end
340 353
341 local grant_type_handlers = {}; 354 local grant_type_handlers = {};
342 local response_type_handlers = {}; 355 local response_type_handlers = {};
343 local verifier_transforms = {}; 356 local verifier_transforms = {};
344 357
358 function grant_type_handlers.implicit()
359 -- Placeholder to make discovery work correctly.
360 -- Access tokens are delivered via redirect when using the implict flow, not
361 -- via the token endpoint, so how did you get here?
362 return oauth_error("invalid_request");
363 end
364
345 function grant_type_handlers.password(params) 365 function grant_type_handlers.password(params)
346 local request_jid = assert(params.username, oauth_error("invalid_request", "missing 'username' (JID)")); 366 local request_jid = assert(params.username, oauth_error("invalid_request", "missing 'username' (JID)"));
347 local request_password = assert(params.password, oauth_error("invalid_request", "missing 'password'")); 367 local request_password = assert(params.password, oauth_error("invalid_request", "missing 'password'"));
348 local request_username, request_host, request_resource = jid.prepped_split(request_jid); 368 local request_username, request_host, request_resource = jid.prepped_split(request_jid);
349 369
369 if pkce_required and not params.code_challenge then 389 if pkce_required and not params.code_challenge then
370 return oauth_error("invalid_request", "PKCE required"); 390 return oauth_error("invalid_request", "PKCE required");
371 end 391 end
372 392
373 local code = id.medium(); 393 local code = id.medium();
394 if params.redirect_uri == device_uri then
395 local is_device, device_state = verify_device_token(params.state);
396 if is_device then
397 -- reconstruct the device_code
398 code = b64url(hashes.hmac_sha256(verification_key, device_state.user_code));
399 end
400 end
374 local ok = codes:set(params.client_id .. "#" .. code, { 401 local ok = codes:set(params.client_id .. "#" .. code, {
375 expires = os.time() + 600; 402 expires = os.time() + 600;
376 granted_jid = granted_jid; 403 granted_jid = granted_jid;
377 granted_scopes = granted_scopes; 404 granted_scopes = granted_scopes;
378 granted_role = granted_role; 405 granted_role = granted_role;
385 end 412 end
386 413
387 local redirect_uri = get_redirect_uri(client, params.redirect_uri); 414 local redirect_uri = get_redirect_uri(client, params.redirect_uri);
388 if redirect_uri == oob_uri then 415 if redirect_uri == oob_uri then
389 return render_page(templates.oob, { client = client; authorization_code = code }, true); 416 return render_page(templates.oob, { client = client; authorization_code = code }, true);
417 elseif redirect_uri == device_uri then
418 return render_page(templates.device, { client = client }, true);
390 elseif not redirect_uri then 419 elseif not redirect_uri then
391 return oauth_error("invalid_redirect_uri"); 420 return oauth_error("invalid_redirect_uri");
392 end 421 end
393 422
394 local redirect = url.parse(redirect_uri); 423 local redirect = url.parse(redirect_uri);
526 555
527 -- new_access_token() requires the actual token 556 -- new_access_token() requires the actual token
528 refresh_token_info.token = params.refresh_token; 557 refresh_token_info.token = params.refresh_token;
529 558
530 return json.encode(new_access_token(refresh_token_info.jid, role, new_scopes, client, nil, refresh_token_info)); 559 return json.encode(new_access_token(refresh_token_info.jid, role, new_scopes, client, nil, refresh_token_info));
560 end
561
562 grant_type_handlers[device_uri] = function(params)
563 if not params.client_id then return oauth_error("invalid_request", "missing 'client_id'"); end
564 if not params.client_secret then return oauth_error("invalid_request", "missing 'client_secret'"); end
565 if not params.device_code then return oauth_error("invalid_request", "missing 'device_code'"); end
566
567 local client = check_client(params.client_id);
568 if not client then
569 return oauth_error("invalid_client", "incorrect credentials");
570 end
571
572 if not verify_client_secret(params.client_id, params.client_secret) then
573 module:log("debug", "client_secret mismatch");
574 return oauth_error("invalid_client", "incorrect credentials");
575 end
576
577 local code = codes:get(params.client_id .. "#" .. params.device_code);
578 if type(code) ~= "table" or code_expired(code) then
579 return oauth_error("expired_token");
580 elseif code.error then
581 return code.error;
582 elseif not code.granted_jid then
583 return oauth_error("authorization_pending");
584 end
585 codes:set(client.client_hash .. "#" .. params.device_code, nil);
586
587 return json.encode(new_access_token(code.granted_jid, code.granted_role, code.granted_scopes, client, code.id_token));
531 end 588 end
532 589
533 -- RFC 7636 Proof Key for Code Exchange by OAuth Public Clients 590 -- RFC 7636 Proof Key for Code Exchange by OAuth Public Clients
534 591
535 function verifier_transforms.plain(code_verifier) 592 function verifier_transforms.plain(code_verifier)
686 743
687 local allowed_grant_type_handlers = module:get_option_set("allowed_oauth2_grant_types", { 744 local allowed_grant_type_handlers = module:get_option_set("allowed_oauth2_grant_types", {
688 "authorization_code"; 745 "authorization_code";
689 "password"; -- TODO Disable. The resource owner password credentials grant [RFC6749] MUST NOT be used. 746 "password"; -- TODO Disable. The resource owner password credentials grant [RFC6749] MUST NOT be used.
690 "refresh_token"; 747 "refresh_token";
748 device_uri;
691 }) 749 })
692 for handler_type in pairs(grant_type_handlers) do 750 for handler_type in pairs(grant_type_handlers) do
693 if not allowed_grant_type_handlers:contains(handler_type) then 751 if not allowed_grant_type_handlers:contains(handler_type) then
694 module:log("debug", "Grant type %q disabled", handler_type); 752 module:log("debug", "Grant type %q disabled", handler_type);
695 grant_type_handlers[handler_type] = nil; 753 grant_type_handlers[handler_type] = nil;
725 event.response.headers.content_type = "application/json"; 783 event.response.headers.content_type = "application/json";
726 event.response.headers.cache_control = "no-store"; 784 event.response.headers.cache_control = "no-store";
727 event.response.headers.pragma = "no-cache"; 785 event.response.headers.pragma = "no-cache";
728 local params = strict_formdecode(event.request.body); 786 local params = strict_formdecode(event.request.body);
729 if not params then 787 if not params then
730 return oauth_error("invalid_request"); 788 return oauth_error("invalid_request", "Could not parse request body as 'application/x-www-form-urlencoded'");
731 end 789 end
732 790
733 if credentials and credentials.type == "basic" then 791 if credentials and credentials.type == "basic" then
734 -- client_secret_basic converted internally to client_secret_post 792 -- client_secret_basic converted internally to client_secret_post
735 params.client_id = http.urldecode(credentials.username); 793 params.client_id = http.urldecode(credentials.username);
737 end 795 end
738 796
739 local grant_type = params.grant_type 797 local grant_type = params.grant_type
740 local grant_handler = grant_type_handlers[grant_type]; 798 local grant_handler = grant_type_handlers[grant_type];
741 if not grant_handler then 799 if not grant_handler then
742 return oauth_error("invalid_request"); 800 return oauth_error("invalid_request", "No such grant type.");
743 end 801 end
744 return grant_handler(params); 802 return grant_handler(params);
745 end 803 end
746 804
747 local function handle_authorization_request(event) 805 local function handle_authorization_request(event)
818 local scopes, roles = split_scopes(requested_scopes); 876 local scopes, roles = split_scopes(requested_scopes);
819 roles = user_assumable_roles(auth_state.user.username, roles); 877 roles = user_assumable_roles(auth_state.user.username, roles);
820 return render_page(templates.consent, { state = auth_state; client = client; scopes = scopes+roles }, true); 878 return render_page(templates.consent, { state = auth_state; client = client; scopes = scopes+roles }, true);
821 elseif not auth_state.consent then 879 elseif not auth_state.consent then
822 -- Notify client of rejection 880 -- Notify client of rejection
881 if redirect_uri == device_uri then
882 local is_device, device_state = verify_device_token(params.state);
883 if is_device then
884 local device_code = b64url(hashes.hmac_sha256(verification_key, device_state.user_code));
885 local code = codes:get(params.client_id .. "#" .. device_code);
886 code.error = oauth_error("access_denied");
887 code.expires = os.time() + 60;
888 codes:set(params.client_id .. "#" .. device_code, code);
889 end
890 end
823 return error_response(request, redirect_uri, oauth_error("access_denied")); 891 return error_response(request, redirect_uri, oauth_error("access_denied"));
824 end 892 end
825 -- else auth_state.consent == true 893 -- else auth_state.consent == true
826 894
827 local granted_scopes = auth_state.scopes 895 local granted_scopes = auth_state.scopes
852 local ret = response_handler(client, params, user_jid, id_token); 920 local ret = response_handler(client, params, user_jid, id_token);
853 if errors.is_err(ret) then 921 if errors.is_err(ret) then
854 return error_response(request, redirect_uri, ret); 922 return error_response(request, redirect_uri, ret);
855 end 923 end
856 return ret; 924 return ret;
925 end
926
927 local function handle_device_authorization_request(event)
928 local request = event.request;
929
930 local credentials = get_request_credentials(request);
931
932 local params = strict_formdecode(request.body);
933 if not params then
934 return render_error(oauth_error("invalid_request", "Invalid query parameters"));
935 end
936
937 if credentials and credentials.type == "basic" then
938 -- client_secret_basic converted internally to client_secret_post
939 params.client_id = http.urldecode(credentials.username);
940 local client_secret = http.urldecode(credentials.password);
941
942 if not verify_client_secret(params.client_id, client_secret) then
943 module:log("debug", "client_secret mismatch");
944 return oauth_error("invalid_client", "incorrect credentials");
945 end
946 else
947 return 401;
948 end
949
950 local client = check_client(params.client_id);
951
952 if not client then
953 return render_error(oauth_error("invalid_request", "Invalid 'client_id' parameter"));
954 end
955
956 if not set.new(client.grant_types):contains(device_uri) then
957 return render_error(oauth_error("invalid_client", "Client not registered for device authorization grant"));
958 end
959
960 local requested_scopes = parse_scopes(params.scope or "");
961 if client.scope then
962 local client_scopes = set.new(parse_scopes(client.scope));
963 requested_scopes:filter(function(scope)
964 return client_scopes:contains(scope);
965 end);
966 end
967
968 -- TODO better code generator, this one should be easy to type from a
969 -- screen onto a phone
970 local user_code = (id.tiny() .. "-" .. id.tiny()):upper();
971 local collisions = 0;
972 while codes:get(user_code) do
973 collisions = collisions + 1;
974 if collisions > 10 then
975 return oauth_error("temporarily_unavailable");
976 end
977 user_code = (id.tiny() .. "-" .. id.tiny()):upper();
978 end
979 -- device code should be derivable after consent but not guessable by the user
980 local device_code = b64url(hashes.hmac_sha256(verification_key, user_code));
981 local verification_uri = module:http_url() .. "/device";
982 local verification_uri_complete = verification_uri .. "?" .. http.formencode({ user_code = user_code });
983
984 local dc_ok = codes:set(params.client_id .. "#" .. device_code, { expires = os.time() + 1200 });
985 local uc_ok = codes:set(user_code,
986 { user_code = user_code; expires = os.time() + 600; client_id = params.client_id;
987 scope = requested_scopes:concat(" ") });
988 if not dc_ok or not uc_ok then
989 return oauth_error("temporarily_unavailable");
990 end
991
992 return {
993 headers = { content_type = "application/json"; cache_control = "no-store"; pragma = "no-cache" };
994 body = json.encode {
995 device_code = device_code;
996 user_code = user_code;
997 verification_uri = verification_uri;
998 verification_uri_complete = verification_uri_complete;
999 expires_in = 600;
1000 interval = 5;
1001 };
1002 }
1003 end
1004
1005 local function handle_device_verification_request(event)
1006 local request = event.request;
1007 local params = strict_formdecode(request.url.query);
1008 if not params or not params.user_code then
1009 return render_page(templates.device, { client = false });
1010 end
1011
1012 local device_info = codes:get(params.user_code);
1013 if not device_info or code_expired(device_info) or not codes:set(params.user_code, nil) then
1014 return render_error(oauth_error("expired_token", "Incorrect or expired code"));
1015 end
1016
1017 return {
1018 status_code = 303;
1019 headers = {
1020 location = module:http_url() .. "/authorize" .. "?" .. http.formencode({
1021 client_id = device_info.client_id;
1022 redirect_uri = device_uri;
1023 response_type = "code";
1024 scope = device_info.scope;
1025 state = new_device_token({ user_code = params.user_code });
1026 });
1027 };
1028 }
857 end 1029 end
858 1030
859 local function handle_revocation_request(event) 1031 local function handle_revocation_request(event)
860 local request, response = event.request, event.response; 1032 local request, response = event.request, event.response;
861 response.headers.cache_control = "no-store"; 1033 response.headers.cache_control = "no-store";
913 "password"; 1085 "password";
914 "client_credentials"; 1086 "client_credentials";
915 "refresh_token"; 1087 "refresh_token";
916 "urn:ietf:params:oauth:grant-type:jwt-bearer"; 1088 "urn:ietf:params:oauth:grant-type:jwt-bearer";
917 "urn:ietf:params:oauth:grant-type:saml2-bearer"; 1089 "urn:ietf:params:oauth:grant-type:saml2-bearer";
1090 device_uri;
918 }; 1091 };
919 }; 1092 };
920 default = { "authorization_code" }; 1093 default = { "authorization_code" };
921 }; 1094 };
922 application_type = { type = "string"; enum = { "native"; "web" }; default = "web" }; 1095 application_type = { type = "string"; enum = { "native"; "web" }; default = "web" };
1067 1240
1068 if not registration_key then 1241 if not registration_key then
1069 module:log("info", "No 'oauth2_registration_key', dynamic client registration disabled") 1242 module:log("info", "No 'oauth2_registration_key', dynamic client registration disabled")
1070 handle_authorization_request = nil 1243 handle_authorization_request = nil
1071 handle_register_request = nil 1244 handle_register_request = nil
1245 handle_device_authorization_request = nil
1246 handle_device_verification_request = nil
1072 end 1247 end
1073 1248
1074 local function handle_userinfo_request(event) 1249 local function handle_userinfo_request(event)
1075 local request = event.request; 1250 local request = event.request;
1076 local credentials = get_request_credentials(request); 1251 local credentials = get_request_credentials(request);
1127 -- OAuth 2.0 in 5 simple steps! 1302 -- OAuth 2.0 in 5 simple steps!
1128 -- This is the normal 'authorization_code' flow. 1303 -- This is the normal 'authorization_code' flow.
1129 1304
1130 -- Step 1. Create OAuth client 1305 -- Step 1. Create OAuth client
1131 ["POST /register"] = handle_register_request; 1306 ["POST /register"] = handle_register_request;
1307
1308 -- Device flow
1309 ["POST /device"] = handle_device_authorization_request;
1310 ["GET /device"] = handle_device_verification_request;
1132 1311
1133 -- Step 2. User-facing login and consent view 1312 -- Step 2. User-facing login and consent view
1134 ["GET /authorize"] = handle_authorization_request; 1313 ["GET /authorize"] = handle_authorization_request;
1135 ["POST /authorize"] = handle_authorization_request; 1314 ["POST /authorize"] = handle_authorization_request;
1136 ["OPTIONS /authorize"] = { status_code = 403; body = "" }; 1315 ["OPTIONS /authorize"] = { status_code = 403; body = "" };
1201 token_endpoint_auth_methods_supported = array({ "client_secret_post"; "client_secret_basic" }); 1380 token_endpoint_auth_methods_supported = array({ "client_secret_post"; "client_secret_basic" });
1202 op_policy_uri = module:get_option_string("oauth2_policy_url", nil); 1381 op_policy_uri = module:get_option_string("oauth2_policy_url", nil);
1203 op_tos_uri = module:get_option_string("oauth2_terms_url", nil); 1382 op_tos_uri = module:get_option_string("oauth2_terms_url", nil);
1204 revocation_endpoint = handle_revocation_request and module:http_url() .. "/revoke" or nil; 1383 revocation_endpoint = handle_revocation_request and module:http_url() .. "/revoke" or nil;
1205 revocation_endpoint_auth_methods_supported = array({ "client_secret_basic" }); 1384 revocation_endpoint_auth_methods_supported = array({ "client_secret_basic" });
1385 device_authorization_endpoint = handle_device_authorization_request and module:http_url() .. "/device";
1206 code_challenge_methods_supported = array(it.keys(verifier_transforms)); 1386 code_challenge_methods_supported = array(it.keys(verifier_transforms));
1207 grant_types_supported = array(it.keys(response_type_handlers)):map(tmap { 1387 grant_types_supported = array(it.keys(grant_type_handlers));
1208 token = "implicit";
1209 code = "authorization_code";
1210 });
1211 response_modes_supported = array(it.keys(response_type_handlers)):map(tmap { token = "fragment"; code = "query" }); 1388 response_modes_supported = array(it.keys(response_type_handlers)):map(tmap { token = "fragment"; code = "query" });
1212 authorization_response_iss_parameter_supported = true; 1389 authorization_response_iss_parameter_supported = true;
1213 service_documentation = module:get_option_string("oauth2_service_documentation", "https://modules.prosody.im/mod_http_oauth2.html"); 1390 service_documentation = module:get_option_string("oauth2_service_documentation", "https://modules.prosody.im/mod_http_oauth2.html");
1214 ui_locales_supported = allowed_locales[1] and allowed_locales; 1391 ui_locales_supported = allowed_locales[1] and allowed_locales;
1215 1392