Các phương pháp xác thực & bảo mật ứng dụng-P2:Digest Authentication

3 phút đọc

Tổng quan

Digest Authentication là phương thức xác thực HTTP được định nghĩa trong RFC 7616, ra đời để khắc phục điểm yếu chết người của Basic Auth: không bao giờ gửi mật khẩu dạng plaintext.

Thay vì gửi username:password encode Base64, Digest Auth sử dụng cơ chế challenge-response: server gửi một chuỗi ngẫu nhiên (nonce), client dùng nonce đó để tính toán một hash MD5 từ credentials và gửi hash đó lên. Server tính lại hash phía mình và so sánh — mật khẩu thực không bao giờ truyền qua mạng.

Đây là bước tiến bộ so với Basic Auth, nhưng vẫn bị coi là lỗi thời trong các hệ thống hiện đại vì MD5 đã bị crack.


Flow xác thực

sequenceDiagram
    participant Client
    participant Server
    Client->>Server: GET /resource
    Server-->>Client: 401 Unauthorized<br/>WWW-Authenticate: Digest realm="api", nonce="dcd98b7102", qop="auth"
    Client->>Client: HA1 = MD5(username:realm:password)
    Client->>Client: HA2 = MD5(method:digestURI)
    Client->>Client: response = MD5(HA1:nonce:nc:cnonce:qop:HA2)
    Client->>Server: GET /resource<br/>Authorization: Digest username="user", nonce="dcd98b7102", response="6629..."
    Server->>Server: Tính lại response hash và so sánh
    alt Hash khớp
        Server-->>Client: 200 OK + Resource
    else Hash không khớp
        Server-->>Client: 401 Unauthorized
    end

🔐 Cơ chế tính hash chi tiết

Digest Auth tính hash theo 3 bước:

Bước 1 — HA1 (hash từ thông tin danh tính):

HA1 = MD5(username:realm:password)
# Ví dụ:
HA1 = MD5("john:api.example.com:secret123")
    = "939e7578ed9e3c518a452acee763bce9"

Bước 2 — HA2 (hash từ method và URI):

HA2 = MD5(method:digestURI)
# Ví dụ:
HA2 = MD5("GET:/resource")
    = "39aff3a2bab80126f2571a5027e8a93d"

Bước 3 — Response (hash cuối cùng gửi lên server):

response = MD5(HA1:nonce:nc:cnonce:qop:HA2)
# Ví dụ:
response = MD5("939e...:dcd98b7102:00000001:0a4f113b:auth:39af...")
         = "6629fae49393a05397450978507c4ef1"

Ví dụ HTTP header thực tế

Request đầu tiên (chưa có credentials):

GET /api/resource HTTP/1.1
Host: api.example.com

Server trả về challenge:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Digest realm="api.example.com",
                         qop="auth",
                         nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
                         opaque="5ccc069c403ebaf9f0171e9517f40e41"

Client gửi lại với credentials đã hash:

GET /api/resource HTTP/1.1
Host: api.example.com
Authorization: Digest username="john",
               realm="api.example.com",
               nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
               uri="/api/resource",
               qop=auth,
               nc=00000001,
               cnonce="0a4f113b",
               response="6629fae49393a05397450978507c4ef1",
               opaque="5ccc069c403ebaf9f0171e9517f40e41"

Ví dụ thực tế

cURL:

# cURL tự xử lý Digest Auth
curl --digest -u username:password https://api.example.com/resource

# Verbose để xem toàn bộ challenge-response flow
curl --digest -u username:password -v https://api.example.com/resource

PHP (Laravel Middleware):

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class DigestAuthentication
{
    // Lưu ý: Digest Auth yêu cầu server biết password plaintext
    // để tính lại hash → không thể dùng bcrypt
    private array $users = [
        'john' => 'secret123',
    ];

    private string $realm = 'api.example.com';

    public function handle(Request $request, Closure $next)
    {
        $authHeader = $request->header('Authorization');

        if (!$authHeader || !str_starts_with($authHeader, 'Digest ')) {
            return $this->requestDigestAuth();
        }

        $params = $this->parseDigestHeader($authHeader);
        $username = $params['username'] ?? '';

        if (!isset($this->users[$username])) {
            return $this->requestDigestAuth();
        }

        $password = $this->users[$username];

        // Tính lại hash để so sánh
        $HA1 = md5("{$username}:{$this->realm}:{$password}");
        $HA2 = md5($request->method() . ':' . ($params['uri'] ?? ''));
        $expected = md5("{$HA1}:{$params['nonce']}:{$params['nc']}:{$params['cnonce']}:{$params['qop']}:{$HA2}");

        if (!hash_equals($expected, $params['response'] ?? '')) {
            return $this->requestDigestAuth();
        }

        return $next($request);
    }

    private function requestDigestAuth()
    {
        $nonce = uniqid();
        $opaque = md5($this->realm);

        return response('Unauthorized', 401)->header(
            'WWW-Authenticate',
            "Digest realm=\"{$this->realm}\", qop=\"auth\", nonce=\"{$nonce}\", opaque=\"{$opaque}\""
        );
    }

    private function parseDigestHeader(string $header): array
    {
        $params = [];
        preg_match_all('/(\w+)=["\']?([^\'"\s,]+)["\']?/', $header, $matches, PREG_SET_ORDER);
        foreach ($matches as $match) {
            $params[$match[1]] = $match[2];
        }
        return $params;
    }
}

JavaScript (Fetch — không hỗ trợ native, cần tự tính hash):

import md5 from 'md5'; // npm install md5

async function digestFetch(url, username, password) {
    // Bước 1: Request đầu — lấy challenge từ server
    const res1 = await fetch(url);
    if (res1.status !== 401) return res1;

    const wwwAuth = res1.headers.get('WWW-Authenticate');
    const nonce   = wwwAuth.match(/nonce="([^"]+)"/)[1];
    const realm   = wwwAuth.match(/realm="([^"]+)"/)[1];
    const opaque  = wwwAuth.match(/opaque="([^"]+)"/) ?.[1];

    // Bước 2: Tính hash
    const nc     = '00000001';
    const cnonce = Math.random().toString(36).substring(2, 10);
    const HA1    = md5(`${username}:${realm}:${password}`);
    const HA2    = md5(`GET:${new URL(url).pathname}`);
    const response = md5(`${HA1}:${nonce}:${nc}:${cnonce}:auth:${HA2}`);

    // Bước 3: Gửi lại với Authorization header
    const authHeader = [
        `Digest username="${username}"`,
        `realm="${realm}"`,
        `nonce="${nonce}"`,
        `uri="${new URL(url).pathname}"`,
        `qop=auth`,
        `nc=${nc}`,
        `cnonce="${cnonce}"`,
        `response="${response}"`,
        opaque ? `opaque="${opaque}"` : '',
    ].filter(Boolean).join(', ');

    return fetch(url, { headers: { Authorization: authHeader } });
}

// Sử dụng
digestFetch('https://api.example.com/resource', 'john', 'secret123')
    .then(res => res.json())
    .then(console.log);

So sánh Digest vs Basic Auth

Tiêu chí Basic Auth Digest Auth
Truyền mật khẩu qua mạng ✅ Có (Base64) ❌ Không (chỉ gửi hash)
Chống replay attack ❌ Không ✅ Có (nonce thay đổi mỗi request)
Độ phức tạp triển khai Rất đơn giản Phức tạp hơn
Thuật toán hash Base64 (không phải hash) MD5 (đã yếu)
Hỗ trợ lưu password dạng hash (bcrypt) ❌ Không cần ❌ Cần plaintext hoặc MD5(user:realm:pass)
Tương thích trình duyệt ✅ Native ✅ Native (nhưng ít dùng)
Mức độ bảo mật thực tế Rất thấp Thấp (MD5 đã bị crack)

⚠️ Tại sao MD5 không còn an toàn?

MD5 từng là thuật toán hash phổ biến, nhưng hiện đã bị coi là broken về mặt bảo mật:

# MD5 cực kỳ nhanh → dễ brute-force
# GPU hiện đại có thể tính hàng tỷ MD5 hash/giây
Hashcat benchmark (RTX 4090):
MD5: ~164,000 MH/s (164 tỷ hash/giây)

# Để so sánh, bcrypt (cost=12):
bcrypt: ~184 kH/s (184 nghìn hash/giây)
# → MD5 nhanh hơn bcrypt ~890,000 lần

Ngoài ra, MD5 còn có collision vulnerability: hai input khác nhau có thể cho ra cùng một hash output.


Ưu & Nhược điểm

Ưu điểm Nhược điểm
Mật khẩu không bao giờ truyền qua mạng MD5 đã bị crack, không an toàn với phần cứng hiện đại
Chống replay attack bằng nonce Server phải lưu password dạng plaintext hoặc MD5 (không thể dùng bcrypt)
Không cần session/state Phức tạp hơn Basic Auth đáng kể
Được hỗ trợ native bởi HTTP clients Không hỗ trợ fine-grained permissions
Tốt hơn Basic Auth nếu bắt buộc phải chọn Ít được hỗ trợ bởi các framework hiện đại

Khi nào nên và không nên dùng?

✅ Có thể cân nhắc khi:

  • Hệ thống legacy đang dùng Digest Auth và chưa thể migrate
  • Giao tiếp với thiết bị IoT / embedded chỉ hỗ trợ Digest Auth (camera IP, router...)
  • Môi trường nội bộ không có HTTPS và cần tốt hơn Basic Auth

❌ Không nên dùng khi:

  • Xây dựng API hoặc ứng dụng mới — dùng JWT hoặc OAuth2 thay thế
  • Cần lưu password an toàn bằng bcrypt/argon2 — Digest Auth không tương thích
  • Hệ thống public-facing có yêu cầu bảo mật cao
  • Cần logout, token revocation, hoặc phân quyền chi tiết

Lưu ý quan trọng

  • Vẫn cần HTTPS — dù không gửi password, hash bị bắt vẫn có thể bị brute-force
  • Server bắt buộc biết password gốc (hoặc dạng MD5(user:realm:pass)) → không dùng được bcrypt
  • nonce phải được tạo ngẫu nhiên và có thời hạn để chống replay attack hiệu quả
  • RFC 7616 (2015) thêm hỗ trợ SHA-256 và SHA-512/256 thay cho MD5, nhưng ít được implement
  • Trong thực tế 2024+: nếu đang chọn giữa Basic và Digest, hãy chọn JWT hoặc API Key over HTTPS

Bài viết liên quan

Đang cập nhật...