0

Series Thực hành SSO | Bài 2: Xây dựng Client App và Cú "Bắt tay" Authorization Code

Chào anh em! Ở Bài 1, chúng ta đã dựng thành công một SSO Server (chạy ở tên miền sso-server.test) được trang bị vũ khí hạng nặng Laravel Passport. Nó đang nằm đó, sẵn sàng chờ cấp phát Token.

Hôm nay, chúng ta sẽ đóng vai một Service Provider – ứng dụng nghiệp vụ cần chức năng đăng nhập (ví dụ: client-app.test). Thay vì tạo bảng users và bắt người dùng gõ lại mật khẩu, Client App sẽ "nhờ" SSO Server xác thực hộ. Để làm được điều này, chúng ta cần đăng ký Client App với SSO Server và thực hiện luồng Authorization Code Flow.

Hôm nay, chúng ta sẽ đóng vai một Service Provider – ứng dụng nghiệp vụ cần chức năng đăng nhập (ví dụ: client-app.test). Thay vì tạo bảng users và bắt người dùng gõ lại mật khẩu, Client App sẽ "nhờ" SSO Server xác thực hộ. Để làm được điều này, chúng ta cần đăng ký Client App với SSO Server và thực hiện luồng Authorization Code Flow.

1. Đăng ký Client App với SSO Server

Cũng giống như ngoài đời, bạn không thể tự nhiên chạy đến một tòa nhà và đòi thẻ ra vào. Bạn phải đăng ký trước.

Mở terminal ở project SSO Server (sso-server.test), chạy lệnh sau để đăng ký một Client mới:

php artisan passport:client

Terminal sẽ hỏi bạn vài thông tin:

  • Tên Client: Gõ vào Client App 1.
  • Redirect URL: Đây là đường dẫn cực kỳ quan trọng. SSO Server sẽ gửi cái "Mã xác nhận" (Authorization Code) về đường dẫn này sau khi user đồng ý. Hãy gõ: http://client-app.test/callback

Sau khi chạy xong, database sẽ sinh ra cho bạn một cặp Client ID (ví dụ: 3) và Client Secret (ví dụ: abc123xyz...). Hãy copy chúng lại để dùng cho bước tiếp theo.

2. Khởi tạo Client App

Mở một terminal mới, tạo project thứ hai đóng vai trò là Client:

composer create-project laravel/laravel client-app
cd client-app

Mở file .env của client-app lên và lưu lại các thông tin chúng ta vừa lấy được từ SSO Server:

SSO_SERVER_URL=http://sso-server.test
SSO_CLIENT_ID=3
SSO_CLIENT_SECRET=abc123xyz...
SSO_REDIRECT_URI=http://client-app.test/callback

3. Chế tạo nút "Đăng nhập bằng SSO"

Khi người dùng vào Client App và bấm "Đăng nhập", chúng ta không hiện form điền mật khẩu. Thay vào đó, chúng ta "đá" (redirect) họ sang SSO Server.

Mở file routes/web.php của client-app, viết route /login:

use Illuminate\Support\Str;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;

Route::get('/login', function (Request $request) {
    $request->session()->put('state', $state = Str::random(40));

    // Xây dựng query string theo đúng chuẩn OAuth2
    $query = http_build_query([
        'client_id' => env('SSO_CLIENT_ID'),
        'redirect_uri' => env('SSO_REDIRECT_URI'),
        'response_type' => 'code',
        'scope' => '', // Quyền truy cập (để trống là mặc định)
        'state' => $state, // Chuỗi ngẫu nhiên để chống tấn công CSRF
    ]);

    // Bẻ lái người dùng sang trang cấp quyền của SSO Server
    return redirect(env('SSO_SERVER_URL') . '/oauth/authorize?' . $query);
});

Lúc này, trình duyệt của người dùng sẽ bay sang trang sso-server.test. Nếu họ chưa đăng nhập ở SSO, hệ thống sẽ bắt họ nhập Username/Password. Nếu họ nhập đúng, màn hình "Client App 1 muốn truy cập thông tin của bạn. Đồng ý hay Từ chối?" sẽ hiện ra.

4. Bắt mã Code và Đổi lấy Access Token (Callback)

Khi người dùng bấm "Đồng ý", SSO Server sẽ redirect họ trở lại đường dẫn http://client-app.test/callback kèm theo một cái mã (Code) trên URL.

Chúng ta cần viết route /callback để bắt lấy cái mã này, và bí mật gọi một API ngầm (HTTP POST) sang SSO Server để đổi mã lấy Access Token.

Route::get('/callback', function (Request $request) {
    // 1. Kiểm tra mã state để chống giả mạo
    $state = $request->session()->pull('state');
    if (strlen($state) > 0 && $state !== $request->state) {
        return response('Lỗi xác thực State!', 403);
    }

    // 2. Gọi HTTP Request thuần sang SSO Server để đổi Code lấy Token
    $response = Http::asForm()->post(env('SSO_SERVER_URL') . '/oauth/token', [
        'grant_type' => 'authorization_code',
        'client_id' => env('SSO_CLIENT_ID'),
        'client_secret' => env('SSO_CLIENT_SECRET'),
        'redirect_uri' => env('SSO_REDIRECT_URI'),
        'code' => $request->code,
    ]);

    $tokenData = $response->json();

    if (isset($tokenData['error'])) {
        return response('Đổi token thất bại!', 500);
    }

    $accessToken = $tokenData['access_token'];

    // 3. Dùng Access Token vừa lấy được để gọi API lấy thông tin User
    $userResponse = Http::withHeaders([
        'Accept' => 'application/json',
        'Authorization' => 'Bearer ' . $accessToken,
    ])->get(env('SSO_SERVER_URL') . '/api/user');

    $ssoUser = $userResponse->json();

    // 4. Xử lý logic đăng nhập nội bộ tại Client App
    // (Tìm user trong DB của client, nếu chưa có thì tạo mới, sau đó Auth::login)
    
    return "Tuyệt vời! Bạn đã đăng nhập thành công với tên: " . $ssoUser['name'];
});

Lưu ý ở bên SSO Server: Để API /api/user hoạt động, bạn cần mở file routes/api.php của SSO Server và đảm bảo route này đang dùng middleware auth:api của Passport.

Tổng kết Bài 2

Anh em vừa hoàn thành xong phần cốt lõi và khó nhất của toàn bộ hệ thống SSO! Bằng các HTTP Request thuần túy, chúng ta đã thấy rõ cách dữ liệu luân chuyển:

  1. Client điều hướng User sang SSO kèm client_id.
  2. SSO trả về code cho Client.
  3. Client lén dùng code + client_secret đổi lấy access_token.
  4. Client dùng access_token lấy data User.

Đó chính là bức tranh hoàn hảo của Authorization Code Flow. Ở các dự án thực tế, thay vì tự viết đoạn code HTTP rườm rà ở route /callback, người ta thường dùng packagelaravel/socialite để cấu hình cho nhanh. Tuy nhiên, việc hiểu sâu tầng đáy này sẽ giúp anh em debug cực kỳ dễ dàng khi tích hợp với các hệ thống lớn.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí