passbolt/passbolt_api · error · UnauthorizedException

Unable to verify Duo code against Duo service.

Error message

Unable to verify Duo code against Duo service.

What it means

Thrown when exchanging the Duo OAuth authorization code for a 2FA result fails with a DuoException during requestDuoAuthenticationDetails(). It wraps the Duo SDK failure in an UnauthorizedException, meaning passbolt could not confirm the MFA code with Duo's API.

Solutions

  1. Have the user restart the MFA flow to get a fresh duo code.
  2. Verify the server can reach the Duo API hostname (curl the https://<host>/oauth/v1/token endpoint).
  3. Check server clock synchronization (NTP) — clock skew breaks OAuth token validation.
  4. Confirm Duo client id/secret in org settings match the Duo application protecting the hostname.
  5. Inspect the wrapped DuoException for the precise Duo API error.

Example fix

// before
def verify(...): doExchange(duoCode)
// after: validate freshness before calling
if ($this->isExpired($mfaVerificationToken)) {
    throw new UnauthorizedException('MFA verification token expired, restart the flow.');
}
def verify(...): doExchange(duoCode)
Defensive patterns

Strategy: try-catch

Validate before calling

if ($this->tokenService->isExpired($mfaVerificationToken)) {
    throw new UnauthorizedException('MFA token expired; restart the verification flow.');
}

Try / catch

try {
    $details = $service->verify($uac, $mfaToken, $duoCode);
} catch (UnauthorizedException $e) {
    // prompt user to redo Duo authentication
    return $this->restartMfaFlow();
}

Prevention

When it happens

Trigger: POSTing an mfa_token/duo_code pair to the MFA verify endpoint where the duo code is expired, already used, invalid, or Duo's API is unreachable/returns an error during exchangeAuthorizationCodeFor2FAResult().

Common situations: User took too long to complete Duo authentication (code expired); replayed or forged duo_code; wrong system clock on the server; network/firewall blocking outbound HTTPS to sso-*.sso.duosecurity.com; incorrect integration secret.

Related errors


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

Appendix: source

Thrown at plugins/PassboltCe/MultiFactorAuthentication/src/Service/Duo/MfaDuoVerifyDuoCodeService.php:104

     * @param string $duoCode The duo code
     * @param string $operatorUsername The operator username
     * @return array
     * @throws \Cake\Http\Exception\UnauthorizedException If an error occurred while retrieving the Duo authentication details
     * @throws \Cake\Http\Exception\InternalErrorException If Duo doesn't return the authentication details as an array.
     */
    private function requestDuoAuthenticationDetails(string $duoCode, string $operatorUsername): array
    {
        try {
            /**
             * @var array $duoAuthenticationData
             * @psalm-suppress UndefinedDocblockClass
             */
            $duoAuthenticationData = $this->duoClient->exchangeAuthorizationCodeFor2FAResult(
                $duoCode,
                $operatorUsername
            );
        } catch (DuoException $e) {
            throw new UnauthorizedException(__('Unable to verify Duo code against Duo service.'), null, $e);
        }

        return $duoAuthenticationData;
    }

    /**
     * Assert that the origin response endpoint is a known Duo response endpoint.
     *
     * @see https://duo.com/docs/oauthapi
     * @param string $duoAuthenticationDetailIss Duo endpoint from callback
     * @return void
     * @throws \Cake\Http\Exception\UnauthorizedException If the duo authentication origin endpoint (iss) does not match the duo hostname
     * defined in the organization settings.
     */
    private function assertDuoAuthenticationEndpoint(string $duoAuthenticationDetailIss): void
    {
        $duoApiHostname = MfaOrgSettings::get()->getDuoOrgSettings()->getDuoApiHostname();
        $expectedIss = "https://$duoApiHostname/oauth/v1/token";

View on GitHub (pinned to 31c1bbc10f)