Race condition refresh token OAuth trong Laravel: xử lý invalid_grant với QuickBooks, Xero, Salesforce, Microsoft và Google

Race condition refresh token: nhiều worker Laravel cùng refresh gây lỗi invalid_grant. Test thật với QuickBooks, Xero, Salesforce và cách xử lý.

Loại
Engineering
Ngày đăng

Quang Dang (github.com/dakiquang), tech lead thiên về sản phẩm, hơn 15 năm làm phần mềm, trong đó 10 năm với SaaS · thử nghiệm ngày 7 tháng 11 năm 2026 · cập nhật ngày 8 tháng 11 năm 2026 · English version

Queue Laravel của bạn có tám worker (queue:work hoặc Horizon), cùng lúc đó một scheduled command cũng đang chạy. Tất cả cùng nhận việc cho một tài khoản đã kết nối, đúng lúc access token vừa hết hạn. Mỗi tiến trình đều phát hiện ra điều đó và gọi token endpoint với cùng một refresh token. Đó là race condition khi refresh token OAuth, thủ phạm đứng sau rất nhiều lỗi invalid_grant tưởng như "ngẫu nhiên".

Chuyện gì xảy ra tiếp theo phụ thuộc hoàn toàn vào provider. Có provider chẳng báo lỗi gì. Có provider khiến người dùng bị mất kết nối và phải kết nối lại. Tôi viết một package Laravel nhỏ để xử lý chuyện này, laravel-oauth-refresh-lock, và chạy thử với tài khoản QuickBooks Online, Xero và Salesforce thật (Microsoft và Google thì chưa). Bài này đi qua từng provider, các nguyên nhân khác gây invalid_grant, rồi tới cách xử lý.

Bài này nói về app của bạn ở vai client gọi API của bên khác. Nếu app của bạn tự phát hành token (Passport, Sanctum hay một package JWT), đó là chuyện khác; Sanctum thậm chí không có refresh token.

Tóm tắt nhanh. Khi nhiều worker cùng refresh một OAuth token, provider nào rotate refresh token đều có thể từ chối hoặc thu hồi lần refresh trùng. QuickBooks Online và Salesforce có thể làm bạn mất kết nối, Xero che race sau grace period 30 phút, còn Microsoft và Google chỉ tốn thêm lời gọi. Cách xử lý: mỗi kết nối một Cache::lock, đọc lại token bên trong lock, và ghi compare-and-swap để lưu access token và refresh token mới trong cùng một lần ghi.

Race condition refresh token là gì?

Race condition khi refresh OAuth token xảy ra lúc nhiều worker cùng gửi một refresh token tới provider có dùng refresh token rotation. Phần lớn provider có rotation sẽ cấp refresh token mới sau mỗi lần refresh và loại bỏ token cũ. Worker đầu tiên thắng. Worker thứ hai nhận được gì là tuỳ provider: một token hợp lệ khác, lỗi invalid_grant, hoặc cả kết nối bị thu hồi.

Kết cục nặng nhất đến từ refresh token reuse detection (phát hiện dùng lại refresh token). RFC 9700 (OAuth 2.0 Security Best Current Practice) mô tả cơ chế này: khi nhận một refresh token đã bị rotate, authorization server không phân biệt được kẻ tấn công với client hợp lệ, nên thu hồi luôn cả refresh token đang dùng (có provider gọi là thu hồi cả token family). Worker thứ hai của chính bạn trông y hệt một kẻ tấn công.

Case study: refresh token rotation theo từng provider

Hành vi của từng provider lấy từ tài liệu chính thức của provider đó, có link trong từng phần. Kết quả "chạy thật" lấy từ các tài khoản test tôi tạo riêng cho việc này: một sandbox company QuickBooks Online, Demo Company của Xero và một org Salesforce Developer Edition, mỗi cái đi kèm một app Laravel 12 mới.

QuickBooks Online: refresh trùng nhận invalid_grant và có thể mất kết nối

Tài liệu nói gì. Intuit đổi giá trị refresh token của QuickBooks Online sau mỗi 24 giờ, và khi giá trị mới được cấp thì giá trị cũ hết hạn. Nếu hai lần refresh dùng cùng một giá trị, lần đầu thành công còn lần thứ hai nhận invalid_grant, và Intuit có thể coi đó là vấn đề bảo mật rồi thu hồi refresh token. Intuit khuyên refresh tuần tự, từng lần một (Intuit OAuth 2.0 FAQ). FAQ không nói rõ refresh trùng trong cùng khung 24 giờ có lỗi không; cứ giả định là có.

Chạy thật. Sandbox company của QuickBooks Online. Sáu tiến trình khởi động cùng lúc với một token đã hết hạn, ba vòng. Vòng nào cũng chỉ một tiến trình refresh, năm tiến trình còn lại dùng token nó đã lưu. Không có invalid_grant; token cuối cùng hợp lệ, refresh response trả về expires_in: 3600. Một vòng, gộp theo kết quả:

   1 REFRESHED the token, company "Sandbox Company …"
   5 used the stored token, company "Sandbox Company …"
6 processes, 1 refresh.

Tôi không chạy thử nghiệm không có lock: việc đó có thể làm sandbox bị thu hồi kết nối. Rủi ro này lấy từ FAQ của Intuit, không phải từ thử nghiệm của tôi.

Rút ra. Đây chính là trường hợp package được viết ra để xử lý: không tuần tự hoá việc refresh, một queue bận rộn có thể làm đứt kết nối sổ sách kế toán của khách hàng.

Code: examples/quickbooks

Xero: grace period 30 phút che giấu race khi rotate refresh token

Tài liệu nói gì. Xero trả về refresh token mới sau mỗi lần refresh. Nếu app không nhận được hoặc không lưu được token mới, nó có thể thử lại bằng refresh token cũ trong 30 phút; sau đó token cũ hết hạn và người dùng phải cấp quyền lại (Xero token types). Xero không nói rõ lỗi trả về khi dùng refresh token cũ sau grace period; OAuth 2.0 quy định invalid_grant cho refresh token không hợp lệ, hết hạn hoặc đã bị thu hồi.

Có hai con số 30 phút rất dễ nhầm: access token sống 30 phút, còn refresh token cũ vẫn dùng được 30 phút sau khi rotate. Bản thân refresh token sống tối đa 60 ngày.

Chạy thật. Demo Company của Xero. Có package: sáu tiến trình, bốn vòng, mỗi vòng một lần refresh.

Sau đó là thí nghiệm đối chứng, không dùng lock: sáu tiến trình refresh cùng lúc bằng cùng một refresh token. Cả sáu đều thành công, và Xero cấp cho mỗi tiến trình một refresh token mới khác nhau. Vậy với Xero, race không hiện ra thành lỗi. Sáu worker giờ giữ sáu refresh token khác nhau; app nào lưu lại token từng worker nhận được thì cuối cùng giữ token được ghi sau cùng. Các token "anh em" còn lại có còn hợp lệ sau 30 phút không thì tài liệu không nói, và tôi chưa đo.

Rút ra. Xero che race đi, nên một tích hợp không xử lý race vẫn trông ổn khi test; lock giúp không sinh ra những token anh em mà số phận sau 30 phút chưa ai biết.

Code: examples/xero (HTTP Basic auth ở token endpoint, tenant lấy từ connections endpoint)

Salesforce: refresh token rotation, dễ dãi vài giây rồi thu hồi cả kết nối

Tài liệu nói gì. Khi bật Refresh Token Rotation của Salesforce, mỗi lần refresh trả về refresh token mới và token cũ bị vô hiệu ngay. Salesforce coi việc dùng lại refresh token là dấu hiệu bị lộ: dùng lại một refresh token đã bị vô hiệu sẽ khiến cả refresh token hiện tại lẫn các access token của nó bị thu hồi, người dùng phải xác thực lại (Refresh Token Rotation). Rotation bật mặc định, và không tắt được, với External Client App mới từ ngày 22 tháng 10 năm 2026 (External Client App defaults).

Token response cũng không có expires_in: thời gian hết hạn session được cấu hình trong Salesforce, nên app chỉ biết access token đã chết khi một lời gọi API trả 401. Điều này làm hỏng thiết kế ban đầu của tôi, vì một package refresh "ngay trước khi hết hạn" sẽ không bao giờ refresh một token không có hạn. Vì vậy package có thêm rejected(), cho đúng luồng quen thuộc "gặp 401, refresh, rồi gọi lại":

$pending = OAuthRefreshLock::for($key);
$accessToken = $pending->accessToken($refresher);

$response = Http::withToken($accessToken)->get($url);
if ($response->status() === 401 && $response->json('0.errorCode') === 'INVALID_SESSION_ID') {
    $accessToken = $pending->rejected($accessToken)->accessToken($refresher);
    $response = Http::withToken($accessToken)->get($url);
}

rejected() sẽ refresh dưới cùng một lock nếu access token đang lưu vẫn là token vừa bị từ chối. Nhiều worker cùng nhận 401 chỉ gây ra một lần refresh: worker đầu refresh, các worker sau thấy access token đã được thay và dùng luôn token mới.

Chạy thật. Một org Salesforce Developer Edition với một External Client App mới. Việc thiết lập cho tôi thấy hai điều tài liệu không nhấn mạnh: với app mới, Refresh Token Rotation, PKCE và giới hạn refresh token không dùng 30 ngày đều bật sẵn và bị khoá, nên yêu cầu cấp quyền bắt buộc phải gửi PKCE; và login.salesforce.com trả invalid_client_id với app vừa tạo, trong khi My Domain của org thì chạy ngay.

Có package: tôi thay access token đang lưu bằng một token Salesforce sẽ từ chối, giống như sau khi session hết hạn, rồi chạy các tiến trình: sáu tiến trình mỗi vòng trong ba vòng, rồi ba tiến trình ở vòng thứ tư. Ở mọi vòng, từng tiến trình đều nhận 401 và báo bằng rejected(); một tiến trình refresh, các tiến trình còn lại dùng token của nó. Kết nối được giữ nguyên. Refresh response không có expires_in và lần nào cũng có refresh token mới, đúng như tài liệu.

Rồi tới các thí nghiệm đối chứng, không dùng lock:

  • Sáu tiến trình refresh cùng lúc bằng cùng một refresh token. Năm thành công, mỗi cái nhận một refresh token mới khác nhau, một cái nhận concurrent_request. Cả năm access token đều dùng được, và refresh token cũ vẫn refresh được vài giây sau đó. Vậy Salesforce chấp nhận việc dùng lại trong một khoảnh khắc ngắn, điều mà bài viết của họ về rotation không hề nhắc.
  • Tôi rotate refresh token một lần, rồi dùng lại token cũ sau 90 giây. Kết quả invalid_grant, và refresh token hiện tại cũng ngừng hoạt động: cả kết nối bị thu hồi, đúng như bài viết của Salesforce mô tả.

Khoảng "dễ dãi" đó kéo dài bao lâu thì tôi chưa đo; đâu đó giữa vài giây và 90 giây. Một worker chậm, thử lại muộn hoặc giữ token cũ sẽ rơi ra ngoài khoảng đó.

Rút ra. Đây là provider khắt khe nhất trong bài. Các lần refresh đồng thời có thể "thoát" trong chốc lát, nhưng một lần dùng lại muộn sẽ chấm dứt kết nối, và việc refresh phải dựa vào lỗi 401 (INVALID_SESSION_ID) chứ không dựa vào thời điểm hết hạn. Package giờ cũng coi concurrent_request là lỗi tạm thời; trang refresh token flow của Salesforce cảnh báo các request đồng thời cùng một refresh token sẽ lỗi lúc được lúc không, và khuyên thử lại.

Code: examples/salesforce

Refresh token của Microsoft và Google: không mất gì nhiều

Microsoft. Refresh token của Microsoft identity platform tự thay bằng token mới sau mỗi lần dùng, nhưng token cũ không bị thu hồi khi đã dùng (Microsoft refresh tokens). Refresh trùng chỉ tốn thêm lời gọi, không làm đứt kết nối.

Google. Google không trả về refresh token mới khi refresh; bạn cứ tiếp tục dùng token đang có (Google OAuth 2.0 for web server apps). Không có race do rotation. Refresh token vẫn có thể ngừng hoạt động vì những lý do khác, liệt kê ở phần dưới.

Rút ra. Với hai provider này, tuần tự hoá việc refresh giúp tiết kiệm lời gọi chứ không phải cứu kết nối. Khi refresh token Microsoft thật sự đã chết, bạn nhận invalid_grant kèm mã AADSTS như AADSTS70008 (error codes); với Google là invalid_grant.

Code: examples/microsoft · examples/google

Tóm tắt: một lần refresh trùng gây ra gì, theo từng provider

Provider

Refresh token cũ sau khi refresh

Một lần refresh trùng

Đã kiểm chứng

QuickBooks Online

hết hạn

invalid_grant, có thể bị thu hồi token

sandbox thật

Salesforce (bật rotation)

dùng được vài giây, rồi vô hiệu

trong vài giây: thành công hoặc concurrent_request; muộn hơn: thu hồi cả kết nối

org Developer Edition thật, kèm thí nghiệm không lock

Xero

dùng được 30 phút

thành công và cấp thêm token

Demo Company thật, kèm thí nghiệm không lock

Microsoft

không bị thu hồi

tốn thêm lời gọi

chỉ theo tài liệu

Google

không rotate

tốn thêm lời gọi

chỉ theo tài liệu

invalid_grant có phải lúc nào cũng do race?

Không. OAuth 2.0 dùng invalid_grant cho mọi refresh token không hợp lệ, hết hạn, đã bị thu hồi hoặc được cấp cho client khác (RFC 6749, mục 5.2). Trước khi đổ cho concurrency, hãy loại trừ những nguyên nhân sau (tất cả lấy từ tài liệu của provider, có link ở trên và trong trang providers của repo):

  • Không lưu refresh token mới. QuickBooks Online và Salesforce loại bỏ giá trị cũ; Xero chỉ cho dùng lại trong 30 phút.
  • Lâu không dùng. QuickBooks Online: hạn 100 ngày, tính lại sau mỗi lần refresh. Xero: tối đa 60 ngày. Salesforce: giới hạn 30 ngày không dùng với External Client App mới. Microsoft: AADSTS70008 hoặc AADSTS700082 khi không hoạt động. Google: sáu tháng không dùng, hoặc 7 ngày với app còn ở trạng thái "Testing" có người dùng bên ngoài.
  • Quyền truy cập bị thu hồi. Người dùng ngắt kết nối app, hoặc (Microsoft, AADSTS50173) grant bị thu hồi, chẳng hạn sau khi đổi mật khẩu.
  • Quá nhiều token. Google vô hiệu token cũ nhất khi một tài khoản có hơn 100 refresh token cho cùng một client.
  • Sai client hoặc sai môi trường. Refresh token cấp cho key của app này lại gửi kèm key của app khác, chẳng hạn key sandbox với production.
  • Policy và giới hạn của Salesforce. Refresh token policy của app có thể cho refresh token hết hạn ngay, sau một khoảng cố định hoặc khi không dùng (OAuth access policies), và danh sách lỗi của Salesforce cũng ghi "IP restricted or invalid login hours" là một nguyên nhân của invalid_grant, không nói rõ flow nào. User bị vô hiệu hoá có mã riêng, inactive_user.
  • Dùng lại muộn sau khi rotate. Salesforce thu hồi cả kết nối.

Với Google, nhiều bài viết trích error_description là Token has been expired or revoked.; tài liệu của Google chỉ nói token "may have expired or has been invalidated".

Race hay bị thu hồi thật? Phân biệt qua log

Chỉ nhìn lỗi thì không biết được. Những dấu hiệu sau thì có:

  • Hai lời gọi refresh cho cùng một kết nối cách nhau vài giây. Ghi log key của kết nối và thời điểm cho mỗi lần refresh. Race để lại hai lời gọi trở lên sát nhau; hết hạn hay bị thu hồi thật chỉ để lại một.
  • Refresh token gửi đi đã bị rotate từ trước. Ghi log một đoạn hash ngắn của refresh token gửi đi và nhận về (không bao giờ ghi chính token). Nếu lời gọi lỗi gửi một giá trị mà lời gọi trước đó đã rotate, đó là race.
  • Gợi ý từ chính provider. Salesforce trả concurrent_request cho một trong hai lần refresh đồng thời. Microsoft đặt mã AADSTS trong error_description, giúp phân biệt không hoạt động (AADSTS70008) với grant bị thu hồi (AADSTS50173).

Khi dùng package, mỗi kết nối chỉ nên có một event TokenRefreshed cho mỗi lần hết hạn, và TokenRevokedByProvider mang mã lỗi. Không event nào chứa giá trị token.

Cách xử lý: Cache::lock cho mỗi token, kèm compare-and-swap

Cách xử lý tối thiểu là một mutex cho mỗi kết nối: một distributed lock (Redis lock, database lock) bọc việc refresh, để chỉ một worker gọi token endpoint, các worker khác chờ và dùng kết quả của nó. Có người gọi pattern này là single-flight. Phác thảo với Cache::lock của Laravel và model token của bạn:

$token = Cache::lock("oauth-refresh:{$key}", 30)->block(10, function () use ($key, $provider) {
    $token = Token::where('key', $key)->firstOrFail();          // read again inside the lock
    if ($token->expires_at?->isAfter(now()->addMinutes(2))) {   // expires_at: nullable 'datetime' cast
        return $token;                                           // another worker already refreshed
    }
    $new = $provider->getAccessToken('refresh_token', ['refresh_token' => $token->refresh_token]);
    $token->update([
        'access_token' => $new->getToken(),
        'refresh_token' => $new->getRefreshToken() ?? $token->refresh_token,
        'expires_at' => $new->getExpires() ? now()->setTimestamp($new->getExpires()) : null,
    ]);
    return $token;
});

Hãy cho các cột token dùng cast encrypted của Laravel; bảng của package đã làm vậy. Chừng đó đã chặn được phần lớn refresh trùng. Nhưng chưa đủ: lock là một lease (có thời hạn), nên một lần refresh chậm hơn 30 giây sẽ để worker thứ hai lọt vào (xem bên dưới); một lần ghi từ bản đọc cũ có thể đè lên token mới hơn; lỗi mạng chập chờn và invalid_grant thật trông như nhau; và với Salesforce (không có expires_in), expires_at là null nên phác thảo này refresh ở mọi lần gọi thay vì chỉ khi gặp 401. Package xử lý những chuyện đó:

$accessToken = OAuthRefreshLock::for("quickbooks:{$realmId}")
    ->accessToken(new LeagueRefresher($provider));
  1. Fast path. Đọc token đang lưu; nếu access token còn hạn lâu hơn khoảng skew (mặc định hai phút, skew_seconds) và không bị báo là bị từ chối, trả về luôn mà không lấy lock.
  2. Mỗi token key một lock, bằng Cache::lock, từ bất kỳ cache store nào của Laravel hỗ trợ atomic lock (Redis, Memcached, DynamoDB, database).
  3. Đọc lại bên trong lock. Các worker phải chờ gần như luôn thấy token mà worker đầu tiên vừa lưu, và trả về luôn mà không gọi provider.
  4. Ghi kiểu compare-and-swap theo số version. Thao tác kết nối và kết nối lại là các lần ghi không điều kiện, bắt đầu từ một version dựa theo thời gian sau khi ngắt kết nối, nên một worker đang giữ bản đọc cũ không thể ghi đè lên kết nối mới hơn.
  5. Phân loại lỗi. Các lỗi mạng mà classifier mặc định nhận ra, cùng HTTP 408, 429 và 5xx, giữ nguyên token và ném TransientRefreshFailure. Chỉ invalid_grant mới đánh dấu token là đã bị thu hồi.

Chọn lock nào trong Laravel?

Lựa chọn

Phối hợp giữa nhiều server

Áp dụng cho job, web request và command

Đọc lại token

Cache::lock trên Redis, Memcached, DynamoDB hoặc database

có, nếu dùng chung store

có

nếu bạn đọc lại bên trong lock

lockForUpdate() trong một database transaction (SQLite bỏ qua)

có

có

có, nhưng transaction mở suốt lời gọi HTTP tới provider

Job middleware WithoutOverlapping

có, nếu dùng chung store

chỉ queued job

không

file cache lock

không, chỉ một server

có

nếu bạn đọc lại bên trong lock

array cache lock

không, chỉ một tiến trình

—

—

Lock hay ghi có điều kiện? Cần cả hai

Có một lập luận khá phổ biến: dùng ghi có điều kiện thay cho distributed lock. Ghi có điều kiện ("chỉ cập nhật nếu version không đổi") đúng là bảo vệ được token store, nhưng cả hai worker vẫn gọi provider, và provider vẫn thấy lời gọi thứ hai. Trong các lần chạy không lock của tôi, Xero cấp sáu refresh token anh em, còn Salesforce trả concurrent_request rồi thu hồi cả kết nối khi bị dùng lại muộn. Chỉ có lock thì hở ở chiều ngược lại: lock là một lease, và lease có thể hết giữa chừng lúc đang refresh. Với lease mặc định 30 giây:

0 s    A  lấy lock, gửi RT1 (chậm)
30 s      lease hết hạn; lời gọi của A vẫn đang chạy
31 s   B  lấy lock, đọc lại: vẫn là RT1, gửi RT1   ← provider thấy RT1 hai lần
32 s   B  nhận invalid_grant, ghi trạng thái thu hồi cho version đó
35 s   A  nhận RT2; compare-and-swap thất bại, thấy B thu hồi đúng version A vừa rotate, lưu RT2

Một thứ tự có thể xảy ra, khi B nhận invalid_grant (QuickBooks, Salesforce).

Vì vậy package làm cả hai: lock chặn lời gọi thứ hai, còn số version (đóng vai trò như một fencing token) giữ token store nhất quán khi lock không đủ. Token mới của A còn dùng được không là tuỳ provider: Salesforce có thể đã thu hồi tất cả sau lần dùng lại muộn. Với Xero (trong 30 phút) hay Microsoft, lần refresh của B thường thành công luôn; khi đó compare-and-swap của A thất bại, token store giữ token của B, còn phía provider có hai thế hệ token. Hãy đặt HTTP timeout của lời gọi refresh thấp hơn hẳn thời hạn lease. Chi tiết trong how it works.

WithoutOverlapping cũng không thay thế được: nó chỉ áp dụng cho queued job cùng key và không đọc lại token (xem bảng ở trên).

Luôn lưu refresh token mới, ghi một lần

Lưu access token, refresh token và thời điểm hết hạn từ mỗi refresh response cùng nhau, trong một lần ghi. Intuit nói thẳng điều này: giữ giá trị refresh token mới nhất từ mỗi response. Nếu bạn lưu access token trước rồi mới lưu refresh token ở lần ghi thứ hai, một sự cố xảy ra giữa chừng sẽ để lại cho bạn một refresh token đã bị loại.

Nếu response không có refresh_token, như với Google, hãy giữ token đang có. Package làm đúng như vậy: một lần refresh không có refresh token mới sẽ giữ nguyên token đang lưu.

Gặp invalid_grant thì làm gì?

  1. Đọc lại token đang lưu một lần. Có thể worker khác đã refresh trong lúc lời gọi của bạn đang chạy, và refresh token của bạn chỉ đơn giản là đã cũ. Package làm việc này: nếu version đang lưu đã đổi trong lúc gọi, nó trả về token mà worker kia đã lưu thay vì đánh dấu là bị thu hồi.
  2. Đừng thử lại. Gửi lại cùng một refresh token đã chết không thể thành công, và với reuse detection còn có thể gây hại thêm. Package đánh dấu token là đã bị thu hồi và ném TokenRevoked; các lần gọi sau ném ngay lỗi đó mà không gọi provider.
  3. Yêu cầu người dùng kết nối lại. Lắng nghe TokenRevokedByProvider để báo cho người dùng hoặc đánh dấu tích hợp có vấn đề. Kiểm tra token đang lưu trước khi xử lý, như README hướng dẫn.

Refresh trước khi hết hạn, khi gặp 401, hay theo lịch?

Refresh ngay trước khi hết hạn nếu provider cho biết thời hạn, và khi gặp 401 nếu provider không cho (Salesforce) hoặc token chết sớm. Package làm cả hai: khoảng skew hai phút cho trường hợp đầu, rejected() cho trường hợp sau.

Một scheduled command refresh mọi token trước khi hết hạn cũng chỉ là thêm một worker. Nó vẫn va chạm với các job phát hiện token hết hạn đúng lúc đó, nên vẫn cần lock. Với Salesforce nó còn không có mốc nào để lên lịch, vì token không có thời hạn. Thêm jitter cho lịch refresh giúp dàn tải giữa các kết nối, nhưng cả jitter lẫn khoảng skew hai phút đều không ngăn được hai worker race trên cùng một kết nối.

Test như thế nào

  • Một test đồng thời fork tám tiến trình PHP, chờ ở một barrier rồi cùng đòi một token sắp hết hạn (hoặc bị từ chối), trong ba vòng. Test chạy với database lock trên SQLite, và trên CI với database lock và Redis lock trên MySQL, ba lần mỗi job. Một lần chạy đối chứng không có lock phải cho ra nhiều hơn một lần refresh; nếu không, test chẳng chứng minh được gì.
  • CI phát hiện một bug thật trong quá trình làm: trên Laravel 10, database cache lock bị deadlock trên MySQL khi nhiều worker tranh một dòng lock (Laravel 11 và 12 dùng chung đoạn code lock đó nên cũng được sửa). Giờ một lỗi tranh chấp khi đang lấy lock được coi là thua lượt và worker tiếp tục chờ; khi việc refresh đã bắt đầu thì không thử lại gì cả.
  • Chạy thật: QuickBooks Online, Xero và Salesforce, như ở trên.

Giới hạn

  • Package phối hợp một ứng dụng dùng chung một cache store. Horizon worker trên nhiều server vẫn được phối hợp miễn là dùng chung store đó (ví dụ Redis). Hai ứng dụng khác nhau cùng dùng một kết nối OAuth thì không được phối hợp.
  • rejected() nhận biết một lần refresh đã xong qua giá trị access token mới; nếu provider trả lại đúng giá trị cũ, mỗi nơi báo lỗi sẽ gây ra một lần refresh.
  • Laravel 10 và 11 được hỗ trợ ở mức best-effort. Cả hai đã hết thời gian hỗ trợ bảo mật của Laravel và có các advisory đã công bố sẽ không được vá, nên hãy nâng cấp nếu có thể (support policy).

Dùng thử

composer require dakiquang/laravel-oauth-refresh-lock league/oauth2-client
php artisan vendor:publish --tag=oauth-refresh-lock-migrations
php artisan migrate

Hỗ trợ Laravel 10–13, PHP 8.1–8.5 (theo yêu cầu PHP tối thiểu của từng bản Laravel). README hướng dẫn kết nối, refresh, rejected(), ngắt kết nối, xử lý lỗi và events; repo có ví dụ chạy được cho QuickBooks Online, Xero, Salesforce, Microsoft Graph và Google. Rất mong nhận phản hồi từ những provider tôi chưa thử.

Dự án mã nguồn mở cá nhân, không liên quan tới bất kỳ công ty hay OAuth provider nào.

Quay lại