Skip to content

fix: refresh access token per request to prevent 1-hour expiry 401 - #37

Open
no-problem-kyoichi wants to merge 1 commit into
mainfrom
fix/dynamic-token-refresh
Open

no-problem-kyoichi wants to merge 1 commit into
mainfrom
fix/dynamic-token-refresh

Conversation

@no-problem-kyoichi

Copy link
Copy Markdown
Contributor

Summary

  • FirestoreClient / StorageClient が init 時に捕獲した OAuth access token を使い続け、1 時間後に失効して全リクエストが 401 Unauthenticated で失敗する間欠バグを修正
  • 新設の内部型 TokenSource で取得戦略を明示(dynamic / emulator / staticToken)、各 API リクエスト発行時に tokenSource.currentToken() から最新を取得
  • .auto / .autoWithDatabaseGCPEnvironment.shared.getAccessToken() 経由で、既存の 5 分バッファ cache refresh に追従
  • public let token: String は後方互換のため残置(init 時の値を返す、非推奨コメント付き)

背景

reading-memory の Cloud Run サーバーで /v1/auth/initialize が間欠的に 500 を返す問題を観測。サーバーログに以下:

❌ [UserProfileRepo.getByUserID] FirestoreError for {userId}: Unauthenticated: {...}

調査の結果、FirestoreClient.tokenpublic let で init 時スナップショットに固定されており、長時間稼働する Cloud Run instance で GCP access token の TTL (1 時間) を超えると、その instance が捌く全 Firestore リクエストが失敗する構造であった。GCPEnvironment actor 側の cache refresh は正しく動作していたが、clients が参照していなかった。

AuthAdminClient.deleteUser は元から let token = try await getAccessToken() を毎回呼ぶ実装で正しかったため、同じパターンを FirestoreClientStorageClient にも適用した。

修正範囲

新規ファイル

  • Sources/Internal/GCPAuth/TokenSource.swift
    • 3 case enum (dynamic / emulator / staticToken)
    • init(config:resolvedToken:)GCPConfiguration からマッピング
    • currentToken() で最新トークンを返す

変更ファイル

  • Sources/FirestoreServer/Core/FirestoreClient.swift
    • internal let tokenSource: TokenSource を追加
    • 3 つの init で tokenSource を初期化
  • Sources/FirestoreServer/Operations/DocumentOperations.swift
    • getDocument / createDocument / updateDocument / deleteDocument / listDocuments の 5 箇所で try await tokenSource.currentToken() を呼ぶよう変更
  • Sources/FirestoreServer/Operations/QueryOperations.swift
    • runQueryRaw で同様の変更
  • Sources/FirebaseStorageServer/Core/StorageClient.swift
    • internal let tokenSource: TokenSource を追加
    • upload / download / delete / getMetadata の 4 箇所で同様の変更

テスト

  • Tests/InternalTests/GCPAuthTests.swift に 8 ケース追加
    • 4 config モード → TokenSource マッピングの確認
    • currentToken() の emulator / static 返却値の確認
    • dynamic case の契約確認

全 15 tests in GCPAuth suite pass。

Test plan

  • swift build 成功 (140s)
  • swift test --filter GCPAuth 全 pass (15 tests)
  • swift test 全 suite pass (connection refused の 1 network test は外部依存で本修正と無関係)
  • merge 後に reading-memory の Package.resolved 更新 → 再デプロイ → 1 時間以上連続稼働させて 401 が消えることを確認

後方互換

  • public let token: String は残置 (init 時の値を返す)。外部コードが直接参照していた場合、挙動は変わらず (失効するトークンを返す)。doc コメントで非推奨を明記。
  • 既存の init shape は全て維持。破壊的変更なし。semver patch リリース (v1.0.18) の対象。

🤖 Generated with Claude Code

Cloud Run の長時間稼働インスタンスで発生していた間欠的な Firestore /
Storage の 401 Unauthenticated エラーを解消する。

## 症状

Cloud Run instance が 1 時間以上生きると、その instance が捌く全
Firestore / Storage リクエストが Unauthenticated で失敗。新しい
instance が spawn されるまで復旧しない。reading-memory プロジェクトで
/v1/auth/initialize が間欠的に 500 を返す問題として観測されていた。

## 根本原因

`FirestoreClient.token` と `StorageClient.token` が init 時のスナップ
ショットで `public let` として固定されていた。`GCPEnvironment` 側に
5 分バッファの cache refresh 機構が存在したが、clients はそれを参照
していなかったため、1 時間後 (GCP access token の TTL) に失効した
トークンを使い続ける結果となっていた。

## 修正

新設の内部型 `TokenSource` で token 取得戦略を表現し、各 API
リクエスト発行時に `tokenSource.currentToken()` を通じて最新トークン
を取得する実装に変更:

- `.dynamic` → `GCPEnvironment.shared.getAccessToken()` 経由
  (cache の 5 分バッファ refresh に追従、metadata server への実
   アクセスは 55 分に 1 回程度)
- `.emulator` → ダミー "owner" トークン
- `.staticToken` → `.explicit(projectId:token:)` で指定された
  静的トークン

影響する operations:
- `FirestoreClient`: getDocument, createDocument, updateDocument,
  deleteDocument, listDocuments, runQueryRaw
- `StorageClient`: upload, download, delete, getMetadata
- `AuthAdminClient.deleteUser` は元から per-request fetch 実装
  のため、本修正の対象外

## 後方互換

`public let token: String` は残置。init 時の値を返す。外部コードが
直接参照していた場合は振る舞い変わらず (失効するトークンが返る)。
将来削除する可能性があるため、doc コメントで非推奨を明示。

## テスト

`TokenSource` の 8 ケース (auto / autoWithDatabase / emulator /
explicit 各モードの生成、currentToken() の返却値、dynamic case
の契約) を新規追加。全 15 tests in GCPAuth suite pass。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant