テックブログ

WebSocketの認証設計:接続時と再接続時のトークン管理

WebSocketの認証設計:接続時と再接続時のトークン管理

WebSocketでも、接続時に利用者が誰なのかを認証し、接続後はチャンネル購読や操作ごとに権限があるかを認可する必要があります。ブラウザ標準のWebSocket APIでは、Authorizationのような任意のHTTPヘッダーをJavaScriptから自由に設定できないため、Cookieや接続後の認証メッセージなど、環境に合った方法を選びます。また、長時間接続ではアクセストークンが途中で期限切れになる可能性があるため、再接続前の有効期限確認、必要な更新処理、再認証までを一体として設計することが重要です。

1. WebSocketでも認証が必要

1-1. 接続相手が誰かを確認する

WebSocketでも、HTTP APIと同様に「誰が接続しているのか」を確認する認証が必要です。wss://で暗号化されていることと、利用者の本人確認ができていることは別の問題です。

WebSocketでは接続が確立すると、その後は同じ接続を使って多数のメッセージを送受信できます。そのため、認証されていない接続からチャット送信、通知購読、管理操作などを実行できる状態にしてしまうと、不正利用につながります。

例えば社内チャットなら、接続時にログイン済みユーザーであることを確認し、そのユーザーIDをWebSocket接続に関連付けます。認証できない場合は通常メッセージを処理せず、認証失敗として接続を閉じるなどの制御が必要です。

1-2. 認証と認可は別の役割

認証(Authentication)認可(Authorization)は別の役割です。認証は「誰なのか」を確認し、認可は「その人が何をしてよいか」を判断します。

例えばユーザーAが正常にログインできたとしても、それだけで管理者専用チャンネルを購読できるとは限りません。接続が認証済みでも、メッセージ送信、チャンネル購読、データ更新などの操作ごとに権限を確認する必要があります。

「認証済みだから全操作OK」という設計は避けます。一般ユーザーがadmin-roomを購読しようとした場合は、認証済みであっても認可チェックによって拒否する、という分離が必要です。

1-3. HTTP APIとの違いを整理する

HTTP APIでは、多くの場合リクエストごとにCookieやAuthorizationヘッダーなどの認証情報を確認します。一方、WebSocketでは最初のHTTPベースのハンドシェイク後に長時間接続を維持するため、認証状態の持ち方が異なります。

WebSocket接続では、接続確立時または接続直後に利用者を認証し、その結果を接続オブジェクトに関連付けて扱う設計が一般的です。その後の各メッセージで毎回フルログイン処理を行うのではなく、接続に保存したユーザー情報を利用しつつ、操作ごとの認可を確認します。

ただし、長時間接続中に認証情報が失効する可能性があります。そのため、「最初に認証できたから接続が終わるまで永久に有効」と単純に考えず、有効期限や再接続時の扱いを決めておく必要があります。

2. WebSocket接続時の認証方法

2-1. Cookieを利用する

ブラウザWebSocketで既存のログインセッションを利用する場合、Cookie認証は有力な選択肢です。WebSocketの接続開始時にはHTTPベースのハンドシェイクが行われるため、条件を満たすCookieがブラウザから送信されます。

すでにWebアプリがセッションCookieでログイン管理している場合、WebSocketサーバー側でもそのCookieからセッションを確認できます。JavaScriptからアクセストークンをWebSocketへ直接設定する必要がなく、HTTP APIと認証基盤を共有しやすい利点があります。

一方で、Cookieが自動送信される仕組みを利用する場合は、Originの検証が重要です。攻撃者のWebサイトから利用者のブラウザを使ってWebSocket接続を開始される、Cross-Site WebSocket Hijackingのような攻撃を防ぐため、サーバー側で許可するOriginを明示的に確認します。

例えば、https://app.example.comからのみWebSocket接続を許可するなら、ハンドシェイク時のOriginが許可リストに含まれているかを確認します。CookieのSecureHttpOnlySameSiteなども適切に設定しますが、Origin確認を省略してよい理由にはなりません。

2-2. 接続後の認証メッセージを利用する

ブラウザでアクセストークンを利用したい場合は、WebSocket接続後に最初のアプリケーションメッセージとして認証情報を送る方法があります。ブラウザ標準WebSocket APIのヘッダー制約を回避しつつ、トークン認証を実装できます。

この方式では、WebSocketのopen後にauthメッセージを送り、サーバーがトークンを検証します。認証が完了するまでは、チャット送信や購読要求など通常のメッセージを処理しないことが重要です。

ブラウザ側の例は次のとおりです。

const socket = new WebSocket("wss://example.com/ws");

socket.addEventListener("open", () => {
  socket.send(
    JSON.stringify({
      type: "auth",
      accessToken: getCurrentAccessToken()
    })
  );
});

socket.addEventListener("message", (event) => {
  const message = JSON.parse(event.data);

  if (message.type === "auth_ok") {
    console.log("WebSocket認証が完了しました");
  }

  if (message.type === "auth_error") {
    console.error("WebSocket認証に失敗しました");
  }
});

コードの役割: WebSocket接続後、最初にアクセストークンを認証メッセージとして送信します。サーバーからauth_okを受け取るまでは、アプリケーション上「利用可能な接続」とは扱いません。

なぜ必要か: openイベントはWebSocket接続が確立したことを意味しますが、アプリケーション上のユーザー認証が完了したことまでは意味しません。「接続済み」と「認証済み」を別の状態として管理する必要があります。

トークンはTLSで保護されたwss://を利用して送信します。ただし、TLSを使っているから認証や認可が不要になるわけではありません。また、クライアント側のログにトークンを出力しないなど、通常の認証情報と同じように慎重に扱います。

2-3. URLクエリへ認証情報を載せる場合の注意点

WebSocket URLのクエリパラメーターに認証情報を付ける方式も技術的には可能ですが、長寿命のアクセストークンを安易に載せる方法は推奨できません。URLはさまざまな場所に記録される可能性があるためです。

例えば次のような形式です。

wss://example.com/ws?token=...

URLはサーバーのアクセスログ、リバースプロキシ、監視システム、エラー記録などに残る可能性があります。トークンが長時間有効であれば、ログから漏えいした際の影響も大きくなります。

どうしてもURLを利用する必要がある場合は、短時間・単回利用の接続専用トークンをHTTP APIで発行して使うなど、漏えい時の影響を限定する設計を検討します。通常の長寿命アクセストークンをそのままURLに入れることを「簡単だから」という理由だけで選ぶのは避けます。

3. ブラウザWebSocket APIの制約

3-1. 任意のAuthorizationヘッダーを設定できない

ブラウザ標準のWebSocketコンストラクターでは、JavaScriptから任意のAuthorizationヘッダーを設定できません。fetch()の感覚でWebSocketにもヘッダーを追加できると考えると、実装時につまずきやすいポイントです。

ブラウザで利用できる基本的な形式は次のようになっています。

const socket = new WebSocket(
  "wss://example.com/ws"
);

またはサブプロトコルを指定できます。

const socket = new WebSocket(
  "wss://example.com/ws",
  ["example-protocol"]
);

new WebSocket(url, { headers: ... })のようなブラウザ標準APIはありません。そのため、BearerトークンをAuthorizationヘッダーへ自由に設定する方式を、そのままブラウザWebSocketへ適用することはできません。

3-2. サーバー環境のWebSocketクライアントとは条件が異なる

Node.jsなどのサーバー環境では、利用するWebSocketライブラリによって接続時のHTTPヘッダーを指定できる場合があります。しかし、それは各ライブラリが提供する機能であり、ブラウザ標準WebSocket APIの機能ではありません。

例えばバックエンドサービス同士のWebSocket接続では、ライブラリ側でAuthorizationヘッダーを設定できることがあります。そのコードをそのままブラウザ向けJavaScriptへ移植しても動作するとは限りません。

WebSocket認証の記事やサンプルコードを見るときは、「ブラウザのJavaScriptなのか」「Node.jsなどのサーバー側クライアントなのか」を必ず確認します。実行環境の違いを無視すると、利用できない認証方式を選んでしまいます。

3-3. 制約を前提に認証方式を選ぶ

ブラウザWebSocketでは、制約を無理に回避しようとするのではなく、Cookie方式や接続後認証メッセージ方式など、APIの特徴に合った方法を選ぶことが重要です。

既存のWebアプリが安全なセッションCookieを中心に構成されているならCookie方式が自然です。一方、HTTP APIですでに短寿命アクセストークンを利用している場合は、接続後に認証メッセージを送る方式が統合しやすいことがあります。

方式を選ぶときは、「実装できるか」だけでなく、トークンがどこに残るか、再接続時にどう再認証するか、クロスサイト接続をどう防ぐか、失効をどう扱うかまで含めて判断します。

Cookie方式と接続後認証メッセージ方式の比較

項目Cookie方式接続後認証メッセージ方式
認証タイミングハンドシェイク時WebSocket接続後
ブラウザ側の実装Cookieをブラウザが送信JavaScriptでトークン送信
既存セッションとの統合しやすいトークン認証と統合しやすい
認証前メッセージ制御ハンドシェイクで拒否可能認証完了までサーバー側で拒否が必要
主な注意点Origin検証、Cookie属性トークン管理、認証前状態の制御
再接続時Cookieを再確認新しい有効トークンで再認証
トークンをURLへ載せる必要基本的にない基本的にない

どちらが常に優れているというものではありません。既存の認証基盤、アプリケーション構成、WebSocketサーバーの配置などから選択します。

4. 接続後の認証状態を管理する

4-1. 認証完了までは通常メッセージを拒否する

接続後認証メッセージ方式では、認証が成功するまで通常のアプリケーションメッセージを受け付けない設計が必要です。WebSocketがOPENになったことと、利用者の認証が成功したことは別だからです。

サーバー側では各接続にauthenticatedのような状態を持ち、認証前に許可するメッセージを限定します。認証前にsubscribesend_messageを受信しても処理しません。

サーバー側の疑似コードは次のようになります。

onConnection(connection):
    connection.authenticated = false
    connection.user = null

onMessage(connection, rawMessage):
    message = parseJson(rawMessage)

    if connection.authenticated == false:
        if message.type != "auth":
            sendError(connection, "AUTH_REQUIRED")
            return

        user = verifyAccessToken(message.accessToken)

        if user == null:
            sendError(connection, "AUTH_FAILED")
            closeConnection(connection)
            return

        connection.authenticated = true
        connection.user = user

        send(connection, {
            "type": "auth_ok"
        })

        return

    handleAuthenticatedMessage(
        connection,
        message
    )

コードの役割: 接続直後はauthenticated = falseとし、auth以外のメッセージを拒否します。トークン検証に成功した後だけ通常のメッセージ処理へ進みます。

なぜ必要か: 認証処理と通常メッセージ処理を並行して許可すると、認証完了前に不正な操作を実行される可能性があります。サーバー側で状態遷移を明確に管理することで防ぎます。

認証待ち状態を無期限に残す必要もありません。一定時間認証メッセージが届かなければ接続を終了するなど、未認証接続によるリソース消費を抑える対策も検討します。

4-2. ユーザー情報を接続に関連付ける

認証に成功したら、確認できたユーザー情報をWebSocket接続へ関連付けます。その後のメッセージ処理では、クライアントが自己申告したユーザーIDではなく、サーバー側で認証済みの情報を基準にします。

例えばクライアントから次のようなメッセージが届いたとしても、

{
  "type": "send_message",
  "userId": "admin",
  "text": "hello"
}

userIdをそのまま信用してはいけません。送信者は認証済み接続に保存したconnection.user.idから決定します。

具体的には、認証後にconnection.user = verifiedUserを保存し、監査ログ、メッセージ投稿者、認可判定などに利用します。クライアントから送られてくるIDは改ざんできるという前提で設計します。

4-3. 購読や操作ごとに認可を確認する

認証済みであっても、チャンネル購読や更新操作ごとに認可を確認する必要があります。認証は「誰か」を示しますが、そのユーザーがすべてのデータへアクセスできることを意味しません。

例えばprivate-room-100への購読要求が届いた場合、サーバーはそのユーザーがルームのメンバーか確認してから購読を登録します。

handleAuthenticatedMessage(connection, message):

    if message.type == "subscribe":
        channel = findChannel(message.channelId)

        if channel == null:
            sendError(connection, "CHANNEL_NOT_FOUND")
            return

        if not canSubscribe(
            connection.user,
            channel
        ):
            sendError(connection, "FORBIDDEN")
            return

        subscribe(
            connection,
            channel
        )

        send(connection, {
            "type": "subscribed",
            "channelId": channel.id
        })

        return

コードの役割: ユーザーが認証済みであることを前提に、さらに対象チャンネルへの参加権限を確認します。

なぜ必要か: クライアントは任意のchannelIdを送信できます。画面上でボタンを非表示にするだけでは防御にならないため、サーバー側で必ず認可します。

同じ考え方は、メッセージ削除、管理操作、個人データ取得などにも適用します。「接続に成功した」という事実だけで全操作を許可してはいけません。

5. トークン期限切れと再接続

5-1. 長時間接続ではトークンが期限切れになる

WebSocketは長時間接続されるため、接続開始時に有効だったアクセストークンが接続中に期限切れになることがあります。これはHTTPの短いリクエストとは異なる設計ポイントです。

例えばアクセストークンの有効期限よりWebSocket接続時間のほうが長ければ、接続中のどこかでトークン期限が到来します。その時点で直ちに切断するか、次の重要操作で再確認するか、一定時間ごとに再認証するかはシステムの要件によって異なります。

重要なのは、「一度認証したら永遠に有効」と無意識に決めないことです。高いセキュリティが必要な操作では有効期限や権限変更を随時確認し、比較的低リスクな通知用途では再接続時に再認証する、といった設計も考えられます。

5-2. 再接続前に有効な認証情報を確認する

WebSocketが切断されて再接続する場合は、前回使用したトークンをそのまま無条件で再利用せず、現在も有効か確認します。切断中に有効期限を迎えている可能性があるためです。

基本的な流れは次のように整理できます。

WebSocket切断
    ↓
再接続が必要
    ↓
アクセストークンの状態を確認
    ↓
まだ有効
    ├─ Yes → WebSocket再接続
    │          ↓
    │        再認証
    │          ↓
    │        購読状態を復元
    │
    └─ No → HTTP側の認証更新処理
               ↓
             更新成功
               ├─ Yes → 新しいトークンで再接続
               └─ No  → 再ログインへ

ブラウザ側では、WebSocketの再接続ロジックから直接Refresh Tokenの詳細を操作するのではなく、既存のHTTP認証処理へ「有効なアクセストークンを取得する」責務を任せると整理しやすくなります。

例えば次のような構造です。

async function reconnectWebSocket() {
  try {
    const accessToken =
      await authManager.getValidAccessToken();

    connectWebSocket(accessToken);
  } catch (error) {
    showLoginRequired();
  }
}

コードの役割: WebSocket再接続前に、認証管理側から現在有効なアクセストークンを取得します。期限切れなら、認証管理側が必要な更新処理を行います。

なぜ必要か: 再接続処理が古いトークンを使い続けると、認証失敗と再接続を繰り返す状態になりかねません。WebSocketの再接続とトークン有効期限を一体で考える必要があります。

5-3. 更新失敗時は再ログインへ誘導する

アクセストークンの更新に失敗し、有効な認証情報を取得できない場合は、WebSocket再接続を無限に繰り返さず、再ログインなどのユーザー操作へ切り替えます。

例えばログインセッションそのものが失効している場合、時間を置いて再接続しても成功しません。再接続の指数バックオフはネットワーク障害には有効ですが、認証失敗そのものを解決する仕組みではありません。

認証系エラーを受け取ったら再試行状態を停止し、「セッションの有効期限が切れました。再度ログインしてください」と表示するなど、復旧方法を明確にします。

再ログイン後は、新しい認証情報でWebSocketを接続し直し、以前購読していたチャンネルや画面状態を必要に応じて復元します。認証復旧とアプリケーション状態の復旧を両方考えることが重要です。

6. WebSocket認証でよくある注意点

6-1. URLやログへ機密情報を残さない

アクセストークンなどの機密情報は、URLやアプリケーションログへ残さないことが重要です。WebSocket認証では「どう送るか」だけでなく、「どこに記録され得るか」まで確認します。

URLクエリに含まれた値は、サーバーやプロキシ、監視ツールなどに記録される可能性があります。また、接続後の認証メッセージ方式でも、受信したメッセージ全体をデバッグログへ出力するとトークンが残る可能性があります。

トークンを扱うメッセージはログから除外する、マスキングする、接続専用の短寿命トークンを利用するなど、漏えい時の影響を小さくする設計を行います。

6-2. Cookie利用時はOriginを確認する

Cookie認証を利用するWebSocketでは、サーバー側でOriginを確認することが重要です。ブラウザがCookieを自動送信することを悪用される可能性があるためです。

攻撃者が自分のWebサイトから被害者のブラウザを使ってWebSocketへ接続し、Cookieが送信されると、サーバーがOriginを確認していなければ認証済み接続として扱ってしまう可能性があります。これはCross-Site WebSocket Hijackingと呼ばれる問題につながります。

サーバーではOriginを許可リストと照合し、想定外のWebサイトからのハンドシェイクを拒否します。wss://によるTLS、Cookie属性、Origin確認、操作単位の認可を組み合わせて防御します。

6-3. 接続済みという理由だけですべてを許可しない

WebSocketが接続済みで、認証も完了しているからといって、その接続から届くすべての操作を許可してはいけません。接続状態は権限そのものではないためです。

クライアント側JavaScriptは利用者によって変更できます。開発者ツールから任意のWebSocketメッセージを送信できるため、「画面に管理ボタンがないから管理操作は送られない」という前提は成り立ちません。

サーバー側で、操作ごとにユーザー権限、対象リソースとの関係、現在の状態などを確認します。特にチャンネル購読、他ユーザーのデータ取得、削除・更新操作では明示的な認可チェックが必要です。

7. まとめ

7-1. 接続・認証・認可・再接続を一連で考える

WebSocketの認証設計では、「接続できたか」「ログイン済みか」「その操作を許可できるか」を別々に考える必要があります。接続成功は認証成功ではなく、認証成功は全操作の許可でもありません。

ブラウザ標準WebSocket APIでは任意のAuthorizationヘッダーを直接設定できないため、Cookieや接続後の認証メッセージなどを利用します。URLクエリに認証情報を載せる場合は、ログなどへの漏えいリスクを十分に考慮します。

さらに、長時間接続ではトークン期限切れが発生します。WebSocketが切断されたら、再接続前に認証情報の有効性を確認し、必要なら更新してから再認証します。更新できなければ無限再試行せず、再ログインへ誘導します。

7-2. WebSocket認証のチェックリスト

WebSocket認証は、接続時のトークン検証だけで完成するものではありません。接続中の認可、期限切れ、再接続、ログ管理まで含めて確認する必要があります。

また、認証方式はアプリケーション全体のログイン方式と合わせて設計すると管理しやすくなります。HTTP APIとWebSocketで完全に別の認証モデルを持つと、失効や再ログイン処理が複雑になりやすいためです。

実装時には、次の項目を確認してください。

  • WebSocket接続でも利用者を認証しているか
  • 認証と認可を区別しているか
  • ブラウザ標準WebSocket APIでは任意のAuthorizationヘッダーを設定できないことを理解しているか
  • Cookie方式と接続後認証方式のどちらが既存構成に合うか検討したか
  • Cookie利用時にOriginを検証しているか
  • Cookieに適切なSecureHttpOnlySameSite属性を設定しているか
  • URLへ長寿命アクセストークンを安易に含めていないか
  • トークンがアクセスログやデバッグログへ残らないか確認したか
  • 接続後認証方式では認証完了まで通常メッセージを拒否しているか
  • 未認証接続を無期限に保持しない設計になっているか
  • 認証済みユーザー情報をサーバー側の接続状態へ関連付けているか
  • クライアントが送るユーザーIDを無条件に信用していないか
  • チャンネル購読時に認可を確認しているか
  • 更新・削除などの操作ごとに認可を確認しているか
  • 長時間接続中のトークン期限切れを想定しているか
  • 再接続前にトークンの有効性を確認しているか
  • 必要ならHTTP側の認証更新処理を実行しているか
  • 更新に失敗した場合は無限再接続せず再ログインへ誘導しているか
  • 再接続後に再認証しているか
  • 再認証後に必要な購読状態を復元しているか
  • TLSを使っていても認証・認可が必要だと理解しているか

WebSocket認証で重要なのは、「トークンをどこに入れるか」だけではありません。接続、認証、認可、期限切れ、再接続を一続きの状態遷移として設計し、サーバー側で最終的な権限判断を行うことが、安全なリアルタイム通信につながります。

8. 参考リンク

この記事では、次の公式仕様・公式資料を参考にしています。