Symfony 8.2 New Published on

Require an SMS code before a transfer

AuthenticationMethodBadge and getAuthenticationProofs(): the token remembers how the user authenticated, and a voter can require a second factor.

Topics covered: Security

New in Symfony 8.2 (in development). Sudo mode tells you when the user proved their identity. A bank also wants to know how: a password is enough to check a balance, not to empty the account. Symfony 8.2 now records on the token every authentication method proven during the session, along with the time it was last used.

The token remembers the method

Each authenticator says so with a badge on the passport, using the names from RFC 8176, gathered in AuthenticationMethod:

return new Passport(
    new UserBadge($username),
    new CustomCredentials($verify, $code),
    [new AuthenticationMethodBadge(AuthenticationMethod::SMS)],
);

form_login, json_login and http_basic already do it, with AuthenticationMethod::PASSWORD. When the same user authenticates a second time, Symfony adds the new proof to the previous ones:

$token->getAuthenticationProofs();
// ['pwd' => 1792300000, 'sms' => 1792300060]

The method as the key, the Unix timestamp of its last use as the value. A voter just has to look.

Your task

Ada's bank (ada / lovelace) has a password login, a /verification page that sends a code by SMS (the fake carrier always sends 314159), and transfers open to any logged-in session. /accounts shows the session's proofs: you'll see yours appear.

  1. CodeSmsAuthenticator handles the code form. It checks the code of the user who is already logged in and says they authenticated by SMS. Without a password given first, a code logs nobody in.
  2. Declare it on the main firewall, next to form_login.
  3. CodeSmsRecentVoter grants RECENT_SMS_CODE when the token carries an SMS proof less than five minutes old.
  4. Transfers require RECENT_SMS_CODE. The rest of the account is fine with the password.

For now, Ada gets a 403 if she hasn't entered a code: the next exercise will send her to the code form instead.

Where the feature comes from: the original pull request.