Search topics
Session timeouts that bring you back to an empty form
A claim form signs people out after 30 minutes without a request, nothing on it says so, and signing in again brings back the form with everything typed gone.
res.redirect('/sign-in') The problem
People who need longer to fill in a form are signed out without knowing there was a limit, and lose everything they wrote when they sign in again
An insurer's claim form asks for a policy number and what happened, in the customer's own words. A leak through the kitchen ceiling takes a while to explain: people look up dates, find a plumber's receipt, or stop halfway for a break. For security, the insurer signs people out 30 minutes after their last request to the server. Typing sends nothing, so the clock runs while someone writes, and nothing on the form says there is a limit at all. Forty minutes later, the customer presses Send claim. The server finds no session, drops what was sent, and redirects to the sign-in page: "Your session has expired. Please sign in again." After signing in, they are back on the claim form, and it is empty.
This is what common frameworks do by default. Devise, a sign-in library for Rails, and Laravel both send people back to the page they came from after signing in again, but they remember only the page's address, not what was posted to it, and Django's sign-in redirect keeps only a next path. In Laravel, the form's security token also ends with the session, so the same press often shows a "419 Page Expired" screen instead.
Success Criterion 2.2.5 Re-authenticating asks that when a signed-in session expires, people can continue what they were doing after signing in again, without losing data. The limit itself is allowed: 2.2.5 doesn't ask for a longer session, only that nothing typed is lost when it ends. W3C's failure F12 describes this form exactly: the session ends, the person signs in again, and the form is not filled with what they entered, so someone who needs more time than the limit can never finish it.
Success Criterion 2.2.6 Timeouts asks that people are told how long they can be inactive before they lose data, unless the data is kept for more than 20 hours. W3C asks for the notice once, at the start of the task, so people can decide whether to gather their documents first or take a break. The form says nothing and keeps nothing, so a lunch break costs everything typed. The browser plays no part in either: the page sends an ordinary form, and the server chooses to throw the fields away when it finds the session gone.
A description that takes one person ten minutes can take another an hour, and the people who need longer are the ones who lose the most: they write the slow part twice, or give up on the claim. On a phone, switching to another app to find a photo or a policy document sends nothing to the server either, so that time counts against the limit too.
3 of 10 groups affected
- No vision (affected)
- Low vision (not affected)
- Colour vision (not affected)
- No hearing (not affected)
- Hard of hearing (not affected)
- No speech (not affected)
- Motor (affected)
- Reach, strength (not affected)
- Cognitive (affected)
- Seizures (not affected)
- Limited language, cognitive and learning abilities People who read and write slowly, lose track of time, or stop to find a receipt meet the limit without knowing there was one, and come back after signing in to an empty form, with the wording they worked on gone.
- Limited manipulation Typing with a switch, eye gaze, an on-screen keyboard or dictation is slow, so a long description easily takes more than 30 minutes without a single request to the server.
- Without vision Screen reader users take longer to move through a long form and to read back what they wrote. After signing in again, they find out field by field that the form is empty.
The limit is a server setting, and the redirect happens only to a form sent after it, so a scan sees a form that works. A scan doesn't sign in, wait half an hour and send the form, and nothing in the page says whether the server would keep what was sent, so the audit has no rule for this failure: only someone who waits it out finds it. Asked to put a form behind a sign-in, an AI assistant may write the framework's own redirect, which is the Before below, and may add a session limit without a word about the form.
Try it
Leave the claim form for 30 minutes, then send it and sign in again
The demo clock above the window stands in for the time away; it is not part of the insurer’s site. Does the form say how long you can leave it? After signing in again, what is left of your claim? Then apply a fix and do the same.
Broken Can you fix this?
The fix
Keep what people typed through signing in again
<form id="claim" method="post" action="/claims"> <h1>Make a claim</h1> <label for="policy">Policy number</label> <input id="policy" name="policy" value="{{ fields.policy }}"> <label for="what">What happened?</label> <textarea id="what" name="what" rows="8">{{ fields.what }}</textarea> <button>Send claim</button></form>What is wrong4 points
- Nothing on the form says that people are signed out after 30 minutes, or that what they typed is then lost.
- Typing sends nothing, so 30 minutes of writing counts as 30 minutes away.
- A claim sent after that is dropped: only the form's address is kept for after sign-in.
- After signing in, the form comes back empty.
Sign in again on the page, then send the same claim
<form id="claim" method="post" action="/claims"> <h1>Make a claim</h1> <label for="policy">Policy number</label> <input id="policy" name="policy" value="{{ fields.policy }}"> <label for="what">What happened?</label> <textarea id="what" name="what" rows="8">{{ fields.what }}</textarea> <button>Send claim</button></form> <dialog id="sign-in-again" aria-labelledby="sign-in-title"> <form id="sign-in" method="post" action="/sign-in"> <h2 id="sign-in-title">Sign in again to send your claim</h2> <p>Your claim is still here. Enter your password to send it.</p> <input type="hidden" name="username" value="{{ user.email }}" autocomplete="username"> <label for="password">Password</label> <input id="password" name="password" type="password" autocomplete="current-password"> <button>Sign in and send</button> </form></dialog>How it is fixed4 steps
- Send the claim from the page with
fetch, so the form stays as it is whatever the server answers. - When the session has ended, answer the claim with 401 instead of a redirect, so the page knows to ask.
- Open a sign-in dialog over the form with
showModal(). Focus moves into it, the account is already filled in, so only the password is asked for, and a password manager can fill that in. - Once signed in, close the dialog and send the same claim again.
This fixes the Before and passes 2.2.5 and 2.2.6: the claim never leaves the page, so no time away loses it. Only closing the tab does, and W3C doesn't count that as a timeout. It needs a script, and a sign-in the page can make itself; a site that signs people in through another site needs the next fix, or the notes on single sign-on below.
Signing in on the page and keeping what was sent both pass 2.2.5 and 2.2.6, and which one fits depends on how the form is built. If the form is already sent by script, or the site is an app, the dialog is the smaller change, and people never leave the page or lose their place in it. If the form is an ordinary HTML form, or signing in goes through another site that a dialog can't show, keep what was sent on the server: it needs no script, but people come back to a reloaded page. The notice alone is not enough: use it for fields you must not keep, beside one of the other two.
Security tokens. Many frameworks tie a form's security token (against cross-site request forgery) to the session, so a form sent after the session has ended fails that check before any of this code runs: in Laravel, that is the "419 Page Expired" screen. Handle that failure the same way as a missing session, by keeping the fields or asking the page to sign in. After signing in on the page, the session is new, so have the sign-in answer with its new token and put that into the claim before sending it again.
Whose data it is. The limit is there for shared and unattended computers, so what is kept must not reach the next person. Give it back only to the account that typed it, and delete it once it is back in the form. The dialog fills in the account and asks only for its password, so nobody else can carry on with the claim from it.
Single sign-on. When signing in again goes through another site, such as a company's identity provider, the page itself leaves for that site and comes back, and a dialog can't hold the form across the trip. Keep what was sent on the server, or have people sign in through a pop-up window while the form stays behind it.
A copy in the tab. Where the server can't be changed, a script can copy the form into sessionStorage as people type, and put it back when the form loads again in the same tab after signing in. The copy lasts as long as the tab, and is gone once it is closed. Store the account with it, and fill the form only for the same account.
Files. A page can't set a file field's value, and a script can put back through files only a file it has kept itself, so a photo chosen for the claim is gone after a reload. Upload attachments as soon as they are chosen, and list them with the claim, or sign in on the page, where the chosen file stays in its field; send the form then as FormData itself, since URLSearchParams can't carry a file.
A draft for longer breaks. Saving a draft to the account as people type, kept for more than 20 hours, goes further: it survives a closed tab, a crash or a change of device, and 2.2.6 doesn't ask for a notice for data kept that long. Pair it with one of the fixes, because anything typed after the session has ended can't be saved until they sign in again. W3C notes that privacy rules may require consent before data like this is kept: ask your privacy and legal advisers when a form holds health or payment details.
What not to keep. PCI DSS strictly limits storing card numbers and security codes, so keep neither this way. Leave those fields empty after signing in again, say which ones need filling, and give the limit at the start for them, as the notice does.
Not a fix. A longer session, such as 60 minutes instead of 30, still loses the claim of whoever is away longer. A dialog that warns before the end and lets people stay signed in is what 2.2.1 asks for, and has its own guide, session timeouts with no warning or way to extend; someone who misses it still loses the form, so it doesn't pass 2.2.5 on its own.
Verify the fix
5 checks, no mouse.
Test with: Keyboard, Screen reader, Password manager
Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the session settings, the sign-in flow or the form change.
How screen readers handle <dialog>
Limits
This covers losing what was typed when a session ends, and saying how long that takes. Warning people before a limit ends and letting them extend it is level A, with its own guide: session timeouts with no warning or way to extend. A task with any time limit at all is 2.2.3 No Timing, with its own guide: checkout timers that release held seats. 2.2.5 and 2.2.6 are AAA, and W3C doesn't recommend requiring AAA for whole sites, but for a long form behind a sign-in, both are met by keeping what was typed. Whether that is safe to keep, and for how long, is for the people who run the service to decide, with their privacy advisers.
This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.
References
- WCAG Understanding: re-authenticating (opens in a new tab)
- WCAG Understanding: timeouts (opens in a new tab)
- WCAG Technique F12 (opens in a new tab)
- WCAG Technique G105 (opens in a new tab)
- developer.mozilla.org: dialog (opens in a new tab)
- cheatsheetseries.owasp.org: Session_Management_Cheat_Sheet (opens in a new tab)
EaseWeb service
Fixed it? Keep it from coming back.
I turn guides like this one into rules your coding agents follow and checks your CI runs. Then I retest by hand with a keyboard and screen readers, periodically.