Skip to content

Commit

Permalink
Merge pull request #29357 from nextcloud/fix/concurrent-duplicate-aut…
Browse files Browse the repository at this point in the history
…h-token-updates
  • Loading branch information
skjnldsv authored Oct 22, 2021
2 parents 686f293 + 7dd7256 commit a9fe0fc
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 9 deletions.
39 changes: 39 additions & 0 deletions lib/private/Authentication/Token/PublicKeyTokenMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -190,4 +190,43 @@ public function hasExpiredTokens(string $uid): bool {

return count($data) === 1;
}

/**
* Update the last activity timestamp
*
* In highly concurrent setups it can happen that two parallel processes
* trigger the update at (nearly) the same time. In that special case it's
* not necessary to hit the database with two actual updates. Therefore the
* target last activity is included in the WHERE clause with a few seconds
* of tolerance.
*
* Example:
* - process 1 (P1) reads the token at timestamp 1500
* - process 1 (P2) reads the token at timestamp 1501
* - activity update interval is 100
*
* This means
*
* - P1 will see a last_activity smaller than the current time and update
* the token row
* - If P2 reads after P1 had written, it will see 1600 as last activity
* and the comparison on last_activity won't be truthy. This means no rows
* need to be updated a second time
* - If P2 reads before P1 had written, it will see 1501 as last activity,
* but the comparison on last_activity will still not be truthy and the
* token row is not updated a second time
*
* @param IToken $token
* @param int $now
*/
public function updateActivity(IToken $token, int $now): void {
$qb = $this->db->getQueryBuilder();
$update = $qb->update($this->getTableName())
->set('last_activity', $qb->createNamedParameter($now, IQueryBuilder::PARAM_INT))
->where(
$qb->expr()->eq('id', $qb->createNamedParameter($token->getId(), IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT),
$qb->expr()->lt('last_activity', $qb->createNamedParameter($now - 15, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT)
);
$update->executeStatement();
}
}
3 changes: 1 addition & 2 deletions lib/private/Authentication/Token/PublicKeyTokenProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -221,9 +221,8 @@ public function updateTokenActivity(IToken $token) {
/** @var PublicKeyToken $token */
$now = $this->time->getTime();
if ($token->getLastActivity() < ($now - $activityInterval)) {
// Update token only once per minute
$token->setLastActivity($now);
$this->mapper->update($token);
$this->mapper->updateActivity($token, $now);
}
}

Expand Down
13 changes: 6 additions & 7 deletions tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,10 @@ public function testGenerateToken() {

public function testUpdateToken() {
$tk = new PublicKeyToken();
$tk->setLastActivity($this->time - 200);
$this->mapper->expects($this->once())
->method('update')
->with($tk);
->method('updateActivity')
->with($tk, $this->time);
$tk->setLastActivity($this->time - 200);

$this->tokenProvider->updateTokenActivity($tk);

Expand All @@ -112,16 +112,15 @@ public function testUpdateToken() {

public function testUpdateTokenDebounce() {
$tk = new PublicKeyToken();

$this->config->method('getSystemValueInt')
->willReturnCallback(function ($value, $default) {
return $default;
});

$tk->setLastActivity($this->time - 30);

$this->mapper->expects($this->never())
->method('update')
->with($tk);
->method('updateActivity')
->with($tk, $this->time);

$this->tokenProvider->updateTokenActivity($tk);
}
Expand Down

0 comments on commit a9fe0fc

Please sign in to comment.