class

KemalIdentity::Postgres::ActionTokenRepository

Inherits KemalIdentity::Accounts::ActionTokenRepository < Reference < Object

Accounts::ActionTokenRepository over auth_action_tokens.

The interesting method is #consume, and what makes it correct is that it is a single conditional UPDATE ... RETURNING rather than a SELECT followed by an UPDATE. Two concurrent requests presenting the same reset link both reach the statement; PostgreSQL serialises them on the row, the second finds used_at IS NULL no longer true, and updates nothing. Exactly one gets a row back.

Written as a read-then-write it would be a race with an account takeover at the end of it.

Constants

COLUMNS = "id, account_id, purpose, token_digest, created_at, expires_at, used_at"
UNIQUE_VIOLATION = "23505"

Constructors

Instance methods

consume(digest : Bytes, purpose : Accounts::ActionPurpose, at : Time) : Accounts::ActionToken | Nil

Spends the token with this digest, for this purpose, and returns it — or nil.

Atomic: exactly one of any number of concurrent callers gets the token back.

purpose is part of the condition, not a label checked afterwards. A token issued to confirm an email address must not be redeemable to reset a password, or anybody able to trigger a confirmation message gets an account takeover.

Returns nil for expired, already used, wrong purpose, and unknown alike.

Source
create(token : Accounts::ActionToken) : Nil

Stores a newly issued token.

Raises KemalIdentity::InfrastructureError if the digest is already present. As with sessions, the unique index exists so that a collision is a loud error rather than two grants sharing a secret.

Source
delete_expired(before : Time) : Int32

Deletes rows past their expiry, returning the count.

Disk reclamation only. Correctness never depends on it: expiry is evaluated inside #consume.

Source
revoke_all_for_account(account_id : String, purpose : Accounts::ActionPurpose, at : Time) : Int32

Marks every outstanding token of this purpose for this account as used, returning how many it spent.

Issuing a new reset link invalidates the previous ones, so a link sitting in an old email — or in an inbox somebody else now controls — stops working. Also the right response to a completed password change.

Source