Symfony 8.2 New Published on

A reset link that only works once

UriSigner::sign() accepts a “version”: sign the link with the password hash, and it dies as soon as the password changes.

Topics covered: HttpFoundationSecurity

New in Symfony 8.2 (in development). UriSigner signs a URL: anyone who receives it can use it, nobody can tamper with it. But a signature doesn't say when the link must stop working. A password reset link stayed valid after use, as long as it hadn't expired (and without an expiration, forever). Anyone who found the email could change the password a second time.

The version

Symfony 8.2 adds an argument to sign(), check(), checkRequest() and verify(): a version. It goes into the signature computation, but not into the URL. As long as it doesn't change, the link is valid; as soon as it changes, it no longer is.

// when sending the email
$link = $signer->sign($url, new \DateInterval('PT30M'), $user->getPassword());

// when the link is opened
$signer->verify($request, $user->getPassword());

For a reset, the natural version is the password hash: as soon as the new password is saved, the hash changes, and every link sent until then dies with it. For a login link, it could be the date of the last login.

verify() throws an UnverifiedSignedUriException (404) when the signature doesn't match, and an ExpiredSignedUriException (403) when the link has expired; a link without a signature gives an UnsignedUriException (404).

Expiration becomes mandatory

A signed link without an expiration is valid forever. Symfony 8.2 deprecates sign() without an expiration, and 9.0 will require it. You pass it on each call, or once and for all:

# config/packages/framework.yaml
framework:
    uri_signer:
        expiration: 3600

Your task

MotDePasseController sends a reset link (the response shows it, instead of the email) then accepts it as many times as you like. The “Request a link” request in the “Requests” tab produces one.

  1. Sign the link with the password hash as the version, and verify it with the current hash: a link that has been used, or sent before the last change, must answer 404.
  2. Make it expire thirty minutes after it is sent.

Where the feature comes from: the original pull request.