passbolt/passbolt_api · error · Cake\Http\Exception\BadRequestException
The challenge is invalid. Deserialization failed.
Error message
The challenge is invalid. Deserialization failed.
What it means
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.
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
Example fix
// before $challenge = json_encode(["version" => "v1", "domain" => $d, "verifyToken" => $t, "expiry" => $e]); // after $challenge = json_encode(["version" => "v1", "domain" => $d, "verify_token" => $t, "verify_token_expiry" => $e]);
Defensive patterns
Strategy: validation
Validate before calling
const required = ['version','domain','verify_token','verify_token_expiry'];
if (required.some(k => !(k in challengeObj))) throw new Error('challenge missing keys: ' + required); Type guard
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); } Try / catch
try { await login(challenge); } catch (e) { if (/Deserialization failed/.test(e.message)) { console.error('challenge payload:', challengeJson); } } Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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).
Understand the failure class
Background: "failed to unmarshal" / json.Unmarshal errors: why parsing a response into a Go struct fails and how to fix it — this error's family across 23 libraries.
Related errors
- The challenge cannot be decrypted.
- The challenge is invalid. Validation Failed.
- The domain is invalid.
- The version is invalid.
- Ajax/Json request not supported.
AI-assisted analysis of passbolt/passbolt_api@31c1bbc10f (2026-09-17).
Data as JSON: /api/errors/c9bc88df88ed5415.
Report an issue: GitHub.
Appendix: source
Thrown at plugins/PassboltCe/JwtAuthentication/src/Authenticator/GpgJwtAuthenticator.php:331
Log::error($exception->getMessage());
throw new InvalidUserSignatureException(__('The user signature could not be verified.'));
} catch (Exception $exception) {
Log::error($exception->getMessage());
throw new BadRequestException(__('The challenge cannot be decrypted.'));
}
// Deserialize JSON
try {
$jsonChallenge = json_decode($clearTextChallenge, true, 2, JSON_THROW_ON_ERROR);
[
'version' => $version,
'domain' => $domain,
'verify_token' => $verifyToken,
'verify_token_expiry' => $verifyTokenExpiry,
] = $jsonChallenge;
} catch (Exception $exception) {
Log::error($exception->getMessage() . "\n" . $clearTextChallenge);
throw new BadRequestException(__('The challenge is invalid. Deserialization failed.'));
}
// Challenge sanity check
// If domain is not known, let the exception be thrown. It will send email alerts.
$this->assertDomain($domain);
try {
$this->assertVersion($version);
(new VerifyTokenValidationService())->validateToken(
$verifyTokenExpiry,
$verifyToken,
$this->request->getData('user_id')
);
} catch (Exception $exception) {
Log::error($exception->getMessage() . "\n" . $clearTextChallenge);
throw new BadRequestException(__('The challenge is invalid. Validation Failed.'));
}
View on GitHub (pinned to 31c1bbc10f)