vxcontrol/pentagi · error

token is either expired or not active yet

Error message

token is either expired or not active yet

What it means

ValidateAPIToken classifies jwt.ErrTokenExpired or jwt.ErrTokenNotValidYet from jwt/v5 into "token is either expired or not active yet". The token parsed correctly but its exp claim is in the past or its nbf (not-before) claim is in the future relative to server time.

Source

Thrown at backend/pkg/server/auth/api_token_jwt.go:51

			Subject:   "api_token",
		},
	}
}

func ValidateAPIToken(tokenString, globalSalt string) (*models.APITokenClaims, error) {
	var claims models.APITokenClaims
	token, err := jwt.ParseWithClaims(tokenString, &claims, func(token *jwt.Token) (any, error) {
		// verify signing algorithm to prevent "alg: none"
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
		}
		return MakeJWTSigningKey(globalSalt), nil
	})
	if err != nil {
		if errors.Is(err, jwt.ErrTokenMalformed) {
			return nil, fmt.Errorf("token is malformed")
		} else if errors.Is(err, jwt.ErrTokenExpired) || errors.Is(err, jwt.ErrTokenNotValidYet) {
			return nil, fmt.Errorf("token is either expired or not active yet")
		} else {
			return nil, fmt.Errorf("token invalid: %w", err)
		}
	}

	if !token.Valid {
		return nil, fmt.Errorf("token is invalid")
	}

	return &claims, nil
}

View on GitHub (pinned to ea665308ba)

Solutions

  1. Have the client request/generate a new API token and retry; tokens are short-lived by design
  2. Verify server clock sync (NTP/chrony) on both issuer and validator to rule out skew
  3. Increase the ttl passed to MakeAPITokenClaims if tokens legitimately need a longer lifetime
  4. Confirm jwt.WithTimeFunc/leeway defaults are acceptable; add leeway if minor drift is expected

Example fix

// before
ttl := uint64(60) // 1 minute, expires almost immediately
token, _ := auth.MakeAPIToken(globalSalt, auth.MakeAPITokenClaims(tokenID, uhash, uid, rid, ttl))
// after
ttl := uint64(3600) // 1 hour
token, _ := auth.MakeAPIToken(globalSalt, auth.MakeAPITokenClaims(tokenID, uhash, uid, rid, ttl))
Defensive patterns

Strategy: retry

Validate before calling

// client-side pre-check before using a cached token
claims, _ := jwt.ParseUnverified(token)
if exp := claims.Claims.(jwt.MapClaims)["exp"]; exp == nil || float64(time.Now().Unix()) >= exp.(float64) {
    token = issueNewToken() // refresh before calling
}

Type guard

func tokenLooksExpired(tok string) bool {
    p, _ := jwt.NewParser().ParseUnverified(tok, &jwt.MapClaims{})
    if p == nil { return true }
    exp, _ := p.Claims.(*jwt.MapClaims).GetExpirationTime()
    return exp == nil || time.Now().Unix() >= int64(*exp)
}

Try / catch

if strings.Contains(err.Error(), "expired or not active yet") {
    token = issueNewToken()
    return doAuthenticatedRequest(token) // single retry with fresh token
}
return err

Prevention

When it happens

Trigger: Validating an API token whose ttl (used in MakeAPITokenClaims ExpiresAt = now + ttl seconds) has elapsed, or a token with a future IssuedAt/nbf, or validation happening on a server whose clock differs significantly from the issuing host.

Common situations: Long-lived API token finally expired after ttl seconds; client cached an old token past its expiry; server clocks skewed (VM drift, wrong TZ/RTC) making a fresh token appear not-yet-valid; tests using static timestamps.

Related errors


AI-assisted analysis of vxcontrol/pentagi@ea665308ba (2026-09-01). Data as JSON: /api/errors/f0aa5e5593489a2d. Report an issue: GitHub.