What does a server store when it issues a password-reset token, and what does the emailed link carry?
answer
- a second way into the account
- the link holds what the row cannot
- store a hash, never the value
- expiry and consumed marker are columns
basics
~20 sThe reset row stores a hash of the token, the account it belongs to, an expiry the server compares, and a consumed marker. The raw token exists once, inside the link that is sent, and is never written down.
solid answer
~50 sTreat a password-reset token as a credential the server issues to itself. Draw a long random value, hash it, and store the hash — never the value — next to the account id, an expiry timestamp and a consumed marker that starts empty. The raw value goes into the link and nowhere else: not the row, not a support tool. On presentation you hash what you were handed and look that hash up, so a read of the reset table yields fingerprints that open nothing. The expiry has to be a stored value the server compares, not a time printed in the email, because only a comparison the server makes is enforceable. A fast cryptographic hash is the right choice here: the token is random rather than human-chosen, so there is no guess space for a work factor to price.
code
pseudocode · 14 lineson request_reset(address):
account = find_account(address)
if account exists:
raw = random_token() # bit count is a separate subject
store_reset_row(
account_id = account.id,
token_hash = hash(raw), # fast cryptographic hash
issued_at = now(),
expires_at = now() + RESET_WINDOW,
consumed_at = null)
send_link(address, link_with(raw)) # the only place raw ever exists
respond(SAME_ACKNOWLEDGEMENT) # same body, same status, either waygo deeper
Know the row by heart: a hash of the token, the account it belongs to, when it stops working, and whether it has been used. The raw value lives in the link and nowhere else.
Explain why the lookup goes through the hash. A read of that table is a common event, and hashing turns it into a list of fingerprints that open nothing. Then say where the expiry is actually enforced.
Say what the window is traded against: a member recovering minutes before a launch window versus a link sitting unused in a mailbox for a week. Say what you do about links that are issued and never opened.
Treat the recovery path as a full authentication path with its own record, its own clock and its own audit trail, and hold it to the standard of the primary path. An account is only as strong as the weakest way into it.
## The reset flow is a second way in A hobbyist rocketry club runs a launch-authorisation site: a qualified member signs in and authorises a flight inside a window that may be only a few hours wide. The password is one way into that account. The password-reset flow is a second way in, and on most sites it is the path written last and reviewed least. Whatever that path accepts as proof is the account's real strength, so the value it hands out — a token carried in a link — is a credential the server issues to itself, and it deserves what any other stored credential gets. ## What the server writes down When a member asks for a link, the server writes one row and sends one message. The row holds: - **a hash of the token**, never the token itself; - **the account it belongs to**, so a presented token cannot be pointed at a different member; - **when it was issued**, which is what any decision about repeated requests is measured from; - **when it stops working**, as a stored value compared at consumption; - **a consumed marker**, empty at issue — this is the single-use gate, not a nicety; - optionally **where the request came from**, which is the evidence you want when a member asks why a link arrived that they never requested. | value | where it lives | why there | |---|---|---| | the raw token | the link, once | it is the proof, so the member must be able to present it | | a hash of the raw token | the row | a read of the table yields nothing that opens an account | | the expiry | the row | only a comparison the server performs is enforceable | | the consumed marker | the row | one link, one password change | | the account id | the row | a token proves nothing on its own about who it is for | The row can live in a relational table or as an expiring entry in a shared key-value store; the shape of the record does not change. ## Store the hash, not the value The realistic threat against this table is a **read**, not a write: a copied backup, a replica nobody remembered, an over-scoped query in an internal tool, an export made for debugging. If the rows hold raw tokens, whoever gets that read opens every account with an outstanding link — and can manufacture outstanding links on demand, simply by asking the site to send them. If the rows hold hashes, the same read yields a list of fingerprints that unlock nothing. Consumption then runs the other way round: hash the value you were handed, look that hash up, and decide on the row you find. The hash is the lookup column, so it is the column that carries the index. Some servers compute that hash under a server-held key — an `HMAC` rather than a bare digest — so a database read alone cannot confirm even a guessed value. The trade is operational: the key has to be present wherever consumption runs, and rotating it invalidates every outstanding link at once. ## A fast hash is the right hash here A deliberately slow key-derivation function exists to price guessing a **human-chosen** secret: a short, low-entropy value an attacker can enumerate. A reset token is not human-chosen. The server draws it at random, and with enough randomness behind it there is no guess space for a work factor to price. A fast cryptographic hash such as SHA-256 is correct, and it keeps consumption quick at the moment a member is trying to get back in before a window closes. How many bits the token carries, and how those bits become the characters in the link, is a separate subject with its own answer. ## Choosing the window The expiry is a judgment about the member, not a round number. Someone recovering at the launch field on a phone with one bar is slower than someone at a desk. Too short and the member burns three links and gives up on the flight; too long and a link sits in a mailbox for a week, surviving whatever happens to that mailbox in the meantime. Pick the window from how the link is delivered and how fast that channel really is — and remember that a short window is **not** a substitute for single-use consumption. It only bounds how long a spare stays useful. ## Two things this record deliberately does not decide 1. **The absolute URL the link is built from.** Assembling it out of a value the incoming request controls is its own hazard with its own answer; the record just carries the token. 2. **What happens at the moment of consumption.** Marking the row used, killing the account's other outstanding links and ending its sessions is the next question, and it is where most of the real defects live.
- Does a password-reset token need the same deliberately slow function used on a stored password?No. The work factor prices guessing a short, human-chosen secret. A reset token is drawn at random by the server, so there is no small guess space to enumerate, and a fast cryptographic hash gives the property you actually want: a table read that yields nothing usable. A slow function here only adds latency on the path a locked-out member is already waiting on.
- What is an honest upper bound on a reset link's lifetime, and what decides it?It is set by the channel and the member, not by a round number: how quickly the message arrives, and how long someone recovering away from a desk realistically takes to open it. Beyond that the link is just a spare sitting in a mailbox. Lengthening the window to cut support traffic is a defensible trade; treating the window as the single-use control is not.
- Is it worth recording which source asked for the link?Yes, as evidence rather than as a control. When a member reports a link they never requested, the issued-at time and the requesting source are what let you tell one curious stranger from someone working through the club roster. It is not a check at consumption: requiring the same source would break the common case of asking on one device and opening the mail on another.
A left-luggage ticket. The counter keeps a stub that matches the ticket but is not the ticket, and the ticket itself only exists in the customer's pocket. Someone who photographs the counter's ledger walks away with nothing they can present.
saying these in an interview costs you the question
- Storing the reset token in plain text so support can resend it
- Building the token from the member's address and a timestamp
- Saying the reset token needs the same work factor as a stored password
- Trusting an expiry printed in the email with no column to compare
- Calling the link safe because it is long, while the table holds it raw