Сообщение об ошибке
token has invalid claims: token is expired
Также выводится как
- Token is expired
- token has invalid claims: token is not valid yet
- token has invalid claims: token used before issued
golang-jwt v4 выводит «Token is expired» (с заглавной T). Остальные ошибки утверждений в v5 используют тот же префикс «token has invalid claims:».
golang-jwt/jwt (Go) — jwt.ErrTokenInvalidClaims joined with jwt.ErrTokenExpired
Что это означает
Подпись токена прошла проверку, но его утверждение `exp` указывает на прошлое (даже с учётом настроенного допуска, если он есть). golang-jwt v5 сообщает об этом как о `ErrTokenInvalidClaims`, объединённой с `ErrTokenExpired`; то же семейство ошибок выдаёт «token is not valid yet» для `nbf` в будущем и «token used before issued» для `iat` в будущем. Разобранный токен всё равно возвращается вместе с ошибкой, но доверять ему нельзя.
Почему это происходит
Срок жизни токена истёк
частоТокены доступа обычно живут 5–60 минут. Клиенты, которые кешируют токен, повторяют запрос намного позже или держат токен в долго работающем задании, предъявляют его уже после `exp`.
Часы проверяющей стороны спешат
частоСервер, у которого спешат часы, считает каждый токен просроченным раньше времени. Контейнеры наследуют часы хоста, поэтому один хост без NTP влияет на все свои сервисы.
Клиент не умеет обновлять токен
иногдаОдностраничные приложения и CLI, которые хранят токен доступа, но никогда не используют токен обновления (или никогда не обрабатывают 401), перестают работать насовсем, как только истекает срок действия первого токена.
В exp записаны не те единицы
редко`exp` — это секунды с начала эпохи. Издатель, который дважды делит миллисекунды или переводит время в местный часовой пояс, может выпускать уже просроченные токены.
Как исправить
- 1.
Распознавайте истечение срока действия и давайте клиенту обновить токен
Отвечайте на `ErrTokenExpired` кодом 401 с подсказкой `invalid_token`. Тогда клиент воспользуется токеном обновления (или пройдёт аутентификацию заново) и повторит запрос.
go token, err := jwt.Parse(raw, keyfunc, jwt.WithValidMethods([]string{"ES256"})) if errors.Is(err, jwt.ErrTokenExpired) { w.Header().Set("WWW-Authenticate", `Bearer error="invalid_token", error_description="token expired"`) http.Error(w, "token expired", http.StatusUnauthorized) // client refreshes and retries return } - 2.
Допускайте небольшое расхождение часов, требуйте exp
Допуск в несколько секунд компенсирует расхождение часов между машинами. `WithExpirationRequired` закрывает противоположную брешь: иначе токены без `exp` никогда не истекали бы.
go parser := jwt.NewParser( jwt.WithValidMethods([]string{"ES256"}), jwt.WithLeeway(30*time.Second), // absorbs clock skew for exp, nbf and iat jwt.WithExpirationRequired(), // tokens without exp are rejected too )