{"record":{"id":"d37c13e8d142c82d","repo":"passbolt/passbolt_api","slug":"unable-to-verify-duo-code-against-duo-service","errorCode":null,"errorMessage":"Unable to verify Duo code against Duo service.","messagePattern":"Unable to verify Duo code against Duo service\\.","errorType":"http","errorClass":"UnauthorizedException","httpStatus":401,"severity":"error","filePath":"plugins/PassboltCe/MultiFactorAuthentication/src/Service/Duo/MfaDuoVerifyDuoCodeService.php","lineNumber":104,"sourceCode":"     * @param string $duoCode The duo code\n     * @param string $operatorUsername The operator username\n     * @return array\n     * @throws \\Cake\\Http\\Exception\\UnauthorizedException If an error occurred while retrieving the Duo authentication details\n     * @throws \\Cake\\Http\\Exception\\InternalErrorException If Duo doesn't return the authentication details as an array.\n     */\n    private function requestDuoAuthenticationDetails(string $duoCode, string $operatorUsername): array\n    {\n        try {\n            /**\n             * @var array $duoAuthenticationData\n             * @psalm-suppress UndefinedDocblockClass\n             */\n            $duoAuthenticationData = $this->duoClient->exchangeAuthorizationCodeFor2FAResult(\n                $duoCode,\n                $operatorUsername\n            );\n        } catch (DuoException $e) {\n            throw new UnauthorizedException(__('Unable to verify Duo code against Duo service.'), null, $e);\n        }\n\n        return $duoAuthenticationData;\n    }\n\n    /**\n     * Assert that the origin response endpoint is a known Duo response endpoint.\n     *\n     * @see https://duo.com/docs/oauthapi\n     * @param string $duoAuthenticationDetailIss Duo endpoint from callback\n     * @return void\n     * @throws \\Cake\\Http\\Exception\\UnauthorizedException If the duo authentication origin endpoint (iss) does not match the duo hostname\n     * defined in the organization settings.\n     */\n    private function assertDuoAuthenticationEndpoint(string $duoAuthenticationDetailIss): void\n    {\n        $duoApiHostname = MfaOrgSettings::get()->getDuoOrgSettings()->getDuoApiHostname();\n        $expectedIss = \"https://$duoApiHostname/oauth/v1/token\";","sourceCodeStart":86,"sourceCodeEnd":122,"githubUrl":"https://github.com/passbolt/passbolt_api/blob/31c1bbc10f32808a607fa9bd81891e898779c0bc/plugins/PassboltCe/MultiFactorAuthentication/src/Service/Duo/MfaDuoVerifyDuoCodeService.php#L86-L122","documentation":"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.","triggerScenarios":"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().","commonSituations":"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.","solutions":["Have the user restart the MFA flow to get a fresh duo code.","Verify the server can reach the Duo API hostname (curl the https://<host>/oauth/v1/token endpoint).","Check server clock synchronization (NTP) — clock skew breaks OAuth token validation.","Confirm Duo client id/secret in org settings match the Duo application protecting the hostname.","Inspect the wrapped DuoException for the precise Duo API error."],"exampleFix":"// before\ndef verify(...): doExchange(duoCode)\n// after: validate freshness before calling\nif ($this->isExpired($mfaVerificationToken)) {\n    throw new UnauthorizedException('MFA verification token expired, restart the flow.');\n}\ndef verify(...): doExchange(duoCode)","handlingStrategy":"try-catch","validationCode":"if ($this->tokenService->isExpired($mfaVerificationToken)) {\n    throw new UnauthorizedException('MFA token expired; restart the verification flow.');\n}","typeGuard":null,"tryCatchPattern":"try {\n    $details = $service->verify($uac, $mfaToken, $duoCode);\n} catch (UnauthorizedException $e) {\n    // prompt user to redo Duo authentication\n    return $this->restartMfaFlow();\n}","preventionTips":["Keep server clock synchronized via NTP.","Ensure outbound HTTPS to the Duo API hostname is allowed.","Never cache or replay duo codes; always start a fresh flow.","Verify Duo client credentials periodically via health check."],"tags":["mfa","duo","authentication","oauth"],"backgroundTag":"oauth-token-exchange-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"}