w7corp/easywechat · error · BadRequestException

Invalid request signature.

Error message

Invalid request signature.

What it means

The signature check sorts [token, timestamp, nonce, ciphertext] (encrypted messages) or [token, timestamp, nonce] (plain/echostr requests) as strings and compares sha1 of their concatenation to the received signature with hash_equals. Any mismatch throws this BadRequestException, and in practice it almost always means the token in the app config differs from the Token configured in the WeChat/Work console, or the ciphertext used in the hash is not the exact Encrypt string from the original body.

Source

Thrown at src/Kernel/Traits/DecryptMessage.php:106

        return is_scalar($value) ? strval($value) : '';
    }

    /**
     * @param  array<int, int|string>  $params
     *
     * @throws BadRequestException
     */
    protected function assertSignatureMatches(array $params, string $signature): void
    {
        if (empty($signature)) {
            throw new BadRequestException('Request signature must not be empty.');
        }

        sort($params, SORT_STRING);

        if (! hash_equals(sha1(implode($params)), $signature)) {
            throw new BadRequestException('Invalid request signature.');
        }
    }
}

View on GitHub (pinned to f0cf0a8b83)

Solutions

  1. Copy the Token field character-for-character from the console into config 'token' (watch leading/trailing spaces).
  2. Give the SDK the raw, undecoded request body — never a re-serialized copy.
  3. Confirm the correct param set: encrypted → msg_signature over token+timestamp+nonce+ciphertext; echostr → signature over token+timestamp+nonce.
  4. Debug once by logging sort($params, SORT_STRING), sha1(implode($params)) and the received signature side by side.

Example fix

// before: stale env token
$app = new Application(['app_id' => $id, 'secret' => $secret, 'token' => env('WECHAT_TOKEN_OLD')]);
// after: single trimmed source of truth identical to the console value
$app = new Application(['app_id' => $id, 'secret' => $secret, 'token' => trim((string) env('WECHAT_TOKEN'))]);
Defensive patterns

Strategy: try-catch

Validate before calling

$p = [$token, (string) $ts, (string) $nonce, $cipher];
sort($p, SORT_STRING);
if (!hash_equals(sha1(implode($p)), $sig)) { return new \Nyholm\Psr7\Response(403); } // mirrors assertSignatureMatches without throwing

Try / catch

try { return $server->serve(); } catch (\EasyWeChat\Kernel\Exceptions\BadRequestException $e) { if (str_contains($e->getMessage(), 'Invalid request signature')) { \Log::alert('signature mismatch', ['token_len' => strlen($token), 'query' => $request->getUri()->getQuery()]); return new \Nyholm\Psr7\Response(403); } throw $e; }

Prevention

When it happens

Trigger: Config 'token' typo/whitespace/stale value versus the console Token; the body was re-encoded (CDATA unwrapped, HTML entities decoded, charset converted) so the ciphertext hashed differs byte-wise from the one WeChat signed; validating an encrypted message with the plain-parameter algorithm or vice versa; timestamp or nonce altered in transit.

Common situations: Env-specific tokens (dev vs prod) crossed; multiple apps sharing one callback with per-app tokens wired to the wrong app; console token regenerated during re-verification; middleware mutating the raw XML before serve().

Related errors


AI-assisted analysis of w7corp/easywechat@f0cf0a8b83 (2026-08-21). Data as JSON: /api/errors/7a06db3f4105d86d. Report an issue: GitHub.