# 警告も延長もないセッションタイムアウト

Source: https://easeweb.dev/learn/time-limits
Topics: 制限時間と動き > 制限時間を延長できる (WCAG 2.2.1)
The fix: セッションを20時間より長くしてページのタイマーをなくすか、制限を残したまま、終了の2分前にモーダルダイアログを開いて「ログインを続ける」ボタンへフォーカスを移し、押されるたびにセッションを更新します。画面に描くだけで、フォーカスも移らず、スクリーンリーダーも読まない警告では足りません。
Test with: keyboard, screen reader, zoom
References: https://www.w3.org/WAI/WCAG22/Understanding/timing-adjustable.html https://www.w3.org/WAI/WCAG22/Techniques/client-side-script/SCR16 https://www.w3.org/WAI/WCAG22/Techniques/client-side-script/SCR1 https://www.w3.org/WAI/WCAG22/Techniques/failures/F40 https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/ https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html

よくある問題を再現した学習用のサンプルです。実在のクライアントの診断結果ではありません。コードは出発点となる実装例のため、実際の製品でも確認してください。ここでは支援技術でのテスト結果は報告していません。

## 場面

文具店の購入手続きで、配送先の住所とギフトメッセージを入力します。利用者はログインしていて、この店はサーバーへの最後のリクエストから15分で利用者をログアウトさせます。フレームワークの例からそのまま写した、セキュリティの既定値です。フォームへの入力はサーバーに何も送らないため、文章を書いている間も時計は進みます。15分になると、ページはログイン画面へ移ります。「セッションの有効期限が切れました」。制限があることも、それが近いことも、ページは何も伝えておらず、住所もメッセージも消えています。サーバー側の制限しかないサイトも多くあります。画面では何も起きず、「次へ」を押したときにフォームが失われます。

警告を出すサイトもあります。終了の2分前に、画面の上に「ログインを続ける」ボタン付きのバナーが現れます。修正に見えますし、気づいた、画面が見えるマウスの利用者には役に立ちます。しかし、バナーはページの最後に追加され、フォーカスも移らず、読み上げもされません。スクリーンリーダーの利用者には何も聞こえず、フォームを拡大している画面拡大ソフトの利用者には見えず、キーボードの利用者はすべての入力欄とフッターのリンクをTabで通過しないとボタンに届きません。

## 確認できる問題

達成基準2.2.1「タイミング調整可能」は、コンテンツが設定するすべての時間制限を対象にしています。サイト自身のサーバーが終わらせるセッションも含まれます。対象外になるのは、ブラウザーやネットワークのように、コンテンツが制御できない制限だけです。それぞれの制限について、利用者が出会う前に解除できるか、既定の10倍以上に調整できるか、時間切れの前に警告して、簡単な操作で20秒以上の猶予のうちに延長でき、それを10回以上繰り返せる必要があります。例外は三つです。オークションのようなリアルタイムのイベント、その活動に欠かせない制限、20時間より長い制限です。

一つ目の購入手続きは、どれも満たしていません。二つ目は警告していますが、描いているだけです。2.2.1は警告の出し方を定めていませんが、警告は、警告する相手に届く必要があります。スクリーンリーダーが読まず、画面拡大ソフトの視野の外にあり、キーボードではほかのすべての後でしか届かない警告では、その人たちは予告なしに同じ締め切りを迎えます。この警告についてのW3Cの達成方法SCR16は、ブラウザー自身の確認ダイアログを使っていて、誰もがすぐにそれに出会います。

## 利用者への影響

15分が長いか短いかは、フォームの入力のしかたによって変わります。スイッチで打つギフトメッセージや、手紙と照らし合わせて確かめる住所は、すぐにそれ以上かかります。そして、時間がより必要な人ほど、フォームが空になったときに失うものが大きく、時間のかかる部分を二度やることになります。スマートフォンでは、入力中はスクリーンキーボードがページの見える範囲を狭め、画面の上に固定したバナーがその範囲から外れることがあります。

## 自動チェックとAIの限界

制限はスクリプトのタイマーやサーバーのセッション設定の中にあり、どちらもスキャンが読むページには現れません。警告がある場合も、それが出るのは13分後で、スキャンはとうに終わっています。axeが報告するのは、この失敗の古い形、遅延を付けてページを再読み込みしたり別のページへ移したりする `<meta http-equiv="refresh">` で、W3Cの失敗例F40とF41に当たります。この診断では、カウントダウンを表示しているページや、セッションの終了に触れているページも、人が確認するよう挙げます。しかし、ページが何も触れていない制限は見えません。AIアシスタントに「一定時間操作がなければ自動ログアウト」を頼むと、よく一つ目の購入手続きと同じタイマーを書き、警告を頼むと、読み上げられないトーストやバナーを隅に加えます。キーボード、スクリーンリーダー、画面拡大ソフトで、実際に時間切れまで待ってみて、初めて分かります。

## 修正前：警告なしでログアウトするセッション

```js server.js
// 最後のリクエストから15分でセッションを終える
app.use(session({ secret: process.env.SESSION_SECRET, rolling: true, cookie: { maxAge: 15 * 60 * 1000 } }));
```

```js
// 同じ時刻にログイン画面へ移す
const signOut = () => { location.href = '/sign-in?expired=1'; };
setTimeout(signOut, 15 * 60 * 1000);
```

ページは、利用者が何をしていても、読み込んでから15分でログアウトさせます。制限を解除する、延ばす、延長を求める方法はありません。フォームに入力した内容は、ページと一緒に失われます。

## 修正前：画面にだけ出る警告

```js server.js
// 最後のリクエストから15分でセッションを終える
app.use(session({ secret: process.env.SESSION_SECRET, rolling: true, cookie: { maxAge: 15 * 60 * 1000 } }));
```

```js
const LIMIT = 15 * 60 * 1000;
const WARN = 2 * 60 * 1000;
let warnTimer, endTimer;

function start() {
  clearTimeout(warnTimer);
  clearTimeout(endTimer);
  warnTimer = setTimeout(warn, LIMIT - WARN);
  endTimer = setTimeout(() => { location.href = '/sign-in?expired=1'; }, LIMIT);
}

// 画面の上に固定するバナー。ページの最後に追加する
function warn() {
  const banner = document.createElement('div');
  banner.className = 'session-warning';
  banner.innerHTML = '2分後にログアウトします。<button type="button">ログインを続ける</button>';
  banner.querySelector('button').addEventListener('click', async () => {
    await fetch('/session/extend', { method: 'POST' });
    banner.remove();
    start();
  });
  document.body.append(banner);
}
start();
```

```css
.session-warning { position: fixed; inset: 0 0 auto; padding: 0.75rem 1rem; background: #fff4d6; }
```

警告の文もボタンも正しく、バナーに気づいたマウスの利用者はログインを続けられます。しかし、フォーカスはフォームに残り、何も読み上げられず、バナーはページの中でフッターの後にあります。スクリーンリーダーの利用者には伝わらず、画面拡大ソフトの利用者には見えず、キーボードの利用者は最後にたどり着きます。

## 修正後：ログイン状態を続けられる警告ダイアログ

```html
<dialog id="session-warning" role="alertdialog"
  aria-labelledby="session-title" aria-describedby="session-text">
  <h2 id="session-title">もう少し時間が必要ですか？</h2>
  <p id="session-text">セキュリティのため、2分後にログアウトします。入力した内容は失われます。</p>
  <form method="dialog">
    <button autofocus>ログインを続ける</button>
  </form>
</dialog>
```

```js server.js
// 同じ15分のセッション。どのリクエストでも更新される
app.use(session({ secret: process.env.SESSION_SECRET, rolling: true, cookie: { maxAge: 15 * 60 * 1000 } }));
app.post('/session/extend', (req, res) => res.sendStatus(204));
```

```js
const LIMIT = 15 * 60 * 1000;
const WARN = 2 * 60 * 1000;
const dialog = document.getElementById('session-warning');
let warnTimer, endTimer;

function start() {
  clearTimeout(warnTimer);
  clearTimeout(endTimer);
  warnTimer = setTimeout(() => dialog.showModal(), LIMIT - WARN);
  endTimer = setTimeout(() => { location.href = '/sign-in?expired=1'; }, LIMIT);
}

// 「ログインを続ける」かEscapeで、セッションを更新する。何度でも
dialog.addEventListener('close', async () => {
  await fetch('/session/extend', { method: 'POST' });
  start();
});
start();
```

制限は残りますが、知らないうちに出会う人はいません。終了の2分前に `showModal()` がページの上に警告を開き、フォーカスを「ログインを続ける」へ移します。スクリーンリーダーはタイトル、残り時間、ボタンを読み上げることができ、画面拡大ソフトはフォーカスを追ってそこへ移り、キーボードの利用者はすでにボタンの上にいます。1回押すか、Escapeを押せば、サーバーのセッションが更新され、ページの時計も最初から数え直します。必要なだけ何度でもできます。ブラウザーはフォーカスを元の場所に戻すため、利用者はそのまま入力を続けられます。誰も答えなければ、これまでどおりセッションは終わります。共用のパソコンでは、それがこの制限の役目です。

## 修正後：20時間より長いセッション

```js server.js
// 最後の訪問から30日間続くセッション
app.use(session({ secret: process.env.SESSION_SECRET, rolling: true, cookie: { maxAge: 30 * 24 * 60 * 60 * 1000 } }));
```

```js
// タイマーは置かない。ページは誰もログアウトさせない
```

2.2.1は20時間より長い制限を対象外にしているため、フォームの入力中に制限に出会う人はいません。警告することも、作るダイアログもありません。これが最も小さな変更です。サーバーの数字を一つ変え、ページのタイマーを一つ消すだけです。開いたままのセッションが守るものが少ないサイトに向いています。たとえば、アカウントのメールアドレス、パスワード、保存したカードを変える前に、パスワードをもう一度求めている店です。銀行、医療記録、共用のパソコンで使われるものには向きません。そこでは、制限に理由があります。

## どの方法を選ぶか

どちらも2.2.1を満たします。どちらが合うかは、アクセシビリティではなく、セッションが何を守っているかで決まります。15分が誰も選んでいない既定値から来ているなら、セッションを20時間より長くします。誰も中断されず、作るものもありません。制限がセキュリティのためにあるなら、それを残して警告ダイアログを加え、自分たちの制限に合わせて時間を決めます。ダイアログ、数行のスクリプト、エンドポイントが必要ですが、本当に席を離れた人に対する保護は残ります。画面に描くだけのバナーは、どちらの場合も残さないでください。

## 実装の判断

**制限に出会う前に解除できる方法でも満たせます。** 長いフォームの最初に置く「時間を長くする」のチェックボックスや、ログイン時にセッションを20時間より長くする「ログインしたままにする」のチェックボックスは、出会う前に制限を解除するか、10倍以上に延ばせる方法で、2.2.1はこれを認めています。ただし、気づいて意味を理解した人にしか効かないため、セキュリティのための制限があるサイトでは警告も残します。

**どれだけ前に警告するか。** 20秒は最低限で、ほとんどの人がキーかスイッチを一つ押せるように選ばれています。スクリーンリーダーでダイアログを読む、目を離していて後から気づくといった場合は、もっと時間がかかります。2分にしても失うものはなく、ずっと多くの人に間に合います。警告はサーバーの制限から逆算します。ページ自体がリクエストを送る場合（住所の検索、下書きの保存など）は、そのたびにページの時計も数え直し、両方をそろえます。

**残り時間。** 修正のように、残り時間はダイアログの文で伝えます。毎秒減る時計を描いてもかまいませんが、`aria-hidden="true"` を付け、ライブリージョンには入れません。そうしないと、スクリーンリーダーがボタンの上で毎秒読み上げます。

**利用者の操作から数える。** 修正は、サーバーと同じく最後のリクエストから数えています。入力中の人にダイアログを見せたくないサイトは、入力の間もセッションを更新できます。たとえば入力イベントが続く間は1分に1回リクエストを送り、手が止まったときのためにダイアログを残します。

**スリープと別のタブ。** 制限を過ぎるまでスリープしていたノートパソコンは、警告を一度も出さないまま制限の後に目覚めます。スリープ中はタイマーが動かないので、タイマーが動いたときとページが再び表示されたとき（`visibilitychange`）に、`Date.now()` と制限が始まった時刻を比べます。制限を過ぎていたら、セッションがまだあるかサーバーに確かめ、なければページでそう伝え、入力した内容は残します。複数のタブを開いている場合は、答えたタブからセッションを更新し、ほかのタブのダイアログは `BroadcastChannel` などで閉じます。

**入力した内容を残す。** 警告があっても、見逃した人はフォームを失います。下書きをブラウザーかサーバーに保存し、再ログインの後に戻すのは2.2.1の範囲を超えます。それは2.2.5「再認証」が求めることで、別のトピックです。

**時間付きの確保。** 購入手続きの間に座席や在庫を確保するカウントダウンも、時間制限です。W3C自身の例では、すぐに売り切れることのあるチケットの2分間の確保を欠かせない制限として扱ったうえで、名前や支払い情報の入力など、できるだけ多くの手順を時間制限のある部分の外へ出しています。確保を延長できる場合は、「時間を延長する」ボタンの付いた同じ警告ダイアログで満たせます。カウントダウンを見せることは、調整できることではありません。ボタンのない隅の時計は、誰にも変えられない制限のままです。

**修正にならないもの。** メッセージを書き込む前からページにあるバナーに `role="alert"` を付けておけば、メッセージは読み上げられますが、フォーカスはフォームに残り、ボタンはページの最後のままです。15分を60分にするなど、固定の制限を長くしても、警告なしに出会う人はいます。

## 修正の確認

1. サーバーのセッション設定とページのスクリプトを読み、すべての制限を書き出す。セッションの有効期間、ログアウト、消去、移動を行うタイマー、遅延付きの `<meta http-equiv="refresh">` などで、どれも20時間より長いか、解除できるか、前に警告がある。
2. 制限を数分に縮めたテスト用のビルドで、フォームをゆっくり入力し、そのまま置いておく。何も起きないか、終了の20秒以上前に警告が出て、誰も答えなければセッションが終わる。
3. キーボードだけで、警告が開くのを待つ。フォーカスは「ログインを続ける」にあり、Enterでログイン状態が続き、フォームは入力したままになる。10回続けても同じように使える。
4. フォームでスクリーンリーダーを動かしたまま、警告が開くのを待つ。自分で移動しなくても、タイトル、残り時間、フォーカスのあるボタンが読み上げられる。
5. 画面拡大ソフトでギフトメッセージを拡大したまま、警告が開くのを待つ。探さなくても警告が視野に入る。

OS、ブラウザーと支援技術のバージョン、ビルド、実施日、各手順の実際の結果を記録してください。セッションの設定、ログインの流れ、警告を変更した際は、再度確認します。

## 制限

ここで扱うのは、セッションやページを終わらせる時間制限と、それを警告する一つの方法です。ひとりでに動く、流れる、更新される内容には、別の達成基準とガイドがあります。[一時停止できない、流れる文字、カルーセル、アニメーション](/ja/learn/moving-content)を参照してください。タイマーで消えるメッセージも2.2.1の時間制限で、こちらにもガイドがあります。[読む前や元に戻す前に消えるトースト](/ja/learn/toast-timing)を参照してください。遅延付きのmeta refreshのように、タイマーでページを再読み込みしたり移動させたりすることも、別のトピックです。データが失われるまでの無操作の時間を伝えること、時間に頼らない手順を設計することは、それぞれAAAの別のトピックです。制限が本当に欠かせないものか、リアルタイムのイベントの一部かは、サービスを運営する人にしか判断できません。また、ひとりでに開くダイアログの扱いはスクリーンリーダーによって異なります。利用者が使っているもので警告を確認してください。

---

このガイドをコーディングエージェントで使う: MCP https://easeweb.dev/mcp（設定は https://easeweb.dev/ja/services/developers）
