ory/hydra · warning

Token is expired

Error message

Token is expired

What it means

jwt.MapClaims.Valid() (forked from golang-jwt) checks registered claims. When VerifyExpiresAt fails — the `exp` claim is in the past relative to TimeFunc() — it records 'Token is expired' into the ValidationError along with ValidationErrorExpired, aggregated with any other claim failures before returning.

Source

Thrown at fosite/token/jwt/map_claims.go:115

		if err != nil {
			return 0, false
		}

		return int64(vf), true
	}
	return 0, false
}

// Validates time based claims "exp, iat, nbf".
// There is no accounting for clock skew.
// As well, if any of the above claims are not in the token, it will still
// be considered a valid claim.
func (m MapClaims) Valid() error {
	vErr := new(ValidationError)
	now := TimeFunc().Unix()

	if !m.VerifyExpiresAt(now, false) {
		vErr.Inner = errors.New("Token is expired")
		vErr.Errors |= ValidationErrorExpired
	}

	if !m.VerifyIssuedAt(now, false) {
		vErr.Inner = errors.New("Token used before issued")
		vErr.Errors |= ValidationErrorIssuedAt
	}

	if !m.VerifyNotBefore(now, false) {
		vErr.Inner = errors.New("Token is not valid yet")
		vErr.Errors |= ValidationErrorNotValidYet
	}

	if vErr.valid() {
		return nil
	}

	return vErr

View on GitHub (pinned to 4174065ffb)

Solutions

  1. Refresh the token / obtain a new JWT before the exp deadline
  2. Fix clock synchronization (NTP) between issuing and validating services
  3. Issue tokens with a longer exp via lifespan configuration if legitimate lifetimes are too short
  4. Handle the expired-token error in the caller by triggering the refresh flow rather than failing the request

Example fix

// before
token, err := jwt.ParseWithClaims(raw, claims, keyFunc) // expired token fails
// after
token, err := jwt.ParseWithClaims(raw, claims, keyFunc)
if err != nil && errors.Is(err, jwt.ErrTokenExpired) {
    return refresh(ctx) // obtain a fresh token and retry
}
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-check expiry before parsing deeply
claims := jwt.MapClaims{}
if exp, ok := claims["exp"].(float64); ok && int64(exp) < time.Now().Unix() {
    return errors.New("token already expired; refresh first")
}

Try / catch

_, err := jwt.ParseWithClaims(raw, &claims, keyFunc)
if err != nil {
    var vErr *jwt.ValidationError
    if errors.As(err, &vErr) && vErr.Errors&jwt.ValidationErrorExpired != 0 {
        return refreshAccessToken(ctx) // recover via refresh flow
    }
    return err
}

Prevention

When it happens

Trigger: Parsing/validating a JWT whose exp claim is earlier than the current time — ParseWithClaims → MapClaims.Valid(); also triggered when the token carries no exp while validation requires it (VerifyExpiresAt returns false).

Common situations: Long-lived access tokens past their lifespan; clocks skewed between token issuer and validator; cached tokens reused after expiry; refresh flow not invoked before the access token lapsed.

Understand the failure class

Related errors


AI-assisted analysis of ory/hydra@4174065ffb (2026-09-03). Data as JSON: /api/errors/c55643416463f88e. Report an issue: GitHub.