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

  1. Log the decrypted challenge (the server logs it) and compare against the expected schema: version, domain, verify_token, verify_token_expiry
  2. Use snake_case keys exactly as the protocol expects
  3. Send the JSON object itself, not a string containing JSON (avoid double encoding)
  4. Update the client SDK to a version matching the server protocol version
  5. 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

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


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)