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
- 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
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
- 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
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
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- The duo authentication origin endpoint does not match the…
- The duo authentication subscriber does not match the…
- The Duo state should match the authentication token state.
- 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/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)