+2

Series Laravel & Google 2FA Thực chiến #2: Cấp phát "Mật lệnh" và Sinh mã QR

1. Tư duy thiết kế API tách rời (Decoupled Architecture)

Nếu bạn làm web MVC truyền thống (Blade Template), bạn có thể dùng PHP để vẽ luôn ảnh SVG của QR Code trả về cho trình duyệt. Nhưng trong hệ thống hiện đại, Backend chỉ trả về dữ liệu (JSON). Việc vẽ mã QR (Render) sẽ do Frontend (React/Vue) đảm nhận.

Do đó, API của chúng ta chỉ cần trả về Provisioning URI (Một đoạn chuỗi URL đặc biệt bắt đầu bằng otpauth://...). Frontend sẽ tự lấy chuỗi này nạp vào thư viện để hiện ra hình vuông đen trắng.

2. Xây dựng Controller Khởi tạo 2FA

Mở Terminal và tạo một Controller mới chuyên lo việc thiết lập bảo mật:

docker-compose exec app php artisan make:controller Api/TwoFactorSetupController

Mở file app/Http/Controllers/Api/TwoFactorSetupController.php và bắt đầu viết logic. Chúng ta sẽ dùng Facade Google2FA mà package đã cung cấp.

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use PragmaRX\Google2FALaravel\Facade as Google2FA;
use Illuminate\Support\Facades\Cache;

class TwoFactorSetupController extends Controller
{
    /**
     * BƯỚC 1 & 2: Sinh Secret Key và trả về chuỗi QR
     */
    public function setup(Request $request)
    {
        $user = $request->user();

        // 1. Sinh ra một mã Secret Key ngẫu nhiên (Dài 16 hoặc 32 ký tự)
        $secretKey = Google2FA::generateSecretKey();

        // 2. Lưu TẠM THỜI mã này vào Cache (Sống trong 15 phút)
        // Tại sao lưu tạm? Vì nếu user chưa quét QR mà tắt máy, ta không nên lưu cứng vào DB vội.
        $cacheKey = '2fa_setup_' . $user->id;
        Cache::put($cacheKey, $secretKey, now()->addMinutes(15));

        // 3. Tạo chuỗi Provisioning URI để Frontend vẽ QR Code
        // Cấu trúc: otpauth://totp/Tên_Cty:Email?secret=Khóa_Bí_Mật&issuer=Tên_Cty
        $qrCodeUrl = Google2FA::getQRCodeUrl(
            'Metro AFC System', // Tên hệ thống (Sẽ in đậm trên app của user)
            $user->email,       // Tên tài khoản để user dễ phân biệt
            $secretKey          // Khóa bí mật
        );

        return response()->json([
            'success' => true,
            'message' => 'Vui lòng dùng ứng dụng Google Authenticator để quét mã QR này.',
            'data' => [
                'secret_key'  => $secretKey, // Trả về raw text phòng khi camera hỏng, user có thể nhập tay
                'qr_code_url' => $qrCodeUrl  // Chuỗi để Frontend vẽ QR
            ]
        ]);
    }
}

3. API Xác nhận và Kích hoạt chính thức

Lúc này, Quản lý ga đã quét xong mã QR, app trên điện thoại bắt đầu nhảy số liên tục. Họ sẽ lấy 6 số đó điền vào form và bấm "Xác nhận". Backend của chúng ta cần kiểm tra xem 6 số đó có đúng với cái Secret Key vừa sinh ra không.

Tiếp tục viết thêm hàm confirm vào bên trong TwoFactorSetupController:

/**
     * BƯỚC 3: Xác thực mã 6 số lần đầu và lưu vào Database
     */
    public function confirm(Request $request)
    {
        // Yêu cầu Frontend phải gửi lên mã OTP 6 số
        $request->validate([
            'otp_code' => 'required|digits:6'
        ]);

        $user = $request->user();
        $cacheKey = '2fa_setup_' . $user->id;

        // 1. Lấy Secret Key đang lưu tạm trong Cache ra
        $secretKey = Cache::get($cacheKey);

        if (!$secretKey) {
            return response()->json([
                'success' => false,
                'message' => 'Phiên thiết lập đã hết hạn. Vui lòng lấy lại mã QR mới.'
            ], 400);
        }

        // 2. Xác thực mã OTP 6 số (Verify)
        // Hàm verifyKey sẽ lấy Secret + Thời gian hiện tại để so sánh với mã user gửi lên
        $isValid = Google2FA::verifyKey($secretKey, $request->input('otp_code'));

        if ($isValid) {
            // 3. Nếu ĐÚNG: Lưu Secret Key vào Database vĩnh viễn
            // Pro-tip: Ở các hệ thống tài chính, người ta thường dùng hàm encrypt() của Laravel 
            // để mã hóa chuỗi $secretKey trước khi lưu xuống DB nhằm chống lộ lọt nếu DB bị hack.
            $user->google2fa_secret = encrypt($secretKey); 
            $user->save();

            // Xóa Cache rác đi
            Cache::forget($cacheKey);

            return response()->json([
                'success' => true,
                'message' => 'Tuyệt vời! Tính năng bảo mật 2 lớp đã được kích hoạt thành công.'
            ]);
        }

        // 4. Nếu SAI: Báo lỗi để user nhập lại
        return response()->json([
            'success' => false,
            'message' => 'Mã xác thực không hợp lệ hoặc đã hết hạn (quá 30s). Vui lòng thử lại.'
        ], 400);
    }

4. Đăng ký Routes bảo vệ

Cuối cùng, chúng ta mở file routes/api.php ra và phơi bày 2 endpoint này. Lưu ý, user bắt buộc phải đăng nhập thành công (vượt qua chốt AuthN) thì mới được phép gọi các API cài đặt này.

use App\Http\Controllers\Api\TwoFactorSetupController;

Route::middleware(['auth:api'])->prefix('2fa')->group(function () {
    // API 1: Lấy mã QR để cài đặt
    Route::get('/setup', [TwoFactorSetupController::class, 'setup']);
    
    // API 2: Gửi thử 6 số lên để chốt sổ
    Route::post('/confirm', [TwoFactorSetupController::class, 'confirm']);
});

Tổng kết Bài 2

Đến đây, bạn đã hoàn thiện một luồng Setup 2FA cực kỳ an toàn, chuyên nghiệp và tối ưu bộ nhớ. Việc sử dụng Cache để lưu mã tạm thời giúp bảng users không bị dính dữ liệu rác nếu nhân viên ấn nút "Bật 2FA" rồi... bỏ đi uống cà phê mà quên không quét mã. Mã hóa encrypt() khi lưu xuống Database cũng là một tiêu chuẩn bắt buộc cho kiến trúc bảo mật cấp cao.

Tài khoản của nhân viên giờ đây đã có "khóa bí mật" nằm gọn trong cơ sở dữ liệu. Ở Bài 3, chúng ta sẽ thiết kế một Middleware đánh chặn tại cửa ngõ Đăng nhập (Login). Bất kỳ ai nhập đúng Username/Password nhưng chưa có mã 6 số thì vẫn bị nhốt ở phòng chờ (2FA Challenge) không cho lọt vào hệ thống lõi!


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í