mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-24 04:16:20 +00:00
02eafb473b
freshAccessToken() serialises refreshes per connection, and its comment says why: both providers rotate the refresh token as they hand out an access token, so the token is good for exactly one use, and "a worker racing the nightly refresh command means the slower one spends a token the faster one has already replaced. The provider answers that with invalid_grant, which is the same thing it says about a genuinely revoked grant: last_error gets written, the settings page turns red, and every admin is told to go and re-consent a connection that was never broken." The nightly refresh command called refresh() directly, outside that lock. It was the racer the comment names, not a party to the arrangement it describes. It now goes through refreshSerially(), which takes the same lock -- named once, in one place, for both callers -- re-reads the row inside it, and refreshes. Unlike freshAccessToken() it refreshes a token that is still usable, which is the point of the daily run: a delegated refresh token dies of disuse and this keeps the window sliding. The lock is taken rather than waited for, unlike the send path. Nobody is standing at a screen for a scheduled job, and a held lock means somebody is refreshing this very connection right now -- which slides the window and establishes its health just as well as doing it again would. One test: with the lock held, the command sends no token request and leaves the connection untouched. Without the fix it spends the refresh token the holder is already spending.
57 lines
2.0 KiB
PHP
57 lines
2.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Modules\Platform\Mail;
|
|
|
|
/**
|
|
* One OAuth mail provider's token machinery: building the consent URL,
|
|
* turning the returned code into tokens, and keeping those tokens fresh.
|
|
*
|
|
* Deliberately not Socialite: a mail connection needs raw tokens with a
|
|
* send scope, not a user identity, and Socialite's user() call would
|
|
* drag in a userinfo permission (User.Read on Graph) that sending mail
|
|
* does not need. Implementations write their results straight onto the
|
|
* MailOAuthConnection row and save it.
|
|
*/
|
|
interface MailOAuthBroker
|
|
{
|
|
/** The provider consent URL the admin's browser is sent to. */
|
|
public function authorizeUrl(MailOAuthConnection $connection, string $state, string $redirectUri): string;
|
|
|
|
/**
|
|
* Exchange the callback's authorization code for tokens and record
|
|
* them, along with the connected mailbox's address, on the connection.
|
|
*
|
|
* @throws MailOAuthException
|
|
*/
|
|
public function exchange(MailOAuthConnection $connection, string $code, string $redirectUri): void;
|
|
|
|
/**
|
|
* Refresh the access token (rotating the refresh token when the
|
|
* provider hands back a new one) and record the outcome — including
|
|
* `last_error` on failure, so the settings page and the scheduled
|
|
* health check read one source of truth.
|
|
*
|
|
* @throws MailOAuthException
|
|
*/
|
|
public function refresh(MailOAuthConnection $connection): void;
|
|
|
|
/**
|
|
* A refresh that is not racing a send: the scheduled health check's
|
|
* way in, serialised against freshAccessToken() on the same
|
|
* connection.
|
|
*
|
|
* @throws MailOAuthException
|
|
*/
|
|
public function refreshSerially(MailOAuthConnection $connection): void;
|
|
|
|
/**
|
|
* An access token currently valid for at least a small safety margin,
|
|
* refreshing first when needed — what transports call at send time.
|
|
*
|
|
* @throws MailOAuthException
|
|
*/
|
|
public function freshAccessToken(MailOAuthConnection $connection): string;
|
|
}
|