fix(api): 登录验证码与 passkey 挑战不再被他人的 start 作废,冷却内重复 start 照常 202,登录挑战按来源限量

This commit is contained in:
Lemon-miaow committed 2026-09-25 16:37:03 +08:00
1 parent 084ba1ed9e
commit 4757353324
22 files changed
+1290 -314

No files matched your search

+26 -18
View File
@@ -2128,8 +2128,11 @@ paths:
matching challenge is stashed server-side and redeemed by finish. Mounted
Public (no prior principal) and gated on local_auth_enabled. An unknown
address and a known account with no enrolled passkey both return the SAME 400
no_passkey, so the door is not an existence oracle; a per-recipient cooldown
(shared shape with the email-OTP and op-login doors) throttles probing.
no_passkey, so the door is not an existence oracle; the per-address sign-in
rate limit bounds probing. Each begin stashes a ceremony of its own beside the
account's other live ones, so a begin by anyone who knows the address never
cancels its owner's. One network (an IPv4 address or IPv6 /48) holds at most 32
live login challenges (429 too_many_challenges past that).
x-felis-face: [external]
x-felis-tier: public
security: []
@@ -2171,7 +2174,7 @@ paths:
schema: { $ref: '#/components/schemas/Error' }
'429':
description: >-
A passkey login for this recipient was started too recently (otp_resend_cooldown);
This network already holds 32 live passkey login challenges (too_many_challenges);
or this client address called the sign-in doors too often (rate_limited, with Retry-After).
content:
application/json:
@@ -2190,12 +2193,12 @@ paths:
description: >-
Second leg of the public passkey door: the caller returns the email (to
re-select the account) and the raw navigator.credentials.get() assertion. The
stashed login challenge is consumed atomically and the assertion is verified
against it; on success a host-only felis_session cookie is minted. Both players
live login challenge whose value the assertion signed (response.clientDataJSON)
is consumed atomically and the assertion is verified against it; on success a host-only felis_session cookie is minted. Both players
and staff may log in this way — a passkey is a two-factor authenticator
(possession + user verification), strong enough to stand alone without the
in-game approval op-login requires. Every failure mode (unknown address, no
live challenge, expired challenge, bad assertion) collapses into one uniform
live challenge for the signed value, expired challenge, bad assertion) collapses into one uniform
passkey_login_invalid, so the door reveals nothing.
x-felis-face: [external]
x-felis-tier: public
@@ -2266,10 +2269,10 @@ paths:
credential it holds for this RP and the account is revealed only by the
userHandle inside the signed assertion at finish. The challenge cannot be
user-keyed, so it is stashed under login_id in a non-user-keyed store and echoed
back at finish. Mounted Public and gated on local_auth_enabled. There is no
recipient or principal to key a per-caller cooldown on, so one client is bounded
by the per-address sign-in rate limit (429 rate_limited) and the table by a hard
global cap on live challenges (429 too_many_challenges). Inert for a credential until its owner
back at finish. Mounted Public and gated on local_auth_enabled. One client is
bounded by the per-address sign-in rate limit (429 rate_limited), one network
(an IPv4 address or IPv6 /48) to 32 live challenges, and the table by a hard
global cap of 16384 (both 429 too_many_challenges). Inert for a credential until its owner
enrolls a resident passkey; email-OTP and username-first passkey remain the
fallbacks, so no authenticator is ever locked out.
x-felis-face: [external]
@@ -2315,9 +2318,9 @@ paths:
schema: { $ref: '#/components/schemas/Error' }
'429':
description: >-
Too many discoverable logins are in flight server-wide (too_many_challenges;
the cap is global, so no per-recipient signal leaks); or this client address
called the sign-in doors too often (rate_limited, with Retry-After).
This network already holds 32 live discoverable challenges, or the store is at
its global cap (too_many_challenges); or this client address called the
sign-in doors too often (rate_limited, with Retry-After).
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
@@ -2413,6 +2416,10 @@ paths:
purpose. An address with no account returns the SAME 202 with no code minted,
and the per-recipient cooldown is kept on that path too, so probing reveals
nothing (existence is learnt only at the sanctioned /auth/options oracle).
One code is mailed per recipient per minute: a start inside that window gets
the same 202 (expires_at of the live code) and mails nothing. A start never
cancels the codes already mailed; the three newest live codes all work, and
signing in with one spends the rest.
An account that spent its daily wrong-code budget (10 per 24h, across every
code) also gets the same 202 and no mail until the window ends. Gated on
local_auth_enabled.
@@ -2458,8 +2465,7 @@ paths:
schema: { $ref: '#/components/schemas/Error' }
'429':
description: >-
A code for this recipient was requested too recently (otp_resend_cooldown);
or this client address called the sign-in doors too often (rate_limited, with Retry-After);
This client address called the sign-in doors too often (rate_limited, with Retry-After);
or the install-wide mail budget is spent (mail_rate_limited, with Retry-After).
content:
application/json:
@@ -2539,7 +2545,10 @@ paths:
op_login purpose, returning the request handle the browser polls. A non-staff
or unknown address gets the SAME 202 with a random, non-persisted handle and no
mail, so this never becomes a staff-enumeration oracle. A staff account that
spent its daily wrong-code budget gets the same neutral 202. Gated on
spent its daily wrong-code budget gets the same neutral 202. One code is mailed
per recipient per minute: a staff start inside that window opens a real request
but mails nothing, and the code already in the inbox finishes it. A start never
cancels the codes already mailed (the three newest live codes all work). Gated on
local_auth_enabled.
x-felis-face: [external]
x-felis-tier: public
@@ -2583,8 +2592,7 @@ paths:
schema: { $ref: '#/components/schemas/Error' }
'429':
description: >-
A code for this recipient was requested too recently (otp_resend_cooldown);
or this client address called the sign-in doors too often (rate_limited, with Retry-After);
This client address called the sign-in doors too often (rate_limited, with Retry-After);
or the install-wide mail budget is spent (mail_rate_limited, with Retry-After).
content:
application/json: