{"record":{"id":"c9bc88df88ed5415","repo":"passbolt/passbolt_api","slug":"the-challenge-is-invalid-deserialization-failed","errorCode":null,"errorMessage":"The challenge is invalid. Deserialization failed.","messagePattern":"The challenge is invalid\\. Deserialization failed\\.","errorType":"exception","errorClass":"Cake\\Http\\Exception\\BadRequestException","httpStatus":400,"severity":"error","filePath":"plugins/PassboltCe/JwtAuthentication/src/Authenticator/GpgJwtAuthenticator.php","lineNumber":331,"sourceCode":"            Log::error($exception->getMessage());\n            throw new InvalidUserSignatureException(__('The user signature could not be verified.'));\n        } catch (Exception $exception) {\n            Log::error($exception->getMessage());\n            throw new BadRequestException(__('The challenge cannot be decrypted.'));\n        }\n\n        // Deserialize JSON\n        try {\n            $jsonChallenge = json_decode($clearTextChallenge, true, 2, JSON_THROW_ON_ERROR);\n            [\n                'version' => $version,\n                'domain' => $domain,\n                'verify_token' => $verifyToken,\n                'verify_token_expiry' => $verifyTokenExpiry,\n            ] = $jsonChallenge;\n        } catch (Exception $exception) {\n            Log::error($exception->getMessage() . \"\\n\" . $clearTextChallenge);\n            throw new BadRequestException(__('The challenge is invalid. Deserialization failed.'));\n        }\n\n        // Challenge sanity check\n\n        // If domain is not known, let the exception be thrown. It will send email alerts.\n        $this->assertDomain($domain);\n        try {\n            $this->assertVersion($version);\n            (new VerifyTokenValidationService())->validateToken(\n                $verifyTokenExpiry,\n                $verifyToken,\n                $this->request->getData('user_id')\n            );\n        } catch (Exception $exception) {\n            Log::error($exception->getMessage() . \"\\n\" . $clearTextChallenge);\n            throw new BadRequestException(__('The challenge is invalid. Validation Failed.'));\n        }\n","sourceCodeStart":313,"sourceCodeEnd":349,"githubUrl":"https://github.com/passbolt/passbolt_api/blob/31c1bbc10f32808a607fa9bd81891e898779c0bc/plugins/PassboltCe/JwtAuthentication/src/Authenticator/GpgJwtAuthenticator.php#L313-L349","documentation":"After successful decryption the server json_decodes the challenge and destructures version, domain, verify_token and verify_token_expiry. This error is thrown when the plaintext is not valid JSON, exceeds nesting depth 2, or is missing one of the required keys, so the destructuring fails.","triggerScenarios":"POST /auth/jwt/login with a decrypted challenge whose plaintext is not the expected JSON object: client sent raw string/token instead of JSON, JSON.stringify of wrong structure, payload nested deeper than 2 levels, or missing keys like verify_token.","commonSituations":"Custom API clients hand-crafting the challenge and forgetting a field; SDK version producing an older/newer challenge schema; double-encoding the JSON so json_decode yields a string; typo'd key names (e.g. verifyToken vs verify_token).","solutions":["Log the decrypted challenge (the server logs it) and compare against the expected schema: version, domain, verify_token, verify_token_expiry","Use snake_case keys exactly as the protocol expects","Send the JSON object itself, not a string containing JSON (avoid double encoding)","Update the client SDK to a version matching the server protocol version","Validate the challenge payload client-side before encrypting"],"exampleFix":"// before\n$challenge = json_encode([\"version\" => \"v1\", \"domain\" => $d, \"verifyToken\" => $t, \"expiry\" => $e]);\n// after\n$challenge = json_encode([\"version\" => \"v1\", \"domain\" => $d, \"verify_token\" => $t, \"verify_token_expiry\" => $e]);","handlingStrategy":"validation","validationCode":"const required = ['version','domain','verify_token','verify_token_expiry'];\nif (required.some(k => !(k in challengeObj))) throw new Error('challenge missing keys: ' + required);","typeGuard":"function isChallenge(c) { return typeof c === 'object' && c !== null && ['version','domain','verify_token','verify_token_expiry'].every(k => typeof k in c ? c[k] !== undefined : false); }","tryCatchPattern":"try { await login(challenge); } catch (e) { if (/Deserialization failed/.test(e.message)) { console.error('challenge payload:', challengeJson); } }","preventionTips":["Use snake_case keys exactly as the protocol defines","JSON-encode exactly once; do not stringify an already-string payload","Keep nesting depth ≤ 2","Validate the payload against the challenge schema in tests before shipping the client"],"tags":["json","challenge","jwt","deserialization"],"backgroundTag":"json-unmarshal-failed","analyzedSha":"31c1bbc10f32808a607fa9bd81891e898779c0bc","analyzedAt":"2026-09-17T00:04:38.960Z","contentChangedAt":"2026-09-17T00:04:38.960Z","schemaVersion":2},"datasetVersion":"2026-09-21T04:17:39.646Z"}