AUTH4D-345 Sign-in method for every proof, and the token budget before the client lookup

Two fixes from the security review of the AUTH4D-333 (repository file docs/api-spec/auth4d-333.md) commit. No request or response shape changes.

POST /oauth/authorize/continue: method for every proof

The sign-in method reported to Flows now comes from the redeemed proof’s purpose for every proof the hosted login can redeem. Before this change, Google, Apple, Microsoft, Epic and SMS sign-ins were reported as session. The method appears in the post_login trigger and the user.signed_in event. The trigger’s context.provider names the same provider, except for passkeys, which are not an identity provider.

proof_purpose Flows method context.provider
email_verification email email
discord_login discord discord
steam_login steam steam
google_login google google
apple_login apple apple
microsoft_login microsoft microsoft
epic_login epic epic
sms_verification phone phone
sso_login sso sso
passkey_login passkey (none)

Without a proof (a reused hosted-login session) the method stays session. After a terms screen (AUTH4D-160 (repository file docs/api-spec/auth4d-160.md)) the method of the original sign-in is kept. Email codes, magic links and passwords all redeem email_verification proofs, so Flows sees email for all three; the idp claim (AUTH4D-258 (repository file docs/api-spec/auth4d-258.md)) still tells them apart.

POST /oauth/token and POST /oauth/revoke: per-IP budget first

The per-IP budget (600 per minute, tokenRequestsPerIp) is now spent before the client is looked up. A request whose client_id does not exist spends a unit too, so a burst of made-up client ids from one address gets 429 slow_down instead of one database read each. A malformed client id is refused before any budget is spent or any read is made.

The per-client budget and the client secret budget are still spent after the client is found and before its secret is compared. Only their 429 carries CORS headers for the client’s registered origin. The per-IP 429 has none, because no client is known yet.

Source: docs/api-spec/auth4d-345.md