go-kit/kit · error · ErrTokenExpired

JWT is expired

Error message

JWT is expired

What it means

Returned by jwt.NewParser when the underlying jwt.ValidationError has the ValidationErrorExpired bit set: the token's exp claim is in the past. The token may be otherwise perfectly well-formed and correctly signed — it has simply aged out of its validity window.

Source

Thrown at auth/jwt/middleware.go:36

	//
	// Deprecated: prefer JWTContextKey.
	JWTTokenContextKey = JWTContextKey

	// JWTClaimsContextKey holds the key used to store the JWT Claims in the
	// context.
	JWTClaimsContextKey contextKey = "JWTClaims"
)

var (
	// ErrTokenContextMissing denotes a token was not passed into the parsing
	// middleware's context.
	ErrTokenContextMissing = errors.New("token up for parsing was not passed through the context")

	// ErrTokenInvalid denotes a token was not able to be validated.
	ErrTokenInvalid = errors.New("JWT was invalid")

	// ErrTokenExpired denotes a token's expire header (exp) has since passed.
	ErrTokenExpired = errors.New("JWT is expired")

	// ErrTokenMalformed denotes a token was not formatted as a JWT.
	ErrTokenMalformed = errors.New("JWT is malformed")

	// ErrTokenNotActive denotes a token's not before header (nbf) is in the
	// future.
	ErrTokenNotActive = errors.New("token is not valid yet")

	// ErrUnexpectedSigningMethod denotes a token was signed with an unexpected
	// signing method.
	ErrUnexpectedSigningMethod = errors.New("unexpected signing method")
)

// NewSigner creates a new JWT generating middleware, specifying key ID,
// signing string, signing method and the claims you would like it to contain.
// Tokens are signed with a Key ID header (kid) which is useful for determining
// the key to use for parsing. Particularly useful for clients.
func NewSigner(kid string, key []byte, method jwt.SigningMethod, claims jwt.Claims) endpoint.Middleware {

View on GitHub (pinned to 78fbbceece)

Solutions

  1. Implement refresh: on this error, obtain a new token (re-login or refresh grant) and retry the request once
  2. Issue tokens with an appropriate exp (e.g. 15–60 min) and refresh them proactively before expiry
  3. Sync clocks (NTP/chrony) on signer and verifier to eliminate skew
  4. Ensure exp is expressed in seconds since epoch (NumericDate), not milliseconds

Example fix

// before: token signed once with a short life and reused forever
claims := &jwt.RegisteredClaims{ExpiresAt: jwt.NewNumericDate(time.Now().Add(5 * time.Minute))}

// after: sane lifetime plus client-side refresh-and-retry
if _, err := ep(ctx, req); err != nil && errors.Is(err, jwt.ErrTokenExpired) {
	newTok := refreshToken() // re-auth / refresh grant
	ctx = context.WithValue(ctx, jwt.JWTContextKey, newTok)
	_, err = ep(ctx, req)
}
Defensive patterns

Strategy: retry

Validate before calling

null

Type guard

null

Try / catch

resp, err := ep(ctx, req)
if err != nil && errors.Is(err, jwt.ErrTokenExpired) {
	if tok, ok := refreshToken(); ok { // re-login or refresh grant
		ctx = context.WithValue(ctx, jwt.JWTContextKey, tok)
		resp, err = ep(ctx, req) // single retry with the fresh token
	}
}
return resp, err

Prevention

When it happens

Trigger: Calling a protected endpoint with a token whose exp has elapsed; long-lived client (CLI, mobile app, background job) reusing a token issued hours/days earlier; clock skew between the issuing host and the verifying server making a just-issued token appear expired; signer that computed exp in milliseconds instead of seconds.

Common situations: No token refresh flow implemented; tokens minted with a very short lifetime; NTP drift on VMs/containers; migration from one auth provider that used different token lifetimes; caching a token in config instead of fetching fresh ones.

Related errors


AI-assisted analysis of go-kit/kit@78fbbceece (2026-08-15). Data as JSON: /api/errors/0dbdcee21e191737. Report an issue: GitHub.