【入門編】 リフレッシュトークンのライフサイクル管理と無効化 – Web APIアーキテクチャ・データ連携実践ガイド

こんにちは!ネットワークの裏側やWebの仕組みを覗くのが大好きな、技術メディアの主筆ライターです。

インフラやネットワークの世界に初めて足を踏み入れると、英語の専門用語や次々と出てくるカタカナの仕組みに、少し圧倒されてしまいますよね。「APIって何だか難しそう…」「トークンって一体どういうこと?」と感じている方も多いのではないでしょうか。

でも、安心してください。一歩ずつ、身近な例えから紐解いていけば、決して難しくありません。今回は、Webシステムのセキュリティを支える重要パーツである「リフレッシュトークンのライフサイクル管理と無効化(トークンローテーション)」について、現実世界の郵便配達に例えながら優しく解説していきますね!

—

1. Webの身分証「アクセストークン」と、そのお悩み

皆さんは、テーマパークの入場券や、ホテルのルームキーをイメージしてみてください。改札やドアを通るたびに、スタッフにパスポートを何ページも見せるのは面倒ですよね。そこで「この券を持っている人は、今日のところは自由に出入りしていいですよ」という小さな証明書をもらいます。これが、Webの世界における「アクセストークン」です。

APIを使ってサーバーからデータを取得するとき、アプリはこのアクセストークンを身分証として提示します。しかし、ここでセキュリティ上の大きなジレンマが生まれます。

  • 有効期限を長くした場合: 万が一、その身分証(トークン)を悪意ある第三者に盗まれてしまうと、長期間にわたってシステムに侵入され放題になってしまいます。
  • 有効期限を短くした場合: 安全性は高まりますが、ユーザーはすぐに「有効期限切れです。もう一度パスワードを入力してください」と追い出されてしまい、使い勝手が非常に悪くなります。

「セキュリティは強固にしたいけれど、ユーザーには快適に使ってもらいたい…!」このジレンマを鮮やかに解決するのが、「アクセストークン」と「リフレッシュトークン」の二刀流なんです。

—

2. 郵便の「書留(再発行チケット)」に例えるライフサイクル

この仕組みを、私たちの身近な「郵便配達と書留の受け取り」に例えてみましょう。

1. ログイン(初回の手続き):
ユーザーがIDとパスワードでログインすると、サーバーは2つのものを渡します。

  • アクセストークン(短命な身分証): 例えば「有効期限が15分間」のパスポート。
  • リフレッシュトークン(長命な引換券): 例えば「有効期限が14日間」の特別な引換券。

2. APIの利用(日々のやり取り):
アプリは、15分間はアクセストークンだけでスイスイAPI通信を行います。
3. 期限切れと再発行(裏での自動連携):
15分が過ぎてアクセストークンが使えなくなると、アプリはユーザーに「もう一度ログインしてください」とは言いません。その代わり、持っていたリフレッシュトークンをそっとサーバーに見せ、「新しいアクセストークンをください」と頼みます。
サーバーは「お、ちゃんとした引換券ですね」と確認し、新しいアクセストークンをそっと発行します。ユーザーは何も意識することなく、そのままアプリを使い続けられるわけです。

非常にスマートですよね! しかし、ここで一つ重大なリスクが生じます。もし、この「リフレッシュトークン(引換券)」自体が悪い人に盗まれてしまったらどうなるでしょうか?

—

3. 盗難リスクに立ち向かう「トークンローテーション」

もしリフレッシュトークンが丸ごと盗まれてしまうと、犯人はその引換券を使って、何回も新しいアクセストークンを裏で作り出し、長期間システムに居座り続けることができてしまいます。

ここで登場するのが、今回の本題である「トークンローテーション(使い捨ての原則)」です。

トークンローテーションの仕組み

郵便の引換券を「1回使ったら、次の新しい引換券に自動で交換する」システムにします。

  • アプリがリフレッシュトークンを使って「新しいアクセストークンをください」と要求する。
  • サーバーは新しいアクセストークンを渡すのと同時に、「今まで使っていた古いリフレッシュトークンをゴミ箱に捨て、新しいリフレッシュトークンを発行」します。

もし盗難にあっていたら?(不正検知のドラマ)

ここで恐ろしいけれど面白いセキュリティのドラマが起きます。
もし本物のユーザーより先に、悪意ある攻撃者が盗んだ古いリフレッシュトークンを使ってしまったとしましょう。

1. 攻撃者が古いリフレッシュトークンを使って新トークンを要求 $\rightarrow$ 成功(攻撃者はトークンを手に入れる)。
2. この時点で、サーバー側ではそのリフレッシュトークンは「使用済み」になり、新しい別のリフレッシュトークンにすり替わっている。
3. 後から、本物のユーザーがアプリから同じ(古い)リフレッシュトークンを使ってアクセスしてしまう。
4. サーバーは気づきます。「あれ? このリフレッシュトークンは、さっきもう使われたはずなのに、どうしてまたここにあるんだ?」

ここでサーバーはピンと来ます。「おい、このトークンは盗まれて悪用されているぞ!」と。
サーバーは直ちにセキュリティアラートを鳴らし、現在発行されているすべてのアクセストークンとリフレッシュトークンを強制的に無効化(ブラックリスト入り)し、ユーザーに「安全のため、もう一度ログインし直してください」と強制ログアウトさせるのです。

これが、実務で使われている高度な無効化とライフサイクル管理のリアルな動きです。

—

4. 実装のイメージを見てみましょう(Pythonでのコード例)

「なるほど、概念は分かったけれど、実際にはどうやってプログラムを書くの?」と思った方のために、Python(FlaskなどのWebフレームワークを想定)を使ったシンプルなリフレッシュ処理とローテーションのイメージコードを用意しました。

日本語のコメントを丁寧に記述したので、処理の流れを追ってみてくださいね。

from datetime import datetime, timedelta
from flask import Flask, jsonify, request

app = Flask(__name__)

# 【シミュレーション用のデータベース】
# 本番環境ではRedisやRDB(PostgreSQLなど)を使用します
# リフレッシュトークンとその無効化フラグ、紐づくユーザーを管理します
token_database = {
    # "リフレッシュトークン文字列": {"user_id": 123, "is_revoked": False}
}


@app.route("/api/v1/refresh", methods=["POST"])
def refresh_token_endpoint():
    """リフレッシュトークンを受け取り、アクセストークンと

    リフレッシュトークンの両方を新しく発行する(ローテーション)エンドポイント
    """
    data = request.get_json()
    client_refresh_token = data.get("refresh_token")

    # 1. リフレッシュトークンが送信されてきているかチェック
    if not client_refresh_token:
        return (
            jsonify(
                {"error": "リフレッシュトークンが見つかりません。"}
            ),
            400,
        )

    # 2. データベースにそのトークンが存在するか確認
    token_record = token_database.get(client_refresh_token)

    if not token_record:
        # トークンが存在しない(あるいは偽物)
        return jsonify({"error": "無効なリフレッシュトークンです。"}),  401

    # 3. すでに無効化されている(使い回しが検知された)場合
    if token_record["is_revoked"]:
        # 【超重要】ここで不正アクセスの可能性を検知!
        # このユーザーに関連する全てのトークンを強制無効化する処理をここに実装します
        print(
            "【警告】すでに使用済みのリフレッシュトークンが使われました!不正アクセスの可能性があります。"
        )
        return (
            jsonify(
                {
                    "error": (
                        "セキュリティ上の理由により、再ログインが必要です。"
                    )
                }
            ),
            403,
        )

    # 4. 【ローテーション実行】古いリフレッシュトークンを「使用済み(無効)」にする
    token_record["is_revoked"] = True

    # 5. 新しいアクセストークンと、新しいリフレッシュトークンを生成する
    user_id = token_record["user_id"]
    new_access_token = generate_new_access_token(user_id)
    new_refresh_token = generate_new_refresh_token(user_id)

    # 新しいリフレッシュトークンをデータベースに登録(初期状態は有効)
    token_database[new_refresh_token] = {
        "user_id": user_id,
        "is_revoked": False,
    }

    # 6. クライアント(アプリやブラウザ)へ新しいペアを返す
    return (
        jsonify(
            {
                "access_token": new_access_token,
                "refresh_token": new_refresh_token,
            }
        ),
        200,
    )


def generate_new_access_token(user_id):
    # ダミーのアクセストークン生成関数(実際にはJWTや暗号署名付きの文字列など)
    return f"access_token_for_user_{user_id}_{datetime.now().timestamp()}"


def generate_new_refresh_token(user_id):
    # ダミーのリフレッシュトークン生成関数(推測しにくいランダムな文字列)
    return f"refresh_token_rot_{user_id}_{datetime.now().timestamp()}"


if __name__ == "__main__":
    app.run(debug=True)

コードのポイントは、token_record["is_revoked"] = True の部分です。古いトークンを即座に「使用済み」のステータスに更新することで、万が一の盗難や不正利用を見逃さない仕組みを作っているのが分かりますよね。

—

現場のインフラやAPI設計においては、こうしたトークンの寿命管理(ライフサイクル)をどこまで厳密に行うかが、Webサービスの信頼性を大きく左右します。「セキュリティは厳しく、使い勝手は軽やかに」というトレードオフを、このトークンローテーションという巧みな技術が支えているのです。

最初は覚えることが多くて大変に感じるかもしれませんが、現実世界のルールや身近な例えに置き換えてみると、パケットやコードの向こう側で起きているドラマがグッと身近に感じられるはずです。

それでは、また次回の技術解説でお会いしましょう! 一歩ずつ、楽しくネットワークとWebの深淵を覗いていきましょうね。

コメント

タイトルとURLをコピーしました