hashicorp/nomad · error

unable to verify signature of JWT token: %v

Error message

unable to verify signature of JWT token: %v

What it means

Validate wraps any failure from the underlying JWT validator (bad signature, expired token, malformed token, wrong audience) with this message. It means the token could not be cryptographically/structurally verified against the auth method's configuration.

Source

Thrown at lib/auth/jwt/validator.go:69

	toAlgFn := func(m string) jwt.Alg { return jwt.Alg(m) }
	algorithms := helper.ConvertSlice(methodConf.SigningAlgs, toAlgFn)

	expected := jwt.Expected{
		Audiences:         methodConf.BoundAudiences,
		SigningAlgorithms: algorithms,
		NotBeforeLeeway:   methodConf.NotBeforeLeeway,
		ExpirationLeeway:  methodConf.ExpirationLeeway,
		ClockSkewLeeway:   methodConf.ClockSkewLeeway,
	}

	validator, err := jwt.NewValidator(keySet)
	if err != nil {
		return nil, err
	}

	claims, err := validator.Validate(ctx, token, expected)
	if err != nil {
		return nil, fmt.Errorf("unable to verify signature of JWT token: %v", err)
	}

	// validate issuer manually, because we allow users to specify an array
	if len(methodConf.BoundIssuer) > 0 {
		if _, ok := claims["iss"]; !ok {
			return nil, fmt.Errorf(
				"auth method specifies BoundIssuers but the provided token does not contain issuer information",
			)
		}
		if iss, ok := claims["iss"].(string); !ok {
			return nil, fmt.Errorf("unable to read iss property of provided token")
		} else if !slices.Contains(methodConf.BoundIssuer, iss) {
			return nil, fmt.Errorf("invalid JWT issuer: %v", claims["iss"])
		}
	}

	return claims, nil
}

View on GitHub (pinned to 482b49bf1a)

Solutions

  1. Read the wrapped %v cause to distinguish expired vs bad-signature vs malformed
  2. Re-obtain a fresh token and retry immediately
  3. Verify the auth method's JWKSURL/KeySource/OIDCDiscoveryURL and BoundAudiences match your IdP
  4. Check clock skew between client and server; ensure IdP keys haven't rotated without cache refresh

Example fix

// before: expired token reused from cache
token := cachedToken
// after: fetch/refresh token before login
token := idp.GetFreshToken()
Defensive patterns

Strategy: try-catch

Validate before calling

// verify token structurally and check expiry before calling Validate
token, _, err := new(jwt.Parser).ParseUnverified(rawToken)
if err != nil { return ErrMalformedToken }
var claims jwt.MapClaims
token.Claims = &claims
if exp, err := claims.GetExpirationTime(); err == nil && exp != nil && exp.Time.Before(time.Now()) {
    return ErrTokenExpired
}

Try / catch

claims, err := validator.Validate(ctx, token, expected)
if err != nil {
    if strings.Contains(err.Error(), "token is expired") {
        return refreshTokenAndRetry()
    }
    return fmt.Errorf("jwt validation failed: %w", err)
}

Prevention

When it happens

Trigger: Login with a JWT whose signature doesn't verify against the configured JWKS/public keys, an expired token, wrong aud claim, or a token signed by an unexpected key.

Common situations: IdP rotated signing keys but cached JWKS is stale; testing a dev token against prod config; clock skew causing expiry; wrong key source (JWKS URL, PEM, or OIDC discovery URL) configured on the auth method.

Related errors


AI-assisted analysis of hashicorp/nomad@482b49bf1a (2026-09-04). Data as JSON: /api/errors/41c2f49f7d0a8cfd. Report an issue: GitHub.