{"record":{"id":"7a06db3f4105d86d","repo":"w7corp/easywechat","slug":"invalid-request-signature","errorCode":null,"errorMessage":"Invalid request signature.","messagePattern":"Invalid request signature\\.","errorType":"exception","errorClass":"BadRequestException","httpStatus":null,"severity":"error","filePath":"src/Kernel/Traits/DecryptMessage.php","lineNumber":106,"sourceCode":"\n        return is_scalar($value) ? strval($value) : '';\n    }\n\n    /**\n     * @param  array<int, int|string>  $params\n     *\n     * @throws BadRequestException\n     */\n    protected function assertSignatureMatches(array $params, string $signature): void\n    {\n        if (empty($signature)) {\n            throw new BadRequestException('Request signature must not be empty.');\n        }\n\n        sort($params, SORT_STRING);\n\n        if (! hash_equals(sha1(implode($params)), $signature)) {\n            throw new BadRequestException('Invalid request signature.');\n        }\n    }\n}\n","sourceCodeStart":88,"sourceCodeEnd":110,"githubUrl":"https://github.com/w7corp/easywechat/blob/f0cf0a8b8361417ed683b8246d0ecbaf0aafcaa8/src/Kernel/Traits/DecryptMessage.php#L88-L110","documentation":"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.","triggerScenarios":"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.","commonSituations":"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().","solutions":["Copy the Token field character-for-character from the console into config 'token' (watch leading/trailing spaces).","Give the SDK the raw, undecoded request body — never a re-serialized copy.","Confirm the correct param set: encrypted → msg_signature over token+timestamp+nonce+ciphertext; echostr → signature over token+timestamp+nonce.","Debug once by logging sort($params, SORT_STRING), sha1(implode($params)) and the received signature side by side."],"exampleFix":"// before: stale env token\n$app = new Application(['app_id' => $id, 'secret' => $secret, 'token' => env('WECHAT_TOKEN_OLD')]);\n// after: single trimmed source of truth identical to the console value\n$app = new Application(['app_id' => $id, 'secret' => $secret, 'token' => trim((string) env('WECHAT_TOKEN'))]);","handlingStrategy":"try-catch","validationCode":"$p = [$token, (string) $ts, (string) $nonce, $cipher];\nsort($p, SORT_STRING);\nif (!hash_equals(sha1(implode($p)), $sig)) { return new \\Nyholm\\Psr7\\Response(403); } // mirrors assertSignatureMatches without throwing","typeGuard":null,"tryCatchPattern":"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; }","preventionTips":["Store the token once per app/environment and trim it","Re-verify the callback URL in the console after any token change","Never mutate the raw body or query before serve()"],"tags":["php","callback","sha1-signature","token","wechat"],"backgroundTag":"signature-verification-failed","analyzedSha":"f0cf0a8b8361417ed683b8246d0ecbaf0aafcaa8","analyzedAt":"2026-08-21T05:29:19.565Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}