Consumer::normalizeValues() adjusts various fields to consistent values. For timestamps, it does something like this:
$this->emailAuthenticated = wfTimestamp( TS_MW, $this->emailAuthenticated ); $this->registration = wfTimestamp( TS_MW, $this->registration ); $this->stageTimestamp = wfTimestamp( TS_MW, $this->stageTimestamp );
"registration" and "stageTimestamp" cannot be null in the db, but emailAuthenticated can.
The current code is fine in normal usage, where emailAuthenticated always has a valid value. But if it is null, it is normalized to the current timestamp. Because this timestamp is used in MWOauthDAO::getChangeToken(), this introduces a subtle timing issue where change tokens do not match if the current timestamp has incremented by (at least) a second between change token calculations.
To my knowledge, this is not a production issue, as this timestamp is always initialized in code. However, it gave inconsistent and unexpected results in local unit testing.
Suggested fix: use wfTimestampOrNull rather than wfTimestamp to normalize the emailAuthenticated value. This better reflects reality, and also causes Consumer::normalizeValues() to behave consistently, thereby eliminating the timing issue.