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
- 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.
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
- 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.
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
- The duo authentication origin endpoint does not match the…
- The duo authentication subscriber does not match the…
- The token should reference an active Duo callback…
- Unable to authenticate to Duo.
- Unable to authenticate to Duo.
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)