{"record":{"id":"5ee3aa74c7ea4615","repo":"passbolt/passbolt_api","slug":"the-token-should-reference-an-active-duo-callback","errorCode":null,"errorMessage":"The token should reference an active Duo callback authentication token.","messagePattern":"The token should reference an active Duo callback authentication token\\.","errorType":"http","errorClass":"UnauthorizedException","httpStatus":401,"severity":"error","filePath":"plugins/PassboltCe/MultiFactorAuthentication/src/Service/Duo/MfaDuoCallbackAuthenticationTokenService.php","lineNumber":97,"sourceCode":"     * @param string $tokenType AuthenticationToken's token type\n     * @param string $token AuthenticationToken's token\n     * @return \\App\\Model\\Entity\\AuthenticationToken\n     * @throws \\Cake\\Http\\Exception\\UnauthorizedException If the token could not be consumed\n     */\n    private function consumeAuthenticationTokenOrFail(\n        UserAccessControl $uac,\n        string $tokenType,\n        string $token\n    ): AuthenticationToken {\n        try {\n            return (new AuthenticationTokenConsumeService())->consumeActiveNotExpiredOrFail(\n                $token,\n                $uac->getId(),\n                $tokenType\n            );\n        } catch (Throwable $th) {\n            $msg = __('The token should reference an active Duo callback authentication token.');\n            throw new UnauthorizedException($msg, null, $th);\n        }\n    }\n\n    /**\n     * Assert the Duo callback authentication token state value.\n     *\n     * @param \\App\\Model\\Entity\\AuthenticationToken $authToken The callback authentication token\n     * @param string $duoState The Duo callback state\n     * @return void\n     * @throws \\Cake\\Http\\Exception\\InternalErrorException if the callback authentication token does not have state defined\n     * @throws \\Cake\\Http\\Exception\\UnauthorizedException if the callback authentication token state value does not match the Duo callback state\n     */\n    private function assertDuoStateMatchesAuthenticationTokenState(\n        AuthenticationToken $authToken,\n        string $duoState\n    ): void {\n        $authTokenState = $authToken->getDataValue('state');\n        if (empty($authTokenState)) {","sourceCodeStart":79,"sourceCodeEnd":115,"githubUrl":"https://github.com/passbolt/passbolt_api/blob/31c1bbc10f32808a607fa9bd81891e898779c0bc/plugins/PassboltCe/MultiFactorAuthentication/src/Service/Duo/MfaDuoCallbackAuthenticationTokenService.php#L79-L115","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Check the previous exception (getPrevious()) to see whether the token was not found, expired, or already consumed","Restart the MFA setup/login flow to generate a fresh authentication token and retry","If retries are hitting the callback, make the client idempotent — do not replay the same callback token twice","Verify the UAC user id matches the user who initiated the MFA flow (same logged-in session)","Check the token's active/created timestamps in the authentication_tokens table to rule out expiry"],"exampleFix":"// before\ntry { $service->consumeAndVerifyAuthenticationToken($uac, $type, $token, $state); }\ncatch (UnauthorizedException $e) { /* token may already be consumed */ }\n// after\ntry { $service->consumeAndVerifyAuthenticationToken($uac, $type, $token, $state); }\ncatch (UnauthorizedException $e) {\n    // restart flow: token consumed/expired — issue a new one\n    $newToken = (new MfaDuoStartSetupService())->startSetup($uac);\n    return $this->redirect($newToken);\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n    $authToken = $service->consumeAndVerifyAuthenticationToken($uac, $type, $token, $state);\n} catch (\\Cake\\Http\\Exception\\UnauthorizedException $e) {\n    $cause = $e->getPrevious(); // not-found vs expired vs already-consumed\n    $this->log('Duo token consume failed: ' . ($cause?->getMessage() ?? ''));\n    // restart the MFA flow with a fresh token\n}","preventionTips":["Make callback handlers idempotent — never replay the same token on retry","Keep MFA setup flows short so tokens do not expire mid-flow","Restart the flow (issue a new token) rather than reusing tokens after failures","Confirm the same authenticated user session owns the token"],"tags":["authentication","token-expired","mfa","duo","unauthorized"],"backgroundTag":"authentication-required","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"}