passbolt/passbolt_api · error · UnauthorizedException
The Duo state should match the authentication token state.
Error message
The Duo state should match the authentication token state.
What it means
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.
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
Example fix
// before (client-side manual callback)
header('Location: /mfa/verify?token=' . $token . '&state=' . urlencode($oldState));
// after
// let Duo redirect carry the original state; server compares raw values
$service->consumeAndVerifyAuthenticationToken($uac, $type, $token, $callbackState); Defensive patterns
Strategy: validation
Validate before calling
if (!is_string($callbackState) || $callbackState === '') {
throw new \Cake\Http\Exception\BadRequestException('Missing Duo state in callback.');
} Try / catch
try {
$authToken = $service->consumeAndVerifyAuthenticationToken($uac, $type, $token, $state);
} catch (\Cake\Http\Exception\UnauthorizedException $e) {
// state mismatch: CSRF-like condition, do not proceed
throw new \Cake\Http\Exception\UnauthorizedException('Duo state verification failed.');
} Prevention
- 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)
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- The token should reference an active Duo callback…
- A Duo state cookie is required.
- A Duo state cookie is required.
- An authentication token state is required.
- Could not create MFA verified cookie.
AI-assisted analysis of passbolt/passbolt_api@31c1bbc10f (2026-09-17).
Data as JSON: /api/errors/eeeee2f442236721.
Report an issue: GitHub.
Appendix: source
Thrown at plugins/PassboltCe/MultiFactorAuthentication/src/Service/Duo/MfaDuoCallbackAuthenticationTokenService.php:119
/**
* 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)) {
throw new InternalErrorException(__('An authentication token state is required.'));
}
if ($authTokenState !== $duoState) {
throw new UnauthorizedException(__('The Duo state should match the authentication token state.'));
}
}
}
View on GitHub (pinned to 31c1bbc10f)