Software /
code /
prosody-modules
Comparison
mod_http_oauth2/mod_http_oauth2.lua @ 5383:df11a2cbc7b7
mod_http_oauth2: Implement RFC 7628 Proof Key for Code Exchange
Likely to become mandatory in OAuth 2.1.
Backwards compatible since the default 'plain' verifier would compare
nil with nil if the relevant parameters are left out.
author | Kim Alvefur <zash@zash.se> |
---|---|
date | Sat, 29 Apr 2023 13:09:46 +0200 |
parent | 5382:12498c0d705f |
child | 5384:b40f29ec391a |
comparison
equal
deleted
inserted
replaced
5382:12498c0d705f | 5383:df11a2cbc7b7 |
---|---|
15 local jwt = require"util.jwt"; | 15 local jwt = require"util.jwt"; |
16 local it = require "util.iterators"; | 16 local it = require "util.iterators"; |
17 local array = require "util.array"; | 17 local array = require "util.array"; |
18 local st = require "util.stanza"; | 18 local st = require "util.stanza"; |
19 | 19 |
20 local function b64url(s) | |
21 return (s:gsub("[+/=]", { ["+"] = "-", ["/"] = "_", ["="] = "" })) | |
22 end | |
23 | |
20 local function read_file(base_path, fn, required) | 24 local function read_file(base_path, fn, required) |
21 local f, err = io.open(base_path .. "/" .. fn); | 25 local f, err = io.open(base_path .. "/" .. fn); |
22 if not f then | 26 if not f then |
23 module:log(required and "error" or "debug", "Unable to load template file: %s", err); | 27 module:log(required and "error" or "debug", "Unable to load template file: %s", err); |
24 if required then | 28 if required then |
67 -- Used to derive client_secret from client_id, set to enable stateless dynamic registration. | 71 -- Used to derive client_secret from client_id, set to enable stateless dynamic registration. |
68 local registration_key = module:get_option_string("oauth2_registration_key"); | 72 local registration_key = module:get_option_string("oauth2_registration_key"); |
69 local registration_algo = module:get_option_string("oauth2_registration_algorithm", "HS256"); | 73 local registration_algo = module:get_option_string("oauth2_registration_algorithm", "HS256"); |
70 local registration_options = module:get_option("oauth2_registration_options", { default_ttl = 60 * 60 * 24 * 90 }); | 74 local registration_options = module:get_option("oauth2_registration_options", { default_ttl = 60 * 60 * 24 * 90 }); |
71 | 75 |
76 local pkce_required = module:get_option_boolean("oauth2_require_code_challenge", false); | |
77 | |
72 local verification_key; | 78 local verification_key; |
73 local jwt_sign, jwt_verify; | 79 local jwt_sign, jwt_verify; |
74 if registration_key then | 80 if registration_key then |
75 -- Tie it to the host if global | 81 -- Tie it to the host if global |
76 verification_key = hashes.hmac_sha256(registration_key, module.host); | 82 verification_key = hashes.hmac_sha256(registration_key, module.host); |
209 end | 215 end |
210 end | 216 end |
211 | 217 |
212 local grant_type_handlers = {}; | 218 local grant_type_handlers = {}; |
213 local response_type_handlers = {}; | 219 local response_type_handlers = {}; |
220 local verifier_transforms = {}; | |
214 | 221 |
215 function grant_type_handlers.password(params) | 222 function grant_type_handlers.password(params) |
216 local request_jid = assert(params.username, oauth_error("invalid_request", "missing 'username' (JID)")); | 223 local request_jid = assert(params.username, oauth_error("invalid_request", "missing 'username' (JID)")); |
217 local request_password = assert(params.password, oauth_error("invalid_request", "missing 'password'")); | 224 local request_password = assert(params.password, oauth_error("invalid_request", "missing 'password'")); |
218 local request_username, request_host, request_resource = jid.prepped_split(request_jid); | 225 local request_username, request_host, request_resource = jid.prepped_split(request_jid); |
233 local request_username, request_host = jid.split(granted_jid); | 240 local request_username, request_host = jid.split(granted_jid); |
234 if not request_host or request_host ~= module.host then | 241 if not request_host or request_host ~= module.host then |
235 return oauth_error("invalid_request", "invalid JID"); | 242 return oauth_error("invalid_request", "invalid JID"); |
236 end | 243 end |
237 local granted_scopes, granted_role = filter_scopes(request_username, params.scope); | 244 local granted_scopes, granted_role = filter_scopes(request_username, params.scope); |
245 | |
246 if pkce_required and not params.code_challenge then | |
247 return oauth_error("invalid_request", "PKCE required"); | |
248 end | |
238 | 249 |
239 local code = id.medium(); | 250 local code = id.medium(); |
240 local ok = codes:set(params.client_id .. "#" .. code, { | 251 local ok = codes:set(params.client_id .. "#" .. code, { |
241 expires = os.time() + 600; | 252 expires = os.time() + 600; |
242 granted_jid = granted_jid; | 253 granted_jid = granted_jid; |
243 granted_scopes = granted_scopes; | 254 granted_scopes = granted_scopes; |
244 granted_role = granted_role; | 255 granted_role = granted_role; |
256 challenge = params.code_challenge; | |
257 challenge_method = params.code_challenge_method; | |
245 id_token = id_token; | 258 id_token = id_token; |
246 }); | 259 }); |
247 if not ok then | 260 if not ok then |
248 return {status_code = 429}; | 261 return {status_code = 429}; |
249 end | 262 end |
338 if not code or type(code) ~= "table" or code_expired(code) then | 351 if not code or type(code) ~= "table" or code_expired(code) then |
339 module:log("debug", "authorization_code invalid or expired: %q", code); | 352 module:log("debug", "authorization_code invalid or expired: %q", code); |
340 return oauth_error("invalid_client", "incorrect credentials"); | 353 return oauth_error("invalid_client", "incorrect credentials"); |
341 end | 354 end |
342 | 355 |
356 -- TODO Decide if the code should be removed or not when PKCE fails | |
357 local transform = verifier_transforms[code.challenge_method or "plain"]; | |
358 if not transform then | |
359 return oauth_error("invalid_request", "unknown challenge transform method"); | |
360 elseif transform(params.code_verifier) ~= code.challenge then | |
361 return oauth_error("invalid_grant", "incorrect credentials"); | |
362 end | |
363 | |
343 return json.encode(new_access_token(code.granted_jid, code.granted_role, code.granted_scopes, client, code.id_token)); | 364 return json.encode(new_access_token(code.granted_jid, code.granted_role, code.granted_scopes, client, code.id_token)); |
344 end | 365 end |
345 | 366 |
346 function grant_type_handlers.refresh_token(params) | 367 function grant_type_handlers.refresh_token(params) |
347 if not params.client_id then return oauth_error("invalid_request", "missing 'client_id'"); end | 368 if not params.client_id then return oauth_error("invalid_request", "missing 'client_id'"); end |
367 refresh_token_info.token = params.refresh_token; | 388 refresh_token_info.token = params.refresh_token; |
368 | 389 |
369 return json.encode(new_access_token( | 390 return json.encode(new_access_token( |
370 refresh_token_info.jid, refresh_token_info.role, refresh_token_info.grant.data.oauth2_scopes, client, nil, refresh_token_info | 391 refresh_token_info.jid, refresh_token_info.role, refresh_token_info.grant.data.oauth2_scopes, client, nil, refresh_token_info |
371 )); | 392 )); |
393 end | |
394 | |
395 -- RFC 7636 Proof Key for Code Exchange by OAuth Public Clients | |
396 | |
397 function verifier_transforms.plain(code_verifier) | |
398 -- code_challenge = code_verifier | |
399 return code_verifier; | |
400 end | |
401 | |
402 function verifier_transforms.S256(code_verifier) | |
403 -- code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) | |
404 return code_verifier and b64url(hashes.SHA256(code_verifier)); | |
372 end | 405 end |
373 | 406 |
374 -- Used to issue/verify short-lived tokens for the authorization process below | 407 -- Used to issue/verify short-lived tokens for the authorization process below |
375 local new_user_token, verify_user_token = jwt.init("HS256", random.bytes(32), nil, { default_ttl = 600 }); | 408 local new_user_token, verify_user_token = jwt.init("HS256", random.bytes(32), nil, { default_ttl = 600 }); |
376 | 409 |
901 token_endpoint = handle_token_grant and module:http_url() .. "/token" or nil; | 934 token_endpoint = handle_token_grant and module:http_url() .. "/token" or nil; |
902 jwks_uri = nil; -- TODO? | 935 jwks_uri = nil; -- TODO? |
903 registration_endpoint = handle_register_request and module:http_url() .. "/register" or nil; | 936 registration_endpoint = handle_register_request and module:http_url() .. "/register" or nil; |
904 scopes_supported = usermanager.get_all_roles and array(it.keys(usermanager.get_all_roles(module.host))):append(array(openid_claims:items())); | 937 scopes_supported = usermanager.get_all_roles and array(it.keys(usermanager.get_all_roles(module.host))):append(array(openid_claims:items())); |
905 response_types_supported = array(it.keys(response_type_handlers)); | 938 response_types_supported = array(it.keys(response_type_handlers)); |
939 code_challenge_methods_supported = array(it.keys(verifier_transforms)); | |
906 authorization_response_iss_parameter_supported = true; | 940 authorization_response_iss_parameter_supported = true; |
907 | 941 |
908 -- OpenID | 942 -- OpenID |
909 userinfo_endpoint = handle_register_request and module:http_url() .. "/userinfo" or nil; | 943 userinfo_endpoint = handle_register_request and module:http_url() .. "/userinfo" or nil; |
910 }; | 944 }; |