)]}'
{"/COMMIT_MSG":[{"author":{"_account_id":14250,"name":"Grzegorz Grasza","email":"xek@redhat.com","username":"xek"},"change_message_id":"1dd1b4909a1b411fb4b3bb4b16ca39ce74b1f753","unresolved":true,"context_lines":[{"line_number":6,"context_line":""},{"line_number":7,"context_line":"Fix: TOTP authentication lockout risk in Keystone"},{"line_number":8,"context_line":""},{"line_number":9,"context_line":"Enabling TOTP authentication in Keystone without users already having TOTP"},{"line_number":10,"context_line":"credentials can result in authentication being completely blocked."},{"line_number":11,"context_line":"Furthermore, if a user (or an administrator) deletes an existing TOTP"},{"line_number":12,"context_line":"credential while TOTP is enforced as a mandatory authentication method,"}],"source_content_type":"text/x-gerrit-commit-message","patch_set":7,"id":"3cf470c5_cd4e426a","line":9,"updated":"2026-08-26 19:39:36.000000000","message":"nit: describe the knob you actually check - user MFA rules. “Enabling TOTP” in Keystone usually means adding totp to the global [auth] methods list.","commit_id":"c3622da3e0927946e7c176ce5ded80f323e2dfc8"}],"/PATCHSET_LEVEL":[{"author":{"_account_id":36012,"name":"Benjamin Lasseye","display_name":"Benjamin Lasseye","email":"blasseye@ikmail.com","username":"blasseye"},"change_message_id":"96ecc4071315882674660b227d25315f3fa6c51c","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":6,"id":"91cf18c6_5262db53","updated":"2026-08-03 09:46:53.000000000","message":"recheck","commit_id":"b6453c7505800fd59942d59c3dc19f79003081cd"},{"author":{"_account_id":36012,"name":"Benjamin Lasseye","display_name":"Benjamin Lasseye","email":"blasseye@ikmail.com","username":"blasseye"},"change_message_id":"6857ce24613b642da7daa6cd7a7ddb3022e3c62e","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":7,"id":"ec719054_2756e3cd","updated":"2026-08-31 07:52:07.000000000","message":"@kevin@stackops.ch Thank you so much for your detailed feedback.\n\nI’m actually working on it right now. \nCurrently, in a version I’ll likely push soon, I’m looking into removing the required totp role if there are no credentials. This allows me to avoid conflicting with what’s already there: https://review.opendev.org/c/openstack/horizon/+/977939\nIn *keystone/auth/core.py* at *check_auth_methods_against_rules* :\n\n```\n        for r in rules:\n            # NOTE(notmorgan): We only check against the actually loaded\n            # auth methods meaning that the keystone administrator may\n            # disable an auth method, and a rule will still pass making it\n            # impossible to accidently lock-out a subset of users with a\n            # bad keystone.conf\n            # NOTE(gtema): https://bugs.launchpad.net/keystone/+bug/2102096\n            # requires that the rule containing only wrong methods is ignored\n            r_set \u003d set(r).intersection(cls._auth_methods())\n            # NOTE: If the rule requires totp but the user has no TOTP\n            # credentials registered, strip totp from the rule to prevent\n            # lockout. This covers misconfigured or in-provisioning users.\n            if \u0027totp\u0027 in r_set:\n                totp_creds \u003d PROVIDERS.credential_api.list_credentials_for_user(\n                    user_id, type\u003d\u0027totp\u0027\n                )\n                if not totp_creds:\n                    msg \u003d (\n                        \u0027User %s has no TOTP credentials configured; \u0027\n                        \u0027the TOTP requirement in MFA rule %s is ignored \u0027\n                        \u0027to prevent lockout. Register a TOTP credential \u0027\n                        \u0027to enforce this rule.\u0027 % (user_id, sorted(r_set))\n                    )\n                    LOG.warning(msg)\n                    r_set \u003d r_set - {\u0027totp\u0027}\n            if r_set:\n                has_valid_auth_methods \u003d True\n            if r_set and set(auth_methods).issuperset(r_set):\n                # Rule Matches no need to continue, return here.\n                LOG.debug(\n                    \u0027Auth methods for user `%(user_id)s`, `%(methods)s` \u0027\n                    \u0027matched MFA rule `%(rule)s`. Loaded \u0027\n                    \u0027auth_methods: `%(loaded)s`\u0027,\n                    {\n                        \u0027user_id\u0027: user_id,\n                        \u0027rule\u0027: list(r_set),\n                        \u0027methods\u0027: auth_methods,\n                        \u0027loaded\u0027: cls._auth_methods(),\n                    },\n                )\n                return True\n```\nIt\u0027s easier to set up. But it\u0027s less intuitive for the user maybe.","commit_id":"c3622da3e0927946e7c176ce5ded80f323e2dfc8"},{"author":{"_account_id":14250,"name":"Grzegorz Grasza","email":"xek@redhat.com","username":"xek"},"change_message_id":"e315bbb3b930c3954a51105839c3da3bff85319b","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":7,"id":"0045ae1b_3046dcd5","updated":"2026-08-26 19:40:14.000000000","message":"Additional finding:\n\n  The new validation lives only in update_user (PATCH /v3/users/{id}). create_user (POST /v3/users) copies options through and\n  never looks at multi_factor_auth_rules.\n\n  So this still locks the user on first login:\n\n  POST /v3/users\n  {\n    \"user\": {\n      \"name\": \"alice\",\n      \"password\": \"...\",\n      \"options\": {\n        \"multi_factor_auth_enabled\": true,\n        \"multi_factor_auth_rules\": [[\"password\", \"totp\"]]\n      }\n    }\n  }\n\n  No TOTP credential exists yet, so they cannot satisfy the rule and cannot enroll after the fact if you also block TOTP create\n  without already being authenticated… actually create_credential is ungated, but they cannot authenticate to create the credential\n  if MFA already requires totp.\n\n  Same failure mode as the bug, different verb. “Same check” means: if you reject that options payload on PATCH, reject it on POST\n  too.","commit_id":"c3622da3e0927946e7c176ce5ded80f323e2dfc8"},{"author":{"_account_id":14250,"name":"Grzegorz Grasza","email":"xek@redhat.com","username":"xek"},"change_message_id":"1dd1b4909a1b411fb4b3bb4b16ca39ce74b1f753","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":7,"id":"6ce921ef_80c4e639","updated":"2026-08-26 19:39:36.000000000","message":"Thanks for tackling LP #2150088. See my comments inline. also:\n\n* This block rotation. You should probably only block the last TOTP, when totp is actually required.\n* This needs a guard update_credential if type is changed away from totp, but should be covered by https://bugs.launchpad.net/keystone/+bug/2159643\n* Hit the REST APIs in tests; add optional-totp, MFA-disabled, last-vs-many","commit_id":"c3622da3e0927946e7c176ce5ded80f323e2dfc8"},{"author":{"_account_id":36012,"name":"Benjamin Lasseye","display_name":"Benjamin Lasseye","email":"blasseye@ikmail.com","username":"blasseye"},"change_message_id":"8d65abc8cadb96d120fef0944b6c05b8694f72f3","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":7,"id":"d4a8a011_58690e80","in_reply_to":"0045ae1b_3046dcd5","updated":"2026-08-28 09:25:07.000000000","message":"Thanks for your review,\nI realize I\u0027ve made things more complicated.\nThe best solution would be to modify /auth/plugins/totp.py so that MFA isn\u0027t required when no TOTP credentials are configured.\nAnd add a warning to notify the user that a credential is missing.","commit_id":"c3622da3e0927946e7c176ce5ded80f323e2dfc8"},{"author":{"_account_id":36012,"name":"Benjamin Lasseye","display_name":"Benjamin Lasseye","email":"blasseye@ikmail.com","username":"blasseye"},"change_message_id":"8d65abc8cadb96d120fef0944b6c05b8694f72f3","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":7,"id":"06d1b743_98dbe055","in_reply_to":"6ce921ef_80c4e639","updated":"2026-08-28 09:25:07.000000000","message":"I wasn\u0027t aware of bug #2159643.\nThanks for your feedback.","commit_id":"c3622da3e0927946e7c176ce5ded80f323e2dfc8"},{"author":{"_account_id":36012,"name":"Benjamin Lasseye","display_name":"Benjamin Lasseye","email":"blasseye@ikmail.com","username":"blasseye"},"change_message_id":"d1134ed1023ffc1ea5ca3ad41ae6cd80e8456dcd","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":7,"id":"119ba0c8_62784bcf","in_reply_to":"d4a8a011_58690e80","updated":"2026-08-28 12:15:17.000000000","message":"More like: /auth/core.py in check_auth_methods_against_rules()","commit_id":"c3622da3e0927946e7c176ce5ded80f323e2dfc8"},{"author":{"_account_id":36012,"name":"Benjamin Lasseye","display_name":"Benjamin Lasseye","email":"blasseye@ikmail.com","username":"blasseye"},"change_message_id":"c5c9df4c003fe57b4f2a30814c5f7c7209e94006","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":7,"id":"2fd77515_b1b1f734","in_reply_to":"ec719054_2756e3cd","updated":"2026-08-31 08:44:53.000000000","message":"Maybe, add a header \"X-Auth-Warning\" to informe the user.\nWhat do you think about it?","commit_id":"c3622da3e0927946e7c176ce5ded80f323e2dfc8"},{"author":{"_account_id":36012,"name":"Benjamin Lasseye","display_name":"Benjamin Lasseye","email":"blasseye@ikmail.com","username":"blasseye"},"change_message_id":"12d375d0d5b6a93956dfa9927b4803bb4084e585","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":13,"id":"75f7a819_1fb76b3c","updated":"2026-09-03 22:01:44.000000000","message":"The fix touches two components:\n\n**keystone/auth/core.py — MFA rule evaluator**\n\nBefore this change, a user with an MFA rule like [\"password\", \"totp\"] and no TOTP credential enrolled would be unable to authenticate at all: the rule required TOTP, but there was nothing to validate against. This effectively locked the user out with no recovery path short of admin intervention.\nThe fix adds a pre-evaluation check: when totp appears in a rule, we query the user\u0027s credentials. If no TOTP credential exists and the user did not submit totp as an auth method, we strip totp from that rule before evaluation and emit a LOG.warning to inform operators. This allows the user to still satisfy the rule via the remaining methods (e.g. password), preventing accidental lockout during TOTP enrollment.\nA few edge cases worth noting:\n- If the rule is [\"totp\"] only (no other method), stripping totp leaves an empty rule. The rule is effectively dropped and contributes nothing to has_valid_auth_methods. Whether auth succeeds depends on the user\u0027s other rules.\n- If the user does submit totp despite having no credential, we do not strip it. The request falls through to the TOTP plugin, which now returns an explicit error (see below).\n\n**keystone/auth/plugins/totp.py — TOTP authentication plugin**\n\nBefore this change, when a user submitted totp but had no credential enrolled, the plugin would iterate over an empty list, leave valid_passcode \u003d False, and return the generic Invalid username or TOTP passcode error — giving no indication the credential was absent.\nThe fix adds an early check: if list_credentials_for_user(type\u003d\u0027totp\u0027) returns empty, we immediately raise Unauthorized with the message:\n\"No TOTP credentials are registered for this user. Choose another authentication method or remove \u0027totp\u0027 from the authentication methods.\"\nThis makes the failure mode explicit and actionable for both users and operators.\n\nFor example, if a user attempts to authenticate using `totp` but has no TOTP credential enrolled, the request now fails explicitly with an HTTP 401 response:\n\n```http\nHTTP/1.1 401 UNAUTHORIZED\nContent-Type: application/json\nWWW-Authenticate: Keystone uri\u003d\"http://172.24.96.232:5000/v3\"\nVary: X-Auth-Token\nx-openstack-request-id: req-66fa98be-4379-4fce-bcf4-ef7e3f14c675\n\n{\"error\":{\"code\":401,\"message\":\"No TOTP credentials are registered for this user. Choose another authentication method or remove \u0027totp\u0027 from the authentication methods. (Disable insecure_debug mode to suppress these details.)\",\"title\":\"Unauthorized\"}}\n```\n\nThis makes the failure mode clear to the user: the `totp` authentication method was requested, but no TOTP credential is registered for the account. The user can either choose another authentication method or enroll a TOTP credential before attempting TOTP authentication again.","commit_id":"55e14cefeb392d9c57eb57aa548a02e9aec570fc"},{"author":{"_account_id":36012,"name":"Benjamin Lasseye","display_name":"Benjamin Lasseye","email":"blasseye@ikmail.com","username":"blasseye"},"change_message_id":"49978a6b4b090247f73f28dfc0c4c6a4786826aa","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":13,"id":"833c5576_adfa238b","in_reply_to":"75f7a819_1fb76b3c","updated":"2026-09-03 22:05:12.000000000","message":"To see the error message, you need to enable `insecure_debug`:\n\n```ini\n# keystone.conf\n[DEFAULT]\ninsecure_debug \u003d true\n```\n\nOtherwise, the error details are only available in the logs.","commit_id":"55e14cefeb392d9c57eb57aa548a02e9aec570fc"},{"author":{"_account_id":36012,"name":"Benjamin Lasseye","display_name":"Benjamin Lasseye","email":"blasseye@ikmail.com","username":"blasseye"},"change_message_id":"bb4e645f3820af30fb1eb93febb2f999fe8b1b95","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":14,"id":"42e68d31_120687a9","updated":"2026-09-04 08:16:38.000000000","message":"I don\u0027t think I\u0027ve missed anything from your comments in your review now.\nA huge thank you @kevin@stackops.ch for taking the time.","commit_id":"5ead745f3354d68e140e7465252935cb72e17251"},{"author":{"_account_id":36012,"name":"Benjamin Lasseye","display_name":"Benjamin Lasseye","email":"blasseye@ikmail.com","username":"blasseye"},"change_message_id":"84731d3bb683d614680f2c11c1a2697e480c13ac","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":14,"id":"ca519ea2_54cb7611","updated":"2026-09-04 12:06:57.000000000","message":"recheck","commit_id":"5ead745f3354d68e140e7465252935cb72e17251"},{"author":{"_account_id":36012,"name":"Benjamin Lasseye","display_name":"Benjamin Lasseye","email":"blasseye@ikmail.com","username":"blasseye"},"change_message_id":"3b48cb6747270f4606d5bd890fce70c1caac3203","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":14,"id":"1a78dc18_6f45aa40","updated":"2026-09-04 12:02:56.000000000","message":"recheck\n\nPretty strange—after a second recheck, it failed again, but not on the same test. As before, maybe it\u0027s an infrastructure issue...","commit_id":"5ead745f3354d68e140e7465252935cb72e17251"}],"keystone/credential/core.py":[{"author":{"_account_id":14250,"name":"Grzegorz Grasza","email":"xek@redhat.com","username":"xek"},"change_message_id":"1dd1b4909a1b411fb4b3bb4b16ca39ce74b1f753","unresolved":true,"context_lines":[{"line_number":194,"context_line":"            user_options \u003d user.get(\u0027options\u0027, {})"},{"line_number":195,"context_line":"            mfa_rules \u003d user_options.get(\u0027multi_factor_auth_rules\u0027, [])"},{"line_number":196,"context_line":"            totp_in_rules \u003d any("},{"line_number":197,"context_line":"                \u0027totp\u0027 in rule for rule in mfa_rules if isinstance(rule, list)"},{"line_number":198,"context_line":"            )"},{"line_number":199,"context_line":"            if totp_in_rules:"},{"line_number":200,"context_line":"                raise exception.Forbidden("}],"source_content_type":"text/x-python","patch_set":7,"id":"243c8d6c_aba58cc6","line":197,"updated":"2026-08-26 19:39:36.000000000","message":"Treats “totp in any rule” as required. [[\"password\"], [\"password\", \"totp\"]] still authenticates with password only (UserMFARulesValidator). The patch even had to add _create_totp_cred() to tests that are not lockouts.","commit_id":"c3622da3e0927946e7c176ce5ded80f323e2dfc8"},{"author":{"_account_id":14250,"name":"Grzegorz Grasza","email":"xek@redhat.com","username":"xek"},"change_message_id":"1dd1b4909a1b411fb4b3bb4b16ca39ce74b1f753","unresolved":true,"context_lines":[{"line_number":197,"context_line":"                \u0027totp\u0027 in rule for rule in mfa_rules if isinstance(rule, list)"},{"line_number":198,"context_line":"            )"},{"line_number":199,"context_line":"            if totp_in_rules:"},{"line_number":200,"context_line":"                raise exception.Forbidden("},{"line_number":201,"context_line":"                    message\u003d_("},{"line_number":202,"context_line":"                        \u0027Cannot delete TOTP credential while \u0027"},{"line_number":203,"context_line":"                        \u0027MFA rules require TOTP. Disable MFA rules first.\u0027"}],"source_content_type":"text/x-python","patch_set":7,"id":"fbb2c035_32d0a875","line":200,"updated":"2026-08-26 19:39:36.000000000","message":"Forbidden is a SecurityError, so “Disable MFA rules first” is stripped unless insecure_debug. Client gets a generic 403. Use ValidationError, Conflict, or ForbiddenNotSecurity.","commit_id":"c3622da3e0927946e7c176ce5ded80f323e2dfc8"}],"keystone/identity/core.py":[{"author":{"_account_id":14250,"name":"Grzegorz Grasza","email":"xek@redhat.com","username":"xek"},"change_message_id":"1dd1b4909a1b411fb4b3bb4b16ca39ce74b1f753","unresolved":true,"context_lines":[{"line_number":1362,"context_line":"            mfa_rules \u003d user.get(\u0027options\u0027, {}).get("},{"line_number":1363,"context_line":"                \u0027multi_factor_auth_rules\u0027, []"},{"line_number":1364,"context_line":"            )"},{"line_number":1365,"context_line":"            if mfa_rules and any(\u0027totp\u0027 in rule for rule in mfa_rules):"},{"line_number":1366,"context_line":"                totp_creds \u003d ("},{"line_number":1367,"context_line":"                    PROVIDERS.credential_api.list_credentials_for_user("},{"line_number":1368,"context_line":"                        user_id, type\u003d\u0027totp\u0027"}],"source_content_type":"text/x-python","patch_set":7,"id":"6d74ef84_364bb6ae","line":1365,"updated":"2026-08-26 19:39:36.000000000","message":"Same issue as above, \"totp\" in any rule is not “TOTP required.”","commit_id":"c3622da3e0927946e7c176ce5ded80f323e2dfc8"}]}
