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, RefreshIntoВернуть 429
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() (или Validate() пользовательских Claims) возвращает дескриптивную ошибку (например, errors.New("user_id is required")), а не сигнальную. Processor оборачивает её в ErrInvalidClaims:

invalid claims: user_id is required
└── ErrInvalidClaims (сигнальная, внешний слой)
    └── user_id is required (дескриптивная, внутренний слой)

Поэтому сопоставление двухуровневое:

go
if errors.Is(err, jwt.ErrInvalidClaims) {
    // Это категория «ошибка валидации Claims»
    fmt.Println("Подробности:", err) // invalid claims: user_id is required
}

Ошибки разбора ParseUnverified

Ошибки разбора, возвращаемые ParseUnverified при malformed токене (сбой декодирования base64, сбой разбора JSON и т.п.), являются обёрнутыми ошибками, а не сигнальными:

go
err := processor.ParseUnverified(malformedToken, &claims)
if err != nil {
    // ❌ Невозможно сопоставить конкретную причину через errors.Is
    // ✅ Можно лишь констатировать факт «сбой разбора»
    fmt.Println("Сбой разбора:", err) // failed to parse token: ...
}

Единственные две сигнальные ошибки ParseUnverifiedErrProcessorClosed (Processor закрыт) и ErrEmptyToken (передана пустая строка); остальные ошибки формата невозможно точно сопоставить через errors.Is.

Когда использовать errors.Is vs errors.As

  • errors.Is: сопоставление сигнальных ошибок (ErrTokenExpired, ErrInvalidClaims и т.д.) для определения «к какой категории отказов относится ошибка».
  • errors.As: извлечение структурированных ошибок (*ValidationError) для получения «какое именно поле и в чём ошиблось».
  • Оба можно комбинировать: сначала errors.Is для категории, затем errors.As для деталей.

Сопоставление с HTTP-статусами

В RESTful API отображение ошибок JWT на соответствующие HTTP-статусы — лучшая практика, позволяющая клиенту различать «проблемы с учётными данными» (401), «проблемы формата запроса» (400) и «серверные проблемы» (500).

Таблица сопоставления

Ошибка JWTHTTP-статусДействие клиента
ErrEmptyToken401 UnauthorizedПредоставьте токен аутентификации
ErrInvalidToken401 UnauthorizedПовторный вход
ErrAlgorithmMismatch401 UnauthorizedИсточник токена недоверенный, повторный вход
ErrTokenExpired401 UnauthorizedОбменять refresh-токен на новый
ErrTokenRevoked401 UnauthorizedТокен отозван, повторный вход
ErrTokenInvalidIssuer401 UnauthorizedИздатель токена не совпадает
ErrTokenInvalidAudience401 UnauthorizedАудитория токена не совпадает
ErrTokenNotValidYet401 UnauthorizedПроверьте синхронизацию часов клиента
ErrTokenTypeMismatch401 UnauthorizedИспользуйте правильный refresh-токен
ErrExpirationRequired401 UnauthorizedВ токене отсутствует утверждение истечения
ErrInvalidClaims400 Bad RequestИсправьте содержимое Claims (сценарий создания)
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 Error: ErrProcessorClosed — серверная аномалия состояния, которую не следует раскрывать клиенту.

Обработка ошибок в веб-сервисе

Следующий обработчик покрывает все распространённые ошибки, которые может вернуть 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 в эндпоинте обновления.

Дальнейшие шаги