mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-19 17:25:19 +03:00
fix(auth): give a sign-in code a budget of guesses and a short life
A six-digit code has a million values, and nothing was counting how many of them a caller tried. The code travelled in an encrypted cookie the caller held, verification compared against that cookie, and a wrong answer simply re-rendered the form. Nobody has to be the person the code was mailed to: type somebody else's address into the first screen and the code goes to their mailbox while the cookie stays with you. At that point the only thing between a stranger and an account is a million requests, and the constant-time comparison protecting the code was guarding a door you could just keep knocking on. Guesses are now counted on the server, under a name that changes with every code. That placement is the point: a counter kept beside the code, in the cookie, is a counter the guesser can wind back by replaying an older copy. Starting over is still allowed and still costs a fresh code sent to the mailbox being aimed at, which is where somebody notices. A correct code spends its record too, so its remaining guesses do not carry into the next one. The cookie also lived for twenty-four hours, which made the pin a password with a million possible values and a day to try them. Ten minutes now, and the code stops being accepted when the clock says so rather than when the cookie happens to go away. Resend had no limit either, so the button was a way to mail a stranger as fast as requests go out. Codes to one address are spaced, and one attempt at signing in can only ask for so many. Both refusals say the same thing on purpose. Which of the two it was is a fact about somebody else's mailbox.
This commit is contained in:
@@ -69,7 +69,13 @@ const DEFAULT_COPY = {
|
||||
/**
|
||||
* Copy for the resend button.
|
||||
*/
|
||||
code_resend: 'Resend'
|
||||
code_resend: 'Resend',
|
||||
/**
|
||||
* Error message when too many codes have been asked for, or too many
|
||||
* guesses made. Deliberately one message for both: which of the two it was
|
||||
* is a fact about somebody else's mailbox.
|
||||
*/
|
||||
rate_limited: 'Too many attempts. Wait a moment and start again.'
|
||||
};
|
||||
|
||||
export type CodeUICopy = typeof DEFAULT_COPY;
|
||||
@@ -124,6 +130,7 @@ export function CodeUI(props: CodeUIOptions): CodeProviderOptions {
|
||||
<Layout>
|
||||
<form data-component="form" method="post">
|
||||
{error?.type === 'invalid_claim' && <FormAlert message={copy.email_invalid} />}
|
||||
{error?.type === 'rate_limit' && <FormAlert message={copy.rate_limited} />}
|
||||
<input type="hidden" name="action" value="request" />
|
||||
<input
|
||||
data-component="input"
|
||||
@@ -151,6 +158,7 @@ export function CodeUI(props: CodeUIOptions): CodeProviderOptions {
|
||||
<Layout>
|
||||
<form data-component="form" class="form" method="post">
|
||||
{error?.type === 'invalid_code' && <FormAlert message={copy.code_invalid} />}
|
||||
{error?.type === 'rate_limit' && <FormAlert message={copy.rate_limited} />}
|
||||
{state.type === 'code' && (
|
||||
<FormAlert
|
||||
message={(state.resend ? copy.code_resent : copy.code_sent) + state.claims.email}
|
||||
|
||||
Reference in New Issue
Block a user