MeWrite Docs

JWT: Token has expired

JWTトークンの有効期限が切れた場合のエラー

概要

JSON Web Token(JWT)の有効期限(exp クレーム)が切れた場合に発生するエラーです。認証フローの再実行やトークンのリフレッシュが必要です。

エラーメッセージ

jwt.exceptions.ExpiredSignatureError: Signature has expired

または

JsonWebTokenError: jwt expired

原因

  1. 有効期限切れ: トークンのexp時刻を過ぎた
  2. 時刻のずれ: サーバー間の時刻同期の問題
  3. リフレッシュ忘れ: トークンの更新が行われていない
  4. 設定ミス: exp クレームが過去の時刻

解決策

1. トークンの有効期限設定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Node.js (jsonwebtoken)
const jwt = require('jsonwebtoken');

// アクセストークン(短い有効期限)
const accessToken = jwt.sign(
  { userId: user.id },
  process.env.JWT_SECRET,
  { expiresIn: '15m' }  // 15分
);

// リフレッシュトークン(長い有効期限)
const refreshToken = jwt.sign(
  { userId: user.id },
  process.env.REFRESH_SECRET,
  { expiresIn: '7d' }  // 7日
);

2. トークン検証

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Node.js
function verifyToken(token) {
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    return { valid: true, decoded };
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      return { valid: false, error: 'Token expired', expiredAt: error.expiredAt };
    }
    return { valid: false, error: 'Invalid token' };
  }
}

3. リフレッシュトークンフロー

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// リフレッシュエンドポイント
app.post('/auth/refresh', async (req, res) => {
  const { refreshToken } = req.body;

  try {
    const decoded = jwt.verify(refreshToken, process.env.REFRESH_SECRET);

    // 新しいアクセストークンを発行
    const newAccessToken = jwt.sign(
      { userId: decoded.userId },
      process.env.JWT_SECRET,
      { expiresIn: '15m' }
    );

    res.json({ accessToken: newAccessToken });
  } catch (error) {
    res.status(401).json({ error: 'Invalid refresh token' });
  }
});

4. フロントエンドでの自動リフレッシュ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Axios インターセプター
import axios from 'axios';

const api = axios.create({ baseURL: '/api' });

api.interceptors.response.use(
  response => response,
  async error => {
    const originalRequest = error.config;

    if (error.response.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;

      try {
        const { data } = await axios.post('/auth/refresh', {
          refreshToken: localStorage.getItem('refreshToken')
        });

        localStorage.setItem('accessToken', data.accessToken);
        originalRequest.headers.Authorization = `Bearer ${data.accessToken}`;

        return api(originalRequest);
      } catch (refreshError) {
        // リフレッシュも失敗 → ログアウト
        localStorage.clear();
        window.location.href = '/login';
      }
    }

    return Promise.reject(error);
  }
);

5. Python での実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import jwt
from datetime import datetime, timedelta

SECRET_KEY = 'your-secret-key'

def create_token(user_id):
    payload = {
        'user_id': user_id,
        'exp': datetime.utcnow() + timedelta(minutes=15),
        'iat': datetime.utcnow()
    }
    return jwt.encode(payload, SECRET_KEY, algorithm='HS256')

def verify_token(token):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
        return payload
    except jwt.ExpiredSignatureError:
        raise Exception('Token has expired')
    except jwt.InvalidTokenError:
        raise Exception('Invalid token')

6. 時刻ずれの許容

1
2
3
4
// 時刻のずれを許容(clock tolerance)
jwt.verify(token, secret, {
  clockTolerance: 30  // 30秒のずれを許容
});

7. トークンのブラックリスト

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Redis でブラックリスト管理
const redis = require('redis');
const client = redis.createClient();

async function blacklistToken(token, expiresIn) {
  await client.setEx(`blacklist:${token}`, expiresIn, 'true');
}

async function isBlacklisted(token) {
  const result = await client.get(`blacklist:${token}`);
  return result !== null;
}

8. セッション付きJWT

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// JWTにセッションIDを含める
const token = jwt.sign(
  {
    userId: user.id,
    sessionId: uuid.v4()  // セッションID
  },
  SECRET_KEY,
  { expiresIn: '15m' }
);

// セッションを無効化してログアウト
async function logout(sessionId) {
  await redis.del(`session:${sessionId}`);
}

9. トークンのペイロード確認

1
2
3
4
# JWT のデコード(署名検証なし)
echo "eyJhbGciOi..." | cut -d'.' -f2 | base64 -d | jq

# jwt.io でオンライン確認

10. セキュリティベストプラクティス

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// アクセストークン:メモリに保存
let accessToken = null;

// リフレッシュトークン:HttpOnly Cookie
res.cookie('refreshToken', refreshToken, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict',
  maxAge: 7 * 24 * 60 * 60 * 1000  // 7日
});

よくある間違い

  • localStorageにトークンを保存(XSS脆弱性)
  • リフレッシュトークンをクライアントに露出
  • トークンの有効期限が長すぎる
  • ログアウト時にサーバー側でトークンを無効化しない

関連エラー

Security の他のエラー

最終更新: 2025-12-09