{"record":{"id":"ef62109ae0b00444","repo":"passbolt/passbolt_api","slug":"the-challenge-is-invalid-validation-failed","errorCode":null,"errorMessage":"The challenge is invalid. Validation Failed.","messagePattern":"The challenge is invalid\\. Validation Failed\\.","errorType":"exception","errorClass":"Cake\\Http\\Exception\\BadRequestException","httpStatus":400,"severity":"error","filePath":"plugins/PassboltCe/JwtAuthentication/src/Authenticator/GpgJwtAuthenticator.php","lineNumber":347,"sourceCode":"        } 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\n        return $verifyToken;\n    }\n\n    /**\n     * @param mixed $fingerprint fingerprint\n     * @throws \\Cake\\Http\\Exception\\InternalErrorException\n     * @return void\n     */\n    public function assertServerFingerprint(mixed $fingerprint): void\n    {\n        if (!is_string($fingerprint) || !PublicKeyValidationService::isValidFingerprint($fingerprint)) {\n            $msg = __('The config for the server private key fingerprint is not available or incomplete.');\n            throw new InternalErrorException($msg);\n        }\n    }\n","sourceCodeStart":329,"sourceCodeEnd":365,"githubUrl":"https://github.com/passbolt/passbolt_api/blob/31c1bbc10f32808a607fa9bd81891e898779c0bc/plugins/PassboltCe/JwtAuthentication/src/Authenticator/GpgJwtAuthenticator.php#L329-L365","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: token generated once at client startup, reused\nconst token = generateVerifyToken(); // cached forever\n// after: fresh token per login call\nconst token = generateVerifyToken();\nawait client.post('/auth/jwt/login', { user_id, challenge: buildChallenge(token, expiry = now + 120s) });","handlingStrategy":"validation","validationCode":"if (Date.now() > new Date(challenge.verify_token_expiry).getTime()) throw new Error('verify token expired');\nif (!/^[A-F0-9-]{32,}$/.test(challenge.verify_token)) throw new Error('verify token malformed');","typeGuard":"null","tryCatchPattern":"try { await login(challenge); } catch (e) { if (/Validation Failed/.test(e.message)) { challenge = buildFreshChallenge(); await login(challenge); } }","preventionTips":["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"],"tags":["validation","jwt","verify-token","challenge"],"backgroundTag":"schema-validation-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"}