passbolt/passbolt_api · error · UnauthorizedException

The token should reference an active Duo callback…

Error message

The token should reference an active Duo callback authentication token.

What it means

consumeAuthenticationTokenOrFail delegates to AuthenticationTokenConsumeService::consumeActiveNotExpiredOrFail, which requires an active, non-expired token of the given type owned by the user. Any Throwable from that lookup is rethrown as this UnauthorizedException, so it covers: no such token, token already consumed, token expired, wrong owner, or wrong type. The original exception is preserved as the previous exception for diagnosis.

Solutions

  1. Check the previous exception (getPrevious()) to see whether the token was not found, expired, or already consumed
  2. Restart the MFA setup/login flow to generate a fresh authentication token and retry
  3. If retries are hitting the callback, make the client idempotent — do not replay the same callback token twice
  4. Verify the UAC user id matches the user who initiated the MFA flow (same logged-in session)
  5. Check the token's active/created timestamps in the authentication_tokens table to rule out expiry

Example fix

// before
try { $service->consumeAndVerifyAuthenticationToken($uac, $type, $token, $state); }
catch (UnauthorizedException $e) { /* token may already be consumed */ }
// after
try { $service->consumeAndVerifyAuthenticationToken($uac, $type, $token, $state); }
catch (UnauthorizedException $e) {
    // restart flow: token consumed/expired — issue a new one
    $newToken = (new MfaDuoStartSetupService())->startSetup($uac);
    return $this->redirect($newToken);
}
Defensive patterns

Strategy: try-catch

Try / catch

try {
    $authToken = $service->consumeAndVerifyAuthenticationToken($uac, $type, $token, $state);
} catch (\Cake\Http\Exception\UnauthorizedException $e) {
    $cause = $e->getPrevious(); // not-found vs expired vs already-consumed
    $this->log('Duo token consume failed: ' . ($cause?->getMessage() ?? ''));
    // restart the MFA flow with a fresh token
}

Prevention

When it happens

Trigger: Calling with a token that does not exist in authentication_tokens; a token already consumed by a previous Duo callback (double POST/retry); an expired token (tokens have a TTL); a token belonging to a different user id than the UAC; a valid UUID of the right format but of a mismatched type.

Common situations: Browser retries the Duo callback after success (token already consumed); user took too long completing the Duo prompt and the token expired; session switched users mid-flow; database row was deleted by a cleanup task or failed setup.

Understand the failure class

Related errors


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

Appendix: source

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

     * @param string $tokenType AuthenticationToken's token type
     * @param string $token AuthenticationToken's token
     * @return \App\Model\Entity\AuthenticationToken
     * @throws \Cake\Http\Exception\UnauthorizedException If the token could not be consumed
     */
    private function consumeAuthenticationTokenOrFail(
        UserAccessControl $uac,
        string $tokenType,
        string $token
    ): AuthenticationToken {
        try {
            return (new AuthenticationTokenConsumeService())->consumeActiveNotExpiredOrFail(
                $token,
                $uac->getId(),
                $tokenType
            );
        } catch (Throwable $th) {
            $msg = __('The token should reference an active Duo callback authentication token.');
            throw new UnauthorizedException($msg, null, $th);
        }
    }

    /**
     * Assert the Duo callback authentication token state value.
     *
     * @param \App\Model\Entity\AuthenticationToken $authToken The callback authentication token
     * @param string $duoState The Duo callback state
     * @return void
     * @throws \Cake\Http\Exception\InternalErrorException if the callback authentication token does not have state defined
     * @throws \Cake\Http\Exception\UnauthorizedException if the callback authentication token state value does not match the Duo callback state
     */
    private function assertDuoStateMatchesAuthenticationTokenState(
        AuthenticationToken $authToken,
        string $duoState
    ): void {
        $authTokenState = $authToken->getDataValue('state');
        if (empty($authTokenState)) {

View on GitHub (pinned to 31c1bbc10f)