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
- Generate a fresh verify token immediately before each login attempt (tokens are short-lived)
- Ensure client and server clocks are synchronized (NTP)
- Use the same user_id in the request body as the one the token was generated for
- Check the token format matches VerifyTokenValidationService expectations (UUID-style random token)
- 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
- Generate a fresh token and expiry immediately before each login call
- Synchronize clocks with NTP on client and server
- Use the same user_id for token generation and the request body
- Never reuse or cache challenges across attempts
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
- Invalid verify token expiry.
- Attempt to access an expired verify token.
- Invalid user ID format.
- Invalid verify token format.
- The challenge cannot be decrypted.
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)