April 6th, 2024
Timeouts in WebAuthn
When creating and asserting a WebAuthn credential, one of the many configuration
knobs afforded to websites is the
timeout
value.
This OPTIONAL member specifies a time, in milliseconds, that the Relying Party is willing to wait for the call to complete. This is treated as a hint, and MAY be overridden by the client.
In practice, browsers will rudely interrupt a user and display an error when the
timeout is reached and return a generic error message to your site. Browsers
may also clamp the value within some "reasonable" minimum
and maximum. timeout being optional it can be tempting to leave the value
undefined and rely on its default. However, this is a mistake: the default value
will then depend on the browser and can be wildly different to what you'd
expect!
Chrome's default used to be 10 minutes but now it's 20 hours. Safari at least as of 10 months ago set 2 minutes, Firefox 30 seconds (!!!), and the "recommended" default w3c value is 5 minutes now but used to be 2 minutes depending on the settings.
The reason for these wide discrepancies is that nobody wants the specification to mandate user experience, and so it's left to each browser vendor to do whatever they consider reasonable. The result is that depending on the browser, the default experience can vary from annoying (e.g. 2 minutes are not enough time to grab the phone you forgot in your car) to unusable for disabled users.
Why do we have timeouts anyway?
Some websites implement cryptographic challenges that are a function of the current timestamp. This is weaker than the recommended alternative of having the server generate a new random challenge per request. It also means you need to bind the validity of the challenge to a short (~minutes) time to narrow the window where a replay attack is possible. And back in the olden times WebAuthn did not support a way to abort ongoing requests and security keys were the only authenticator, it made sense to have a way to stop the user from finishing an authentication ceremony when the challenge had already expired to save them the disappointment of a rejected sign in request.
Short default timeouts also used to be more reasonable when tapping a security key attached to your computer was all you had to do to sign in. These days, if your user is new to passkeys, depending on their provider they might have to enroll some form of user verification, pull out their phone, or enable sync services. You do not want to interrupt your user while they're doing that.
A note on conditional UI (i.e. passkey autofill)
When assertion a credential through a conditional mediation request, the timeout is ignored (see step 3.3). This was a deliberate choice to avoid passkeys confusingly "disappearing" from the autofill popup. Implementing a time-based challenge for is more complex, since you'll want to abort the request and restart it periodically with a fresh challenge. Save yourself that work by not using time based challenges.
Design around not needing a timeout
The best thing you can do for your users is to set a timeout long enough, that
it is effectively the same as not having a timeout, like 100000000 which is 27
hours and a nice round number. This makes the experience more pleasant and
accessible for users with disabilities.
Design your challenge response protocol to issue a new random challenge per WebAuthn ceremony, discard the challenge after the ceremony ends, and periodically remove old challenges (say, after a day or so). Some browsers might still clamp your timeout value down to a maximum, but at least you did the best you could for your users. This is a more secure design and can simplify your front-end if you decide to implement Conditional UI.
If you must
If you absolutely must bind your challenge to a timestamp, then set the timeout to the exact lifetime of your challenge. You should still handle the case where a browser returns an assertion for an expired challenge with a helpful error message, since an expired challenge might still slip through to your back-end.