Amazon Cognito リフレッシュトークンローテーション検証

2025年4月にサポートが発表されたCognitoリフレッシュトークンローテーション機能を検証し、その動作とセキュリティ効果を解説します。

対象読者

  • 認証機能を扱うフロントエンド・バックエンド開発者
  • Cognitoを利用中または導入検討中の技術者
  • セキュリティ向上に関心のあるエンジニア

背景

2025年4月、Amazon Cognitoでリフレッシュトークンローテーション機能のサポートが発表されました。

Amazon Cognito が更新トークンのローテーションに対応

この機能により、リフレッシュトークンの使い回しによるセキュリティリスクを軽減できるようになりました。
本記事では実際の検証結果を紹介します。

Cognitoが発行するトーク

トーク 用途 有効期限 リスク
Access Token API認可 5分〜1日
ID Token ID情報 5分〜1日
Refresh Token トークン更新 60分〜10年

リフレッシュトークンは長寿命のため、漏洩時のセキュリティリスクが高くなります。

ローテーション機能の概要

従来の動作

ローテーション有効時

効果: 使用済みトークンの無効化により、リプレイ攻撃等のリスクを軽減

検証① - 機能確認

環境設定

検証を容易にするため、以下の設定でCognitoクライアントを作成しました:

  • クライアントシークレット: なし(検証の簡素化)
  • トークン有効期限: 設定可能な最短値
  • リフレッシュトークンローテーション: 有効
  • 猶予期間: 0秒
  • ALLOW_REFRESH_TOKEN_AUTH: 無効(ローテーション有効時の必須設定)

注意: 本検証では猶予期間を0秒に設定していますが、実際のプロダクション環境では、ネットワーク遅延や並列リクエストを考慮して適切な猶予期間を設定する必要がありそうです。

Cognitoクライアント編集画面

検証コード

import boto3
import requests
import time
 
# 以下の定数はご自身の環境に合わせて編集してください
REGION = "<YOUR_AWS_REGION>"
USER_POOL_ID = "<YOUR_USER_POOL_ID>"
COGNITO_DOMAIN = "<YOUR_COGNITO_DOMAIN>"
CLIENT_ID = "<YOUR_CLIENT_ID>"
USERNAME = "<YOUR_USERNAME>"
PASSWORD = "<YOUR_PASSWORD>"
 
def login():
    print("Logging in via AdminInitiateAuth...")
    client = boto3.client("cognito-idp", region_name=REGION)
    resp = client.admin_initiate_auth(
        UserPoolId=USER_POOL_ID,
        ClientId=CLIENT_ID,
        AuthFlow="ADMIN_NO_SRP_AUTH",
        AuthParameters={
            "USERNAME": USERNAME,
            "PASSWORD": PASSWORD
        }
    )
    return resp["AuthenticationResult"]
 
def refresh_token(refresh_token):
    print("Refreshing token via /oauth2/token...")
    url = f"https://{COGNITO_DOMAIN}/oauth2/token"
    data = {
        "grant_type": "refresh_token",
        "client_id": CLIENT_ID,
        "refresh_token": refresh_token,
    }
    headers = {"Content-Type": "application/x-www-form-urlencoded"}
    res = requests.post(url, data=data, headers=headers)
    res.raise_for_status()
    return res.json()
 
if __name__ == "__main__":
    first = login()
    print("\nInitial login:")
    print("Access Token:", first['AccessToken'][:40], "...")
    print("Refresh Token:", first['RefreshToken'][:40], "...")
     
    time.sleep(5)  # Wait a bit before refreshing
 
    second = refresh_token(first["RefreshToken"])
    print("\nAfter refresh:")
    print("New Access Token:", second["access_token"][:40], "...")
    print("New Refresh Token:", second["refresh_token"][:40], "...")
 
    # Test reusing old token
    try:
        print("\nTesting old refresh token...")
        refresh_token(first["RefreshToken"])
        print("WARNING: Old token reuse succeeded - rotation may not be enabled")
    except requests.exceptions.HTTPError as e:
        print("SUCCESS: Old token is invalidated - rotation is enabled!")
        print("Error:", e.response.status_code, e.response.text)

実行結果

Logging in via AdminInitiateAuth...

Initial login:
Access Token: eyJraWQiOiJBcGl1T1p2QXczeVNuNmNcL3RmTUlS ...
Refresh Token: eyJjdHkiOiJKV1QiLCJlbmMiOiJBMjU2R0NNIiwi ...
Refreshing token via /oauth2/token...

After refresh:
New Access Token: eyJraWQiOiJBcGl1T1p2QXczeVNuNmNcL3RmTUlS ...
New Refresh Token: eyJjdHkiOiJKV1QiLCJlbmMiOiJBMjU2R0NNIiwi ...

Testing old refresh token...
Refreshing token via /oauth2/token...
SUCCESS: Old token is invalidated - rotation is enabled!
Error: 400 {"error":"invalid_grant"}

結果: 古いリフレッシュトークンが正常に無効化されることを確認。

検証② - 有効期限への影響

疑問点

AWSの発表では「中断なくアクセス維持」とありますが、これは有効期限延長を意味するのでしょうか?

デベロッパーガイドには「新しい更新トークンは、元の更新トークンの残りの期間有効」と記載されています。

検証方法

1分間隔でトークンを更新し続け、元の有効期限で停止するかを確認します。

import boto3
import requests
import time
from datetime import datetime # 追加
 
# 定数定義(REGION、USER_POOL_ID等)とlogin()、refresh_token()関数は
# test00.pyと同じ内容のため省略
 
if __name__ == "__main__":
    result = login()
    refresh = result["RefreshToken"]
    access = result["AccessToken"]
 
    print("\nStart time:", datetime.now())
    print("Initial refresh token (first 30 chars):", refresh[:30], "...")
 
    while True:
        try:
            print(f"[{datetime.now().strftime('%H:%M:%S')}] Updating token...")
            result = refresh_token(refresh)
            access = result.get("access_token")
            new_refresh = result.get("refresh_token")
            if new_refresh:
                refresh = new_refresh  # Update if rotated
                print("Refresh token rotated.")
            else:
                print("Refresh token not rotated (same).")
        except requests.exceptions.HTTPError as e:
            print(f"Update failed at {datetime.now()}")
            print("Error:", e.response.status_code, e.response.text)
            break
 
        time.sleep(60)  # Wait 1 minute

結果

※長いので途中のログは省略しています。

Logging in via AdminInitiateAuth...

Start time: 2025-07-12 14:15:15.749232
Initial refresh token (first 30 chars): eyJjdHkiOiJKV1QiLCJlbmMiOiJBMj ...
[14:15:15] Updating token...
Refresh token rotated.
[14:16:16] Updating token...
Refresh token rotated.
[14:17:17] Updating token...
Refresh token rotated.

... (1分おきの継続的なローテーション) ...

[15:14:11] Updating token...
Refresh token rotated.
[15:15:12] Updating token...
Refresh token rotated.
[15:16:13] Updating token...
Update failed at 2025-07-12 15:16:14.626224
Error: 400 {"error":"invalid_grant"}

結果: 初回発行から約61分後に停止しています。有効期限は延長されないことがわかりました

結論

ローテーション機能は...

  • 古いトークンを無効化
  • 有効期限は元のトークンの残り期間を継承
  • セキュリティ向上のための機能であり、トークンの有効期限を延長する機能ではない

まとめ

検証結果

2つの検証により、以下のことが確認できました:

  1. ローテーション機能は正常に動作

    • 古いリフレッシュトークンは無効化される
    • {"error":"invalid_grant"}でエラーが返される
  2. 有効期限は延長されない

    • 初回発行から設定した期限(今回は60分)で無効化
    • ローテーションしても元のトークンの残り期間を継承
  3. AWSの発表の「中断なくアクセス維持」の意味

    • 有効期限延長ではなく、セキュアなローテーションによる安全性向上
    • 期限内であれば、毎回新しいトークンで安全に更新可能

実装時の注意点

今回の検証から分かった実装時の注意点:

  • エラーハンドリングが必須: 古いトークンの再利用は400エラーとなる
  • 設定の確認: ALLOW_REFRESH_TOKEN_AUTHを無効にする必要がある
  • 猶予期間の設定: 0秒の場合は即座に無効化、最大60秒まで設定可能
    • 実際のプロダクション環境では、ネットワーク遅延や並列リクエストを考慮して、適切な猶予期間を設定する必要があると考えられます。

リフレッシュトークンローテーションは、認証セキュリティを向上させる有効な機能であることがわかりました。