Skip to content

トークンリフレッシュとローテーション

CyberGo JWT は2層トークン設計を採用しています:短期のアクセストークンは API 認証に使用し、長期のリフレッシュトークンはアクセストークン失効後に新しいトークンを取得するために使用します。この設計はセキュリティとユーザー体験のバランスを取ります。

2層トークンモデル

トークン種別発行メソッドデフォルト TTL用途
アクセストークンCreate15 分API 認証、頻繁な検証
リフレッシュトークンCreateRefresh7 日新しいアクセストークンとの交換、使用頻度が低い

トークン種別は token_type クレーム(access / refresh)でマークされます。Refresh メソッドは access 種別のトークンを拒否し、アクセストークンがリフレッシュに使用されるのを防ぎます。

TTL 設定

go
cfg := jwt.DefaultConfig()
cfg.SecretKey = "hmac-key-that-has-at-least-32-bytes!"
cfg.AccessTokenTTL = 15 * time.Minute    // アクセストークン有効期間
cfg.RefreshTokenTTL = 7 * 24 * time.Hour // リフレッシュトークン有効期間 (AccessTokenTTL より長い必要あり)

制約

Config.Validate()RefreshTokenTTL > AccessTokenTTL を要求し、違反時は ErrInvalidConfig を返します。

基本的なリフレッシュフロー

1. トークンペアの発行

go
package main

import (
    "fmt"
    "time"

    "github.com/cybergodev/jwt"
)

func main() {
    cfg := jwt.DefaultConfig()
    cfg.SecretKey = "hmac-key-that-has-at-least-32-bytes!"
    cfg.AccessTokenTTL = 15 * time.Minute
    cfg.RefreshTokenTTL = 7 * 24 * time.Hour

    processor, err := jwt.New(cfg)
    if err != nil {
        panic(err)
    }
    defer processor.Close()

    claims := &jwt.Claims{UserID: "user123", Username: "alice"}

    // アクセストークン(短期)
    accessToken, err := processor.Create(claims)
    if err != nil {
        panic(err)
    }

    // リフレッシュトークン(長期)
    refreshToken, err := processor.CreateRefresh(claims)
    if err != nil {
        panic(err)
    }

    fmt.Println("Access Token:", accessToken)
    fmt.Println("Refresh Token:", refreshToken)
}

2. 新しいアクセストークンのリフレッシュ

アクセストークン失効後、リフレッシュトークンを使って新しいトークンを取得します:

go
// refreshToken は以前に CreateRefresh で発行されたトークン
newAccessToken, err := processor.Refresh(refreshToken)
if err != nil {
    switch {
    case errors.Is(err, jwt.ErrTokenExpired):
        // リフレッシュトークン失効、ユーザーの再認証が必要
    case errors.Is(err, jwt.ErrTokenRevoked):
        // リフレッシュトークンが失効済み
    case errors.Is(err, jwt.ErrTokenTypeMismatch):
        // リフレッシュトークンではなくアクセストークンが渡された
    default:
        // その他のエラー
    }
    return
}
fmt.Println("New Access Token:", newAccessToken)

Refresh はリフレッシュトークンを完全に検証します: 署名、有効期限、発行者、オーディエンス、ブラックリスト状態。

カスタム Claims のリフレッシュ

カスタム Claims タイプを使用する場合、RefreshInto を使ってパース結果をカスタム構造体に格納します:

go
type MyClaims struct {
    UserID string `json:"user_id"`
    Role   string `json:"role"`
    jwt.RegisteredClaims
}

func (c *MyClaims) GetRegisteredClaims() *jwt.RegisteredClaims {
    return &c.RegisteredClaims
}

func (c *MyClaims) Validate() error {
    if c.UserID == "" {
        return errors.New("user_id is required")
    }
    return nil
}
go
// カスタム Claims でリフレッシュトークンを発行
refreshToken, err := processor.CreateRefresh(&MyClaims{UserID: "123", Role: "admin"})

// カスタム構造体にリフレッシュ
result := &MyClaims{}
newToken, err := processor.RefreshInto(refreshToken, result)

ローテーション戦略

再利用モード(デフォルト)

デフォルトでは、Refresh は元のリフレッシュトークンを失効しません。元のトークンは失効または明示的な失効まで有効で、複数回使用できます:

go
// 最初のリフレッシュ
token1, err := processor.Refresh(refreshToken)
if err != nil {
    panic(err)
}

// 同じ refreshToken がまだ有効で、再度リフレッシュ可能
token2, err := processor.Refresh(refreshToken)
if err != nil {
    panic(err)
}

適用シナリオ: モバイルアプリ、単一デバイスログイン。ユーザーは頻繁に再認証する必要がなく、リフレッシュトークンは TTL 内で繰り返し使用できます。

一回限りローテーション

セキュリティ要件が高いシナリオでは、各リフレッシュ後に古いリフレッシュトークンを即座に失効させ、一回限り使用を実現します:

go
// リフレッシュ後、即座に古いトークンを失効
newAccessToken, err := processor.Refresh(refreshToken)
if err != nil {
    panic(err)
}

// 古いリフレッシュトークンを失効させ、再使用不可にする
if err := processor.Revoke(refreshToken); err != nil {
    panic(err)
}

適用シナリオ: Web アプリケーション、高セキュリティシステム。各リフレッシュ後に古いトークンが即座に無効化され、トークン漏洩リスクを軽減します。

戦略比較

次元再利用モード一回限りローテーション
セキュリティ低い(漏洩トークンが再利用可能)高い(漏洩トークンは一回限り)
ユーザー体験良い(頻繁な再認証不要)普通(リフレッシュ失敗時に再ログイン必要)
実装複雑さ追加コード不要Revoke 呼び出し必要
ブラックリスト負荷低い高い(各リフレッシュでエントリ追加)

リフレッシュトークン漏洩検出

一回限りローテーションモードでは、攻撃者が失効済みリフレッシュトークンを使用すると RefreshErrTokenRevoked を返します。アプリケーション層でこれを用いてトークン漏洩を検出し、強制再認証を行えます。

タイプ安全性

CyberGo JWT は token_type クレームでトークン種別を区別します。RefreshRefreshInto はアクセストークンを拒否します:

go
// アクセストークンでリフレッシュを試みると拒否される
_, err := processor.Refresh(accessToken)
// err は ErrTokenTypeMismatch をラップ: expected refresh token, got access token

これによりアクセストークンが新しいトークンの取得に使用されるのを防ぎ、2層モデルのタイプ分離を保証します。

token_type クレームのないトークン(旧バージョンで発行されたトークン)は後方互換性のため受け入れられます。

セキュリティ上の注意

  • Refresh は自動失効しない: 元のリフレッシュトークンは Refresh 後も有効です。一回限りローテーションには手動で Revoke を呼び出す必要があります。
  • Claims の深層検証なし: Refresh は標準 JWT フィールド(署名、有効期限、発行者、オーディエンス、ブラックリスト)と基本構造(UserID または Username が空でない)を検証しますが、フィールド長制限やインジェクションパターンは再検証せず、作成時に検証されたことを信頼します。
  • 署名の一貫性: 新しいアクセストークンはリフレッシュトークンと同じ署名アルゴリズムとキーを使用します。アルゴリズム間のクロスリフレッシュはサポートされません。

次のステップ