# HG changeset patch # User Kim Alvefur # Date 1682766586 -7200 # Node ID df11a2cbc7b743bb5927e106db9e927026297e82 # Parent 12498c0d705f80ee001b6ad0f22dd79d05a4ebdc 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. diff -r 12498c0d705f -r df11a2cbc7b7 mod_http_oauth2/README.markdown --- a/mod_http_oauth2/README.markdown Sat Apr 29 11:26:04 2023 +0200 +++ b/mod_http_oauth2/README.markdown Sat Apr 29 13:09:46 2023 +0200 @@ -46,6 +46,7 @@ - [RFC 6749: The OAuth 2.0 Authorization Framework](https://www.rfc-editor.org/rfc/rfc6749) - [RFC 7628: A Set of Simple Authentication and Security Layer (SASL) Mechanisms for OAuth](https://www.rfc-editor.org/rfc/rfc7628) +- [RFC 7636: Proof Key for Code Exchange by OAuth Public Clients](https://www.rfc-editor.org/rfc/rfc7636) - [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html) - [OpenID Connect Dynamic Client Registration 1.0](https://openid.net/specs/openid-connect-registration-1_0.html) & [RFC 7591: OAuth 2.0 Dynamic Client Registration](https://www.rfc-editor.org/rfc/rfc7591.html) - [OpenID Connect Discovery 1.0](https://openid.net/specs/openid-connect-discovery-1_0.html) @@ -121,6 +122,13 @@ } ``` +The [Proof Key for Code Exchange][RFC 7636] mitigation method can be +made required: + +```lua +oauth2_require_code_challenge = true +``` + ## Deployment notes ### Access management diff -r 12498c0d705f -r df11a2cbc7b7 mod_http_oauth2/mod_http_oauth2.lua --- a/mod_http_oauth2/mod_http_oauth2.lua Sat Apr 29 11:26:04 2023 +0200 +++ b/mod_http_oauth2/mod_http_oauth2.lua Sat Apr 29 13:09:46 2023 +0200 @@ -17,6 +17,10 @@ local array = require "util.array"; local st = require "util.stanza"; +local function b64url(s) + return (s:gsub("[+/=]", { ["+"] = "-", ["/"] = "_", ["="] = "" })) +end + local function read_file(base_path, fn, required) local f, err = io.open(base_path .. "/" .. fn); if not f then @@ -69,6 +73,8 @@ local registration_algo = module:get_option_string("oauth2_registration_algorithm", "HS256"); local registration_options = module:get_option("oauth2_registration_options", { default_ttl = 60 * 60 * 24 * 90 }); +local pkce_required = module:get_option_boolean("oauth2_require_code_challenge", false); + local verification_key; local jwt_sign, jwt_verify; if registration_key then @@ -211,6 +217,7 @@ local grant_type_handlers = {}; local response_type_handlers = {}; +local verifier_transforms = {}; function grant_type_handlers.password(params) local request_jid = assert(params.username, oauth_error("invalid_request", "missing 'username' (JID)")); @@ -236,12 +243,18 @@ end local granted_scopes, granted_role = filter_scopes(request_username, params.scope); + if pkce_required and not params.code_challenge then + return oauth_error("invalid_request", "PKCE required"); + end + local code = id.medium(); local ok = codes:set(params.client_id .. "#" .. code, { expires = os.time() + 600; granted_jid = granted_jid; granted_scopes = granted_scopes; granted_role = granted_role; + challenge = params.code_challenge; + challenge_method = params.code_challenge_method; id_token = id_token; }); if not ok then @@ -340,6 +353,14 @@ return oauth_error("invalid_client", "incorrect credentials"); end + -- TODO Decide if the code should be removed or not when PKCE fails + local transform = verifier_transforms[code.challenge_method or "plain"]; + if not transform then + return oauth_error("invalid_request", "unknown challenge transform method"); + elseif transform(params.code_verifier) ~= code.challenge then + return oauth_error("invalid_grant", "incorrect credentials"); + end + return json.encode(new_access_token(code.granted_jid, code.granted_role, code.granted_scopes, client, code.id_token)); end @@ -371,6 +392,18 @@ )); end +-- RFC 7636 Proof Key for Code Exchange by OAuth Public Clients + +function verifier_transforms.plain(code_verifier) + -- code_challenge = code_verifier + return code_verifier; +end + +function verifier_transforms.S256(code_verifier) + -- code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) + return code_verifier and b64url(hashes.SHA256(code_verifier)); +end + -- Used to issue/verify short-lived tokens for the authorization process below local new_user_token, verify_user_token = jwt.init("HS256", random.bytes(32), nil, { default_ttl = 600 }); @@ -903,6 +936,7 @@ registration_endpoint = handle_register_request and module:http_url() .. "/register" or nil; scopes_supported = usermanager.get_all_roles and array(it.keys(usermanager.get_all_roles(module.host))):append(array(openid_claims:items())); response_types_supported = array(it.keys(response_type_handlers)); + code_challenge_methods_supported = array(it.keys(verifier_transforms)); authorization_response_iss_parameter_supported = true; -- OpenID