Error message
token has invalid claims: token is expired
Also printed as
- Token is expired
- token has invalid claims: token is not valid yet
- token has invalid claims: token used before issued
golang-jwt v4 prints "Token is expired" (capital T). The other claim errors of v5 use the same "token has invalid claims:" prefix.
golang-jwt/jwt (Go) — jwt.ErrTokenInvalidClaims joined with jwt.ErrTokenExpired
What it means
The token's signature verified, but its `exp` claim lies in the past (beyond any leeway you configured). golang-jwt v5 reports this as `ErrTokenInvalidClaims` joined with `ErrTokenExpired`; the same family produces "token is not valid yet" for a future `nbf` and "token used before issued" for a future `iat`. The parsed token is still returned alongside the error, but it must not be trusted.
Why it happens
The token outlived its lifetime
commonAccess tokens typically live 5–60 minutes. Clients that cache a token, retry a request much later, or keep it in a long-running job present it after `exp`.
The verifier's clock is ahead
commonA server whose clock runs fast expires every token early. Containers inherit the host clock, so a single host without NTP affects all its services.
The client has no refresh path
occasionalSingle-page apps and CLIs that store an access token but never use the refresh token (or never handle 401) fail permanently once the first token expires.
exp was written in the wrong unit
rare`exp` is seconds since the epoch. An issuer that divides milliseconds twice or uses a local-time conversion can mint tokens that are already expired.
How to fix it
- 1.
Detect expiry and let the client refresh
Map `ErrTokenExpired` to 401 with an `invalid_token` hint. The client then uses its refresh token (or re-authenticates) and retries.
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.
Allow small clock skew, require exp
A leeway of seconds absorbs drift between machines. `WithExpirationRequired` closes the opposite gap: tokens without `exp` would otherwise never expire.
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 )