passbolt/passbolt_api · error · Cake\Http\Exception\BadRequestException

The challenge is invalid. Validation Failed.

Error message

The challenge is invalid. Validation Failed.

What it means

After domain and version checks, verifyChallenge runs VerifyTokenValidationService::validateToken on verify_token_expiry, verify_token and the request's user_id. Any exception from that service (expired token, invalid token format, mismatched token/user) is wrapped into this BadRequestException.

Solutions

  1. Generate a fresh verify token immediately before each login attempt (tokens are short-lived)
  2. Ensure client and server clocks are synchronized (NTP)
  3. Use the same user_id in the request body as the one the token was generated for
  4. Check the token format matches VerifyTokenValidationService expectations (UUID-style random token)
  5. Read the underlying exception message in the server logs, which includes the specific validation failure

Example fix

// before: token generated once at client startup, reused
const token = generateVerifyToken(); // cached forever
// after: fresh token per login call
const token = generateVerifyToken();
await client.post('/auth/jwt/login', { user_id, challenge: buildChallenge(token, expiry = now + 120s) });
Defensive patterns

Strategy: validation

Validate before calling

if (Date.now() > new Date(challenge.verify_token_expiry).getTime()) throw new Error('verify token expired');
if (!/^[A-F0-9-]{32,}$/.test(challenge.verify_token)) throw new Error('verify token malformed');

Type guard

null

Try / catch

try { await login(challenge); } catch (e) { if (/Validation Failed/.test(e.message)) { challenge = buildFreshChallenge(); await login(challenge); } }

Prevention

When it happens

Trigger: POST /auth/jwt/login where the decrypted challenge contains an expired verify_token_expiry, a malformed verify_token (not matching expected format/length), or a token that does not correspond to the user_id sent in the request body.

Common situations: Client clock skew making a fresh token appear expired; reusing a challenge from a previous login attempt after the token expired; client generating a token with wrong length/charset; user_id in form data differing from the one used when generating the token.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of passbolt/passbolt_api@31c1bbc10f (2026-09-17). Data as JSON: /api/errors/ef62109ae0b00444. Report an issue: GitHub.

Appendix: source

Thrown at plugins/PassboltCe/JwtAuthentication/src/Authenticator/GpgJwtAuthenticator.php:347

        } 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.'));
        }

        return $verifyToken;
    }

    /**
     * @param mixed $fingerprint fingerprint
     * @throws \Cake\Http\Exception\InternalErrorException
     * @return void
     */
    public function assertServerFingerprint(mixed $fingerprint): void
    {
        if (!is_string($fingerprint) || !PublicKeyValidationService::isValidFingerprint($fingerprint)) {
            $msg = __('The config for the server private key fingerprint is not available or incomplete.');
            throw new InternalErrorException($msg);
        }
    }

View on GitHub (pinned to 31c1bbc10f)