passbolt/passbolt_api · error · BadRequestException

Unable to verify Duo authentication.

Error message

Unable to verify Duo authentication.

What it means

After consuming the callback token and checking the state, enable() delegates the user's Duo code verification to MfaDuoVerifyDuoCodeService, which exchanges the one-time Duo code with the Duo Universal SDK. Any Throwable in that verification (expired/already-used code, invalid code, network error to Duo API, client misconfiguration) is wrapped in this BadRequestException with the original error preserved as previous.

Solutions

  1. Restart the whole Duo flow — Duo codes are single-use and short-lived, so get a fresh prompt and code
  2. Check getPrevious() to distinguish an invalid/expired code from a Duo API connectivity error
  3. Verify the Duo org settings (client id, secret, api hostname) match the integration the user authenticated against
  4. Ensure the server can reach https://<api-hostname> (outbound HTTPS, DNS, proxy) e.g. curl the Duo endpoint
  5. Make the client submit the callback exactly once to avoid code replay

Example fix

// before
try {
    $service->enable($uac, $dto, $token);
} catch (BadRequestException $e) { /* swallowed */ }
// after
try {
    $service->enable($uac, $dto, $token);
} catch (BadRequestException $e) {
    $this->log('Duo verify failed: ' . $e->getPrevious()?->getMessage());
    return $this->redirect('/mfa/setup/duo'); // restart flow for a fresh code
}
Defensive patterns

Strategy: try-catch

Try / catch

try {
    $service->enable($uac, $dto, $token);
} catch (\Cake\Http\Exception\BadRequestException $e) {
    $this->log('Duo code verification failed: ' . $e->getPrevious()?->getMessage());
    // redirect to restart the Duo prompt for a fresh one-time code
}

Prevention

When it happens

Trigger: Calling enable() with a duoCode that Duo rejects: code already redeemed by a prior request, code expired (Duo codes are short-lived), wrong Duo client credentials/api hostname, or unreachable Duo API from the server.

Common situations: Browser double-submits the callback and the second request reuses the consumed code; server clock skew or slow flow exceeding Duo's code TTL; Duo integration hostname/client id mismatch between org settings and the client used to start the prompt; firewall blocks outbound HTTPS to Duo.

Understand the failure class

Related errors


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

Appendix: source

Thrown at plugins/PassboltCe/MultiFactorAuthentication/src/Service/Duo/MfaDuoEnableService.php:97

        MfaDuoCallbackDto $duoCallbackDto,
        string $token
    ): AuthenticationToken {
        if (!Validation::uuid($token)) {
            throw new InvalidArgumentException('The authentication token should be a valid UUID.');
        }
        $authenticationTokenType = AuthenticationToken::TYPE_MFA_SETUP;
        $authenticationToken = (new MfaDuoCallbackAuthenticationTokenService())
            ->consumeAndVerifyAuthenticationToken(
                $uac,
                $authenticationTokenType,
                $token,
                $duoCallbackDto->state
            );
        try {
            (new MfaDuoVerifyDuoCodeService($authenticationTokenType, $this->duoClient))
                ->verify($uac, $duoCallbackDto->duoCode);
        } catch (Throwable $th) {
            throw new BadRequestException(__('Unable to verify Duo authentication.'), null, $th);
        }
        $this->enableProvider($uac);

        return $authenticationToken;
    }

    /**
     * Enable the provider for the operator.
     *
     * @param \App\Utility\UserAccessControl $uac The user access control
     * @return void
     * @throw InternalErrorException If it could not enable the Duo MFA provider.
     */
    private function enableProvider(UserAccessControl $uac): void
    {
        try {
            MfaAccountSettings::enableProvider($uac, MfaSettings::PROVIDER_DUO);
        } catch (Throwable $th) {

View on GitHub (pinned to 31c1bbc10f)