はじめに
こんにちは。アメリカ在住で独学エンジニアを目指している Taira です。
React + Rails API (Devise Token Auth) で認証機能を実装していると、/auth/validate_token
を叩くたびに access-token
の値が頻繁に変わる現象に遭遇することがあります。
私の場合はそれに気づかず、フロントの原因究明をずっとやって、半日潰れました。。。
今回はその失敗談を踏まえて、記事を書こうと思います。
1. /auth/validate_token
とは?
-
役割
access-token
,client
,uid
の 3 つを用いて、現在のトークンが有効かどうかを検証するエンドポイントです。 -
用途
- ページリロード後のセッション確認
- SPA の初期表示時にログイン状態を判定するためのチェック
2. なぜ access-token
が頻繁に変わるのか?
Devise Token Auth の仕様
Devise Token Auth では、トークンを毎回ローテーションする仕組みが標準で組み込まれています。
- API リクエストが成功すると、新しい
access-token
がレスポンスヘッダーに返る - 次回以降のリクエストでは、新しいトークンを使う必要がある
なぜローテーションするのか?
- セキュリティ強化:トークン盗用時のリスクを減らすため
- リプレイ攻撃対策:古いトークンを使い回せなくする
- 短寿命トークンによる安全なセッション維持
3. よくある問題点
-
フロント側で古いトークンを使ってしまう
-
/auth/validate_token
のレスポンスで更新されたトークンを保存しないと、次回リクエストで認証エラーになる
-
-
複数タブでセッションが競合する
- タブ A でトークン更新 → タブ B が古いトークンを送信 → エラー発生
4. フロント側での対応方法
ポイント
- レスポンスヘッダーの
access-token
が存在する場合は、常にストアを更新する - Zustand や Redux などの状態管理と
localStorage
/sessionStorage
を併用して保持する
コード例(Zustand & axios 使用)
axios のインターセプタを使用することで response の値をデフォルトで監視し続けることができます。
import { useAuthStore } from '@/stores/authStore';
import axios, { type InternalAxiosRequestConfig } from 'axios';
export const api = axios.create({
baseURL: `${import.meta.env.VITE_API_BASE_URL}/api/v1`,
withCredentials: false,
headers: { 'Content-Type': 'application/json' },
});
// ヘッダー名を定数化
const HEADER_ACCESS_TOKEN = 'access-token';
const HEADER_CLIENT = 'client';
const HEADER_UID = 'uid';
const HEADER_TOKEN_TYPE = 'token-type';
const HEADER_EXPIRY = 'expiry';
const { setAuth, clearAuth } = useAuthStore.getState();
// リクエスト前:現在のトークンをヘッダーに付与
api.interceptors.request.use((config: InternalAxiosRequestConfig) => {
const auth = useAuthStore.getState().auth;
if (auth) {
config.headers[HEADER_ACCESS_TOKEN] = auth['access-token'];
config.headers[HEADER_CLIENT] = auth.client;
config.headers[HEADER_UID] = auth.uid;
config.headers[HEADER_TOKEN_TYPE] = auth['token-type'];
config.headers[HEADER_EXPIRY] = auth.expiry;
}
return config;
});
// レスポンス後:新しいトークンがあればストア更新
api.interceptors.response.use(
(response) => {
const headers = response.headers;
if (headers[HEADER_ACCESS_TOKEN]) {
setAuth({
[HEADER_ACCESS_TOKEN]: headers[HEADER_ACCESS_TOKEN],
[HEADER_CLIENT]: headers[HEADER_CLIENT],
[HEADER_UID]: headers[HEADER_UID],
[HEADER_TOKEN_TYPE]: headers[HEADER_TOKEN_TYPE],
[HEADER_EXPIRY]: headers[HEADER_EXPIRY],
});
}
return response;
},
(error) => {
if (error.response?.status === 401) {
clearAuth(); // 401 はセッション切れとして扱う
}
return Promise.reject(error);
}
);
// 呼び出し方の例
import { api } from '@/lib/api';
import type { fetchTeachersResponse } from '../types/teacher';
export const fetchTeachers = async (): Promise<fetchTeachersResponse> => {
const response = await api.get<fetchTeachersResponse>('/teachers');
return response.data;
};
---
## まとめ
- `/auth/validate_token` 実行時に `access-token` が変わるのは **仕様通り**
- 毎回レスポンスヘッダーを確認してトークンを更新する必要がある
- Zustand や Redux でストアを更新し、axios のインターセプタを使用して常時監視するのがおすすめです。