Skip to content

エラー処理

CyberGo JWT はセンチネルエラー(sentinel errors)パターンを使用しており、すべてのエラーは errors.Is() で判定します。

基本パターン

go
claims, valid, err := processor.Validate(tokenString)
if err != nil {
    switch {
    case errors.Is(err, jwt.ErrTokenExpired):
        // トークン有効期限切れ
    case errors.Is(err, jwt.ErrTokenRevoked):
        // トークンが失効済み
    case errors.Is(err, jwt.ErrTokenInvalidIssuer):
        // 発行者が一致しない
    case errors.Is(err, jwt.ErrTokenInvalidAudience):
        // オーディエンスが一致しない
    case errors.Is(err, jwt.ErrInvalidToken):
        // 署名が無効またはフォーマットエラー
    case errors.Is(err, jwt.ErrProcessorClosed):
        // Processor がクローズ済み
    default:
        // その他のエラー
    }
}

errors.Is() の使用

err == jwt.ErrTokenExpired や文字列マッチングは使用しないでください。errors.Is() はラップされたエラーも正しく処理します。

エラーの分類

設定段階

jwt.New() は以下のエラーを返す可能性があります:

エラー原因解決方法
ErrInvalidConfig複数の設定項目が不正Config の各フィールドを確認
ErrInvalidSecretKeyHMAC 秘密鍵が 32 バイト未満または弱鍵より強力な鍵を使用
ErrInvalidSigningMethodサポートされていない署名アルゴリズム内蔵の 12 種のアルゴリズムを使用

トークン操作

エラーメソッド処理の推奨
ErrEmptyTokenすべてのトークン操作メソッドリクエストヘッダーを確認
ErrInvalidTokenValidate, Refresh, ValidateInto, RefreshInto, Revoke, IsRevoked署名の不一致、アクセスを拒否
ErrAlgorithmMismatchValidate, Refresh, ValidateInto, RefreshIntoトークンのアルゴリズムが設定と不一致、アクセスを拒否
ErrExpirationRequiredValidate, Refresh, ValidateInto, RefreshIntoRequireExpiration 有効だがトークンに exp クレームなし
ErrTokenTypeMismatchRefresh, RefreshIntoアクセストークン(token_type=access)でリフレッシュ試行、アクセスを拒否
ErrTokenExpiredValidate, Refresh, ValidateInto, RefreshIntoユーザーにトークンのリフレッシュを案内
ErrTokenNotValidYetValidate, Refresh, ValidateInto, RefreshIntoクロックの同期を確認
ErrTokenInvalidIssuerValidate, Refresh, ValidateInto, RefreshInto, Revoke, IsRevoked発行者が一致しない
ErrTokenInvalidAudienceValidate, Refresh, ValidateInto, RefreshInto, Revoke, IsRevokedオーディエンスが一致しない
ErrTokenRevokedValidate, Refresh, ValidateInto, RefreshIntoトークンが失効済み、アクセスを拒否
ErrInvalidClaimsCreate, CreateRefresh, Validate, Refresh, ValidateInto, RefreshIntoビジネス検証の失敗
ErrTokenMissingIDRevoke, IsRevokedトークンに jti がない

レート制限とブラックリスト

エラーメソッド処理の推奨
ErrRateLimitExceededCreate, CreateRefresh, Refresh, RefreshInto429 を返す
ErrBlacklistNotConfiguredRevokeブラックリストを設定

ライフサイクル

エラーメソッド処理の推奨
ErrProcessorClosedすべてのメソッドProcessor を再作成
ErrStoreClosedRevoke などストアがクローズ済み

エラー型

ValidationError

フィールドレベルの検証失敗時に返され、具体的なフィールドとエラー情報を含みます:

go
type ValidationError struct {
    Field   string  // エラーが発生したフィールド名
    Message string  // エラーの説明
    Err     error   // 内部エラー
}

エラーラッピングチェーン

CyberGo JWT のエラーはセンチネルエラー(errors.Is でマッチング可能)とラップエラー(errors.As で構造化情報を抽出)に分かれます。ラッピングチェーンを理解することで、失敗原因を正確に特定できます。

ValidationError と errors.As

フィールドレベルの検証失敗(長さ超過、インジェクション検出など)は *ValidationError を返し、具体的なフィールド名とエラー情報を含みます。何層にラップされていても errors.As で貫通できます:

go
token, err := processor.Create(claims)
if err != nil {
    var ve *jwt.ValidationError
    if errors.As(err, &ve) {
        fmt.Printf("フィールド: %s, 理由: %s\n", ve.Field, ve.Message)
        // フィールド: user_id, 理由: suspicious pattern detected
        return
    }
    // フィールドレベルのエラーでない場合、errors.Is 分岐へ
}

ErrInvalidClaims が Claims.Validate() をラップ

Claims.Validate()(またはカスタム Claims の Validate())が返すのは記述的エラー(例:errors.New("user_id is required"))であり、センチネルエラーではありません。Processor はこれを ErrInvalidClaims でラップします:

invalid claims: user_id is required
└── ErrInvalidClaims(センチネル、外層)
    └── user_id is required(記述的、内層)

そのためマッチング方法は 2 層になります:

go
if errors.Is(err, jwt.ErrInvalidClaims) {
    // Claims 検証失敗のカテゴリである
    fmt.Println("詳細:", err) // invalid claims: user_id is required
}

ParseUnverified のパースエラー

ParseUnverified はトークンのフォーマットエラー(base64 デコード失敗、JSON パース失敗など)時に返すパースエラーはラップエラーであり、センチネルエラーではありません:

go
err := processor.ParseUnverified(malformedToken, &claims)
if err != nil {
    // ❌ errors.Is で具体的原因をマッチングできない
    // ✅ 「パース失敗」という事実のみ判定可能
    fmt.Println("パース失敗:", err) // failed to parse token: ...
}

ParseUnverified のセンチネルエラーは ErrProcessorClosed(Processor がクローズ済み)と ErrEmptyToken(空文字列の渡入)の 2 つのみで、その他のフォーマットエラーは errors.Is で正確にマッチングできません。

errors.Is vs errors.As の使い分け

  • errors.Is:センチネルエラー(ErrTokenExpiredErrInvalidClaims など)をマッチングし、「どのカテゴリの失敗か」を判定。
  • errors.As:構造化エラー(*ValidationError)を抽出し、「具体的にどのフィールドで何が問題か」を取得。
  • 両者は組み合わせ可能:まず errors.Is でカテゴリを特定し、次に errors.As で詳細を抽出。

HTTP ステータスコードマッピング

RESTful API では、JWT エラーを適切な HTTP ステータスコードにマッピングすることがベストプラクティスです——クライアントはこれに基づき「資格情報の問題」(401)、「リクエストフォーマットの問題」(400)、「サーバー側の問題」(500)を区別できます。

マッピング表

JWT エラーHTTP ステータスコードクライアントのアクション
ErrEmptyToken401 Unauthorized認証トークンを提供
ErrInvalidToken401 Unauthorized再ログイン
ErrAlgorithmMismatch401 Unauthorizedトークン送信元が非信頼、再ログイン
ErrTokenExpired401 Unauthorizedリフレッシュトークンで新トークンを取得
ErrTokenRevoked401 Unauthorizedトークンが失効済み、再ログイン
ErrTokenInvalidIssuer401 Unauthorizedトークン発行者が不一致
ErrTokenInvalidAudience401 Unauthorizedトークンオーディエンスが不一致
ErrTokenNotValidYet401 Unauthorizedクライアントのクロック同期を確認
ErrTokenTypeMismatch401 Unauthorized正しいリフレッシュトークンを使用
ErrExpirationRequired401 Unauthorizedトークンに有効期限クレームがない
ErrInvalidClaims400 Bad RequestClaims の内容を修正(作成シーン)
ErrRateLimitExceeded429 Too Many Requestsリクエスト頻度を下げ、後で再試行
ErrProcessorClosed500 Internal Server Errorサーバー側で Processor の再起動が必要

RESTful ベストプラクティス

  • 401 Unauthorized:すべてのトークン有効性の問題(期限切れ、失効、署名エラー、発行者/オーディエンス不一致)。クライアントは再認証やトークンのリフレッシュを案内すべきです。
  • 400 Bad Request:トークン作成時の Claims 検証失敗——これは認証失敗ではなく呼び出し側のプログラミングエラーです。
  • 429 Too Many Requests:レート制限のトリガ時にこのコードを返し、Retry-After ヘッダーで待機時間をクライアントに通知します。
  • 500 Internal Server ErrorErrProcessorClosed はサーバー側の状態異常であり、クライアントに露出させるべきではありません。

Web サービスでのエラー処理

以下のハンドラは Validate が返す可能性のある一般的なエラーをすべてカバーし、HTTP ステータスコードマッピングに従い適切なレスポンスを返します:

go
package main

import (
    "encoding/json"
    "errors"
    "net/http"

    "github.com/cybergodev/jwt"
)

// authError は JWT エラーを HTTP ステータスコードとメッセージにマッピング
func authError(w http.ResponseWriter, err error) {
    w.Header().Set("Content-Type", "application/json")

    switch {
    // トークン期限切れ — クライアントにリフレッシュを案内
    case errors.Is(err, jwt.ErrTokenExpired):
        w.WriteHeader(http.StatusUnauthorized)
        json.NewEncoder(w).Encode(map[string]string{
            "error":   "token_expired",
            "message": "トークンの有効期限が切れました、リフレッシュしてください",
        })

    // トークンが失効済み
    case errors.Is(err, jwt.ErrTokenRevoked):
        w.WriteHeader(http.StatusUnauthorized)
        json.NewEncoder(w).Encode(map[string]string{
            "error":   "token_revoked",
            "message": "トークンは失効されました",
        })

    // 発行者が不一致
    case errors.Is(err, jwt.ErrTokenInvalidIssuer):
        w.WriteHeader(http.StatusUnauthorized)
        json.NewEncoder(w).Encode(map[string]string{
            "error":   "invalid_issuer",
            "message": "発行者が一致しません",
        })

    // オーディエンスが不一致
    case errors.Is(err, jwt.ErrTokenInvalidAudience):
        w.WriteHeader(http.StatusUnauthorized)
        json.NewEncoder(w).Encode(map[string]string{
            "error":   "invalid_audience",
            "message": "オーディエンスが一致しません",
        })

    // まだ有効でない — クロック不同期
    case errors.Is(err, jwt.ErrTokenNotValidYet):
        w.WriteHeader(http.StatusUnauthorized)
        json.NewEncoder(w).Encode(map[string]string{
            "error":   "token_not_valid_yet",
            "message": "トークンはまだ有効ではありません",
        })

    // アルゴリズム不一致
    case errors.Is(err, jwt.ErrAlgorithmMismatch):
        w.WriteHeader(http.StatusUnauthorized)
        json.NewEncoder(w).Encode(map[string]string{
            "error":   "algorithm_mismatch",
            "message": "署名アルゴリズムが一致しません",
        })

    // トークン無効(署名エラー、フォーマットエラー、空トークン)
    case errors.Is(err, jwt.ErrInvalidToken),
        errors.Is(err, jwt.ErrEmptyToken),
        errors.Is(err, jwt.ErrExpirationRequired):
        w.WriteHeader(http.StatusUnauthorized)
        json.NewEncoder(w).Encode(map[string]string{
            "error":   "invalid_token",
            "message": "トークンが無効です",
        })

    // Claims 検証失敗 — フィールドレベルの詳細を抽出試行
    case errors.Is(err, jwt.ErrInvalidClaims):
        var ve *jwt.ValidationError
        if errors.As(err, &ve) {
            w.WriteHeader(http.StatusBadRequest)
            json.NewEncoder(w).Encode(map[string]string{
                "error":   "validation_failed",
                "field":   ve.Field,
                "message": ve.Message,
            })
        } else {
            w.WriteHeader(http.StatusBadRequest)
            json.NewEncoder(w).Encode(map[string]string{
                "error":   "validation_failed",
                "message": "クレーム検証に失敗しました",
            })
        }

    // レート制限
    case errors.Is(err, jwt.ErrRateLimitExceeded):
        w.Header().Set("Retry-After", "60")
        w.WriteHeader(http.StatusTooManyRequests)
        json.NewEncoder(w).Encode(map[string]string{
            "error":   "rate_limited",
            "message": "リクエストが多すぎます、後で再試行してください",
        })

    // システムエラー — Processor がクローズ済み
    case errors.Is(err, jwt.ErrProcessorClosed):
        w.WriteHeader(http.StatusInternalServerError)
        json.NewEncoder(w).Encode(map[string]string{
            "error":   "internal_error",
            "message": "一時的にサービスが利用できません",
        })

    // フォールバック
    default:
        w.WriteHeader(http.StatusUnauthorized)
        json.NewEncoder(w).Encode(map[string]string{
            "error":   "auth_failed",
            "message": "認証に失敗しました",
        })
    }
}

func handleProtected(w http.ResponseWriter, r *http.Request) {
    tokenString := extractToken(r)
    claims, valid, err := processor.Validate(tokenString)
    if err != nil {
        authError(w, err)
        return
    }
    if !valid {
        authError(w, jwt.ErrInvalidToken)
        return
    }
    // 認証成功、リクエストを処理
    _ = claims
}

authError の再利用

authError は具体的なルートに依存しないエラーマッピング関数であり、認証が必要なすべてのハンドラで再利用できます。リフレッシュエンドポイントErrTokenTypeMismatch を処理する際にも呼び出せます。

次のステップ