Mercurial > prosody-modules
comparison mod_http_oauth2/mod_http_oauth2.lua @ 5589: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 |
| parents | feadbd481285 |
| children | b496ebc12aed |
comparison
equal
deleted
inserted
replaced
| 5588:59acf7f540c1 | 5589: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 |
