Skip to content
ecdsa.com
golang-jwt/jwt (Go)Go

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

How to fix it

  1. 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. 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
    )

Related errors

Browse the full signature error database