dgraph-io/dgraph · error

token contains an invalid JSON Web Token: Token is expired

Error message

token contains an invalid JSON Web Token: Token is expired

What it means

When jwt.Parse fails because the token's exp claim is in the past, ParseJWT rewraps it with errTokenExpired and jwt.ErrTokenInvalidClaims ('token contains an invalid JWT: Token is expired') for backward compatibility. It means the JWT is structurally fine but its lifetime has ended, so it must be refreshed.

Source

Thrown at x/jwt_helper.go:43

		return []byte(kb)
	}
	return k
}

func ParseJWT(jwtStr string) (jwt.MapClaims, error) {
	token, err := jwt.Parse(jwtStr, func(token *jwt.Token) (interface{}, error) {
		if WorkerConfig.AclJwtAlg == nil {
			return nil, errors.Errorf("ACL is disabled")
		}
		if token.Method.Alg() != WorkerConfig.AclJwtAlg.Alg() {
			return nil, errors.Errorf("unexpected signing method in token: %v", token.Header["alg"])
		}
		return MaybeKeyToBytes(WorkerConfig.AclPublicKey), nil
	})
	if err != nil {
		// This is for backward compatibility in clients
		if errors.Is(err, jwt.ErrTokenExpired) {
			err = errors.Wrap(errTokenExpired, jwt.ErrTokenInvalidClaims.Error())
		}
		return nil, errors.Wrapf(err, "unable to parse jwt token")
	}

	claims, ok := token.Claims.(jwt.MapClaims)
	if !ok || !token.Valid {
		return nil, errors.Errorf("claims in jwt token is not map claims")
	}
	return claims, nil
}

func ExtractUserName(jwtToken string) (string, error) {
	claims, err := ParseJWT(jwtToken)
	if err != nil {
		return "", err
	}
	userId, ok := claims["userid"].(string)
	if !ok {

View on GitHub (pinned to 759e242be6)

Solutions

  1. Obtain a fresh access JWT (e.g. via refresh token / Login API) and retry the request.
  2. Reduce client TTL assumptions: re-login or refresh before/at expiry instead of caching tokens indefinitely.
  3. Check clock synchronization (NTP) if the token looks freshly minted yet is reported expired.
  4. If the token should still be valid, inspect its exp claim (jwt.io / decoder) to confirm actual expiry.

Example fix

// before: reuse cached token forever
token := cachedToken
// after: refresh when expired
if claims, err := x.ParseJWT(token); err != nil && errors.Is(err, jwt.ErrTokenExpired) {
    token = refreshToken(cfg.RefreshJwt)
}
Defensive patterns

Strategy: retry

Validate before calling

claims := jwt.MapClaims{}
if _, _, err := jwt.NewParser().ParseUnverified(token, claims); err == nil {
    if exp, err2 := claims.GetExpirationTime(); err2 == nil && exp != nil && exp.Before(time.Now()) {
        token = refreshAccessToken() // fetch new token before calling the API
    }
}

Type guard

func isExpiredJwtErr(err error) bool { return err != nil && errors.Is(err, jwt.ErrTokenExpired) }

Try / catch

claims, err := x.ParseJWT(token)
if errors.Is(err, jwt.ErrTokenExpired) { // matches wrapped errTokenExpired
    token = refreshToken()
    claims, err = x.ParseJWT(token)
}

Prevention

When it happens

Trigger: Calling ParseJWT/ExtractUserName/ExtractNamespaceFromJwt with an access JWT whose exp claim is earlier than the current time (errors.Is(err, jwt.ErrTokenExpired) is true).

Common situations: Long-running client processes caching an access JWT past its TTL (default access-ttl); system clock skew between client and server; tokens minted long ago and replayed after server restarts.

Understand the failure class

Related errors


AI-assisted analysis of dgraph-io/dgraph@759e242be6 (2026-09-01). Data as JSON: /api/errors/fe367735752e9112. Report an issue: GitHub.