Series Thực hành SSO | Bài 3: Bài toán "Đau đầu" mang tên Single Logout (SLO)
Chào anh em! Ở Bài 2, chúng ta đã code thành công cú "bắt tay" giữa Client App và SSO Server. User vào Client App -> Chuyển hướng sang SSO -> Đăng nhập -> Trả về Client App cùng với Access Token. Rất mượt mà!
Nhưng hãy tưởng tượng kịch bản này: Hệ thống của bạn có 3 ứng dụng (Bán vé, Quản lý kho, Nhân sự) cùng dùng chung 1 SSO Server. Người dùng bấm nút "Đăng xuất" ở app Bán vé. Session ở app Bán vé bị xóa. Nhưng Access Token của họ vẫn còn hiệu lực, và Session ở SSO Server vẫn đang tồn tại! Lát sau, họ quay lại app Quản lý kho, hệ thống thấy SSO Server vẫn còn Session nên tự động cho đăng nhập luôn mà không thèm hỏi mật khẩu.
Nếu đây là máy tính công cộng, hậu quả sẽ là thảm họa. Giải pháp duy nhất là Single Logout (SLO).
1. Hai trường phái của Single Logout
Để báo cho toàn bộ hệ thống biết rằng "Ông A đã đăng xuất", chúng ta có 2 cách phổ biến:
- Front-channel Logout (Qua trình duyệt): Khi user bấm đăng xuất ở Client A, Client A sẽ redirect trình duyệt sang SSO Server. SSO Server xóa session của nó, rồi lại dùng thẻ
<iframe>ẩn hoặc redirect liên tiếp để bắt trình duyệt gọi URL đăng xuất của Client B, Client C. Cách này khá cổ điển, dễ bị trình duyệt chặn (block third-party cookies). - Back-channel Logout (Qua Backend - Server to Server): Khi SSO Server nhận lệnh đăng xuất, nó sẽ âm thầm gửi một HTTP POST request (giống như Webhook) từ server của nó thẳng tới server của Client B, Client C để ra lệnh: "Xóa ngay session của user A đi".
Với tư duy của những kỹ sư thiết kế hệ thống backend, chúng ta sẽ chọn Back-channel Logout vì tính ổn định và bảo mật cao của nó. Sự nhất quán về trạng thái (state consistency) giữa các server luôn được đảm bảo mà không phụ thuộc vào trình duyệt của người dùng.
2. Bước 1: Xóa Session và Thu hồi Token tại SSO Server
Khi người dùng bấm Đăng xuất tại Client App, Client App phải lập tức xóa session nội bộ của nó, sau đó redirect người dùng sang một endpoint đăng xuất của SSO Server.
Tại Client App (client-app.test/routes/web.php):
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
Route::post('/logout', function (Request $request) {
// 1. Xóa session ở Client App
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
// 2. Chuyển hướng sang SSO Server để yêu cầu xóa session gốc
$ssoLogoutUrl = env('SSO_SERVER_URL') . '/api/sso-logout';
return redirect($ssoLogoutUrl);
});
Tại SSO Server (sso-server.test/routes/api.php):
SSO Server nhận yêu cầu này, nó phải làm 2 việc: Thu hồi (Revoke) tất cả các Access Token của user này và xóa Session gốc.
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
Route::get('/sso-logout', function (Request $request) {
$user = $request->user(); // Lấy user hiện tại thông qua token/session
if ($user) {
// Thu hồi toàn bộ Access Token và Refresh Token của user này trong Passport
$user->tokens->each(function ($token) {
$token->revoke();
});
// (Tùy chọn) Gửi Webhook Back-channel đến các Client khác tại đây
// broadcastLogoutToOtherClients($user->id);
Auth::logout();
$request->session()->invalidate();
}
// Redirect người dùng về lại trang chủ của Client App (hoặc trang Login)
return redirect('http://client-app.test/login');
})->middleware('auth:api');
3. Bước 2: Back-channel Logout - Báo tin cho các Client khác (Nâng cao)
Để hoàn thiện bức tranh, nếu hệ thống có nhiều Client, SSO Server cần lưu lại một danh sách các "Webhook Đăng xuất" của từng Client.
Khi hàm broadcastLogoutToOtherClients($userId) được gọi, SSO Server sẽ đẩy một Job vào Queue (RabbitMQ hoặc Redis) để bắn API sang các Client khác:
// Đoạn code minh họa logic Broadcast tại SSO Server
function broadcastLogoutToOtherClients($userId) {
$clients = DB::table('oauth_clients')->get();
foreach ($clients as $client) {
if ($client->logout_webhook_url) {
// Gửi một Server-to-Server request với mã bí mật
Http::post($client->logout_webhook_url, [
'user_id' => $userId,
'secret' => $client->secret
]);
}
}
}
Tại các Client App, anh em chỉ cần mở một API Endpoint để hứng cái Webhook này, tìm trong cache/database xem session nào thuộc về $userId đó và tiến hành xóa bỏ. Thế là toàn bộ hệ thống đồng nhất trạng thái đăng xuất ngay tắp lự!
Tổng kết Bài 3
Bằng việc kết hợp luồng Redirect (để xóa Cookie trên trình duyệt) và Back-channel Webhook (để đồng bộ trạng thái giữa các Server), chúng ta đã giải quyết triệt để bài toán Single Logout. Những lỗi ngớ ngẩn kiểu "vẫn còn phiên đăng nhập ảo" sẽ bị loại bỏ hoàn toàn.
All rights reserved