0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Rails】/auth/validate_token 実行時に access-token が頻繁に変わるの知らなかった

Posted at

はじめに

こんにちは。アメリカ在住で独学エンジニアを目指している 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 のインターセプタを使用して常時監視するのがおすすめです
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?