As per launchpad bug at:
https://bugs.launchpad.net/keystone/+bug/2157347
Two related single-use enforcement gaps exist in Keystone's MFA authentication
flow. They are independent bugs that compound each other.
Bug 1 — MFA auth receipt not consumed after use
------------------------------------------------
MFA auth receipts, introduced in the Stein release (bp mfa-auth-receipt), are
never invalidated after being used to complete authentication.
In keystone/receipt/handlers.py, extract_receipt() validates the receipt but
never revokes it:
def extract_receipt(auth_context):
receipt_id = flask.request.headers.get(
authorization.AUTH_RECEIPT_HEADER, None)
if receipt_id:
receipt = PROVIDERS.receipt_provider_api.validate_receipt(
receipt_id)
if auth_context['user_id'] != receipt.user_id:
raise exception.ReceiptNotFound(...)
return receipt
After authentication.py issues a full token using the receipt, there is no
call to consume it. The receipt (a Fernet token) is stateless, so without
explicit tracking it can be presented any number of times until it expires
(default 300 seconds / 5 minutes).
The original spec (mfa-auth-receipt.rst, Security Impact section) acknowledged
this: "an eventual action we should take is to do some form of receipt
revocations when turned into a token." That action was never taken.
Bug 2 — TOTP passcode not invalidated after use
------------------------------------------------
The TOTP authentication plugin (keystone/auth/plugins/totp.py) never records
which passcodes have been accepted. The same 6-digit code can be submitted
multiple times within its validity window and each submission authenticates
successfully (line 109):
if auth_passcode in generated_passcodes:
valid_passcode = True
By default Keystone accepts the current 30-second window plus one previous
window ([totp] included_previous_windows = 1), giving an effective replay
window of up to 60 seconds. RFC 6238 Section 5.2 recommends that servers
mark a passcode as used and reject reuse within the same time step.
This bug affects both the challenge-response flow (password → receipt → TOTP)
and the all-upfront flow (password + TOTP in one request).
Impact
------
Bug 2 (TOTP replay): an attacker who intercepts a TOTP code can resubmit it
within the ~60-second validity window to obtain additional tokens.
Bug 1 (receipt replay): one completed victim authentication can be reused
multiple times within the 5-minute receipt TTL. The receipt appears in the
401 response header; each time the attacker can observe a fresh TOTP code
within that window, they can pair it with the captured receipt to obtain
another full token — without the victim authenticating again.
The receipt is notable because it appears in response headers, which are
more commonly logged than request bodies. An attacker with access to
server-side logs or response traffic can capture a receipt without capturing
the password.
The spec compared receipt replay to "users can already have multiple tokens."
That is wrong: every legitimate token required the victim to complete MFA
independently. Receipt replay allows the attacker to complete additional MFA
authentications on the victim's behalf from a single captured receipt.
Verified on devstack (Keystone 29.x, 2026.1 cycle):
Bug 1 (receipt replay across TOTP windows):
- Step 1: password → receipt. Complete MFA with receipt + TOTP code T1 → token.
- Waited for TOTP window to rotate (31s). New code T2 generated (T1 ≠ T2).
- Same receipt + T2 → HTTP 201, distinct valid token.
- Receipt was not consumed after first use.
Bug 2 (TOTP replay within window):
- Same TOTP code submitted 3 times in succession.
- All 3 returned HTTP 201 with distinct, independently valid tokens.
Severity: high. MFA is deployed when passwords are considered insufficient.
These bugs together reduce hardware MFA from a persistent barrier requiring
physical presence on every auth, to a one-time obstacle: the attacker needs
only one observation of decrypted traffic. Bug 1 alone reduces it to one
observation per 5-minute receipt window. Bug 2 alone reduces it to multiple
tokens from one observation within 60 seconds.
Suggested fix
-------------
Both bugs can be fixed using oslo.cache with appropriate TTLs:
Bug 1: after a receipt is used to issue a full token, store the receipt ID
in cache with TTL = receipt expiry (300 seconds). Reject any subsequent
request presenting a cached receipt ID.
Bug 2: after a TOTP passcode is accepted, store
"totp_used:{user_id}:{passcode}" in cache with TTL =
(included_previous_windows + 1) * 30 seconds (default 60 seconds). Reject
any subsequent request for the same user+passcode combination.
If the cache backend is the null backend (caching disabled), log a warning
that replay protection is degraded.
Both fixes require no database migration and are straightforward to backport.
The same oslo.cache pattern is proposed for the related OAuth1 nonce replay
fix (LP#2157345).
References:
- Original spec (Stein): https://specs.openstack.org/openstack/keystone-specs/specs/keystone/stein/mfa-auth-receipt.html
- Implementing commit: d9e6c1d4d ("Implement auth receipts spec")
- RFC 6238 Section 5.2 (TOTP single-use recommendation)
- keystone/receipt/handlers.py, keystone/auth/plugins/totp.py
Affected versions: All Keystone releases since Stein (19.0.0) for Bug 1;
Rocky (17.0.0+) for Bug 2 (when TOTP is enabled).