{"record":{"id":"eeeee2f442236721","repo":"passbolt/passbolt_api","slug":"the-duo-state-should-match-the-authentication-token-state","errorCode":null,"errorMessage":"The Duo state should match the authentication token state.","messagePattern":"The Duo state should match the authentication token state\\.","errorType":"http","errorClass":"UnauthorizedException","httpStatus":401,"severity":"error","filePath":"plugins/PassboltCe/MultiFactorAuthentication/src/Service/Duo/MfaDuoCallbackAuthenticationTokenService.php","lineNumber":119,"sourceCode":"    /**\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)) {\n            throw new InternalErrorException(__('An authentication token state is required.'));\n        }\n        if ($authTokenState !== $duoState) {\n            throw new UnauthorizedException(__('The Duo state should match the authentication token state.'));\n        }\n    }\n}\n","sourceCodeStart":101,"sourceCodeEnd":123,"githubUrl":"https://github.com/passbolt/passbolt_api/blob/31c1bbc10f32808a607fa9bd81891e898779c0bc/plugins/PassboltCe/MultiFactorAuthentication/src/Service/Duo/MfaDuoCallbackAuthenticationTokenService.php#L101-L123","documentation":"Duo's security model relies on the client returning the same 'state' value it was given at flow start (CSRF-style protection). This error is thrown as UnauthorizedException when the state received in the Duo callback does not exactly string-match the state stored in the passbolt authentication token data. It indicates the callback did not originate from the same Duo flow that issued the token, or the state was altered in transit.","triggerScenarios":"The 'state' query parameter returned by Duo differs from the token's stored state: a different browser/tab completed the Duo prompt; the state was tampered with in the URL; two MFA flows were started and the callback from one is paired with the token of the other; the state was URL-encoded/decoded differently.","commonSituations":"User opens the setup page in two tabs and completes Duo in the wrong one; a proxy or redirect rewrites query parameters; tests reuse a fixed state string across tokens; session loss causes a fresh token to be issued but Duo returns the old state.","solutions":["Ensure the same browser session that started the Duo prompt completes it — discard the stale tab and restart the flow","Restart MFA setup to mint a fresh token and new state, then complete Duo in one flow only","Compare (and log at debug level) the callback state vs the stored state to spot encoding differences (e.g. double URL-encoding)","Never construct the callback URL manually — follow the redirect Duo provides so the state is passed through untouched"],"exampleFix":"// before (client-side manual callback)\nheader('Location: /mfa/verify?token=' . $token . '&state=' . urlencode($oldState));\n// after\n// let Duo redirect carry the original state; server compares raw values\n$service->consumeAndVerifyAuthenticationToken($uac, $type, $token, $callbackState);","handlingStrategy":"validation","validationCode":"if (!is_string($callbackState) || $callbackState === '') {\n    throw new \\Cake\\Http\\Exception\\BadRequestException('Missing Duo state in callback.');\n}","typeGuard":null,"tryCatchPattern":"try {\n    $authToken = $service->consumeAndVerifyAuthenticationToken($uac, $type, $token, $state);\n} catch (\\Cake\\Http\\Exception\\UnauthorizedException $e) {\n    // state mismatch: CSRF-like condition, do not proceed\n    throw new \\Cake\\Http\\Exception\\UnauthorizedException('Duo state verification failed.');\n}","preventionTips":["Never modify, re-encode, or pre-validate the state value between issuing it and the callback","Complete the Duo prompt in the same browser session/tab that started it","Restart the flow if multiple setups were opened concurrently","Pass state through redirects untouched (no manual query-string rebuilding)"],"tags":["csrf","state-mismatch","mfa","duo","unauthorized"],"backgroundTag":"state-mismatch","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"}