Hi. Xin chào mọi người, và đây là sự trở lại của cháu trai bà bán kem đánh răng 🤭
Mới đây mình mới tiếp thu được một chiêu thức khá hay và muốn 'enjoy cái moment' này cho mọi ngừi 🫠, thôi toy ko mún nói nhiều nữa, mn cứ đọc đi rồi sẽ hiểu, ko hiểu mặc kệ 🙂))
First thing first
Mình là Đờ đến từ đền lờ, developer với một tay nghề non trẻ, từ PHP, Nodejs, Reactjs và gần đây nhất là Typescript, cái gì mình cũng chọt vào 1 chút, dạo này thì mình focus hơn vào backend.
Again, mình là một chú developer thích viết lách, cũng đã khá lâu kể từ bài blog cuối cùng, hôm nay mình sẽ khởi động lại bằng topic “Lưu access token ở đâu thì hợp lý?”
Các phương thức authentication
Chúng ta đảo qua một chút về các phương thức xác thực trước:
Basic Authentication: Dùng base64 để encode tên người dùng và mật khẩu, và đưa nó vào header:
GET /api/example HTTP/1.1
Host: example.com
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
Accept: application/json
Phương thức này quá cũ kĩ và không bảo mật, vì bạn có thể dễ dàng decrypt lại chuỗi authorization bên trên.
Session & Cookie: Lưu thông tin session đăng nhập vào cookie của trình duyệt phía client, và lưu session phía memory của server. Nhược điểm chính là không scale được, cũng như không support được mobile app, vì chúng đâu có cookie 😗.
GET /api/example HTTP/1.1
Host: example.com
Cookie: session_id=abc123def456; user_id=789
Accept: application/json
Token-based: Sử dụng token để xác thực người dùng. Thông thường, khi người dùng đăng nhập thành công, hệ thống sẽ generate và trả về 1 token, mà phổ biến nhất chính là JWT.
GET /api/example HTTP/1.1
Host: example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRmF
sZSIsImlhdCI6MTUxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Accept: application/json
Với phương thức session & cookie, thì dĩ nhiên trình duyệt auto nhận authentication data và lưu vào cookie rồi, vậy còn JWT chúng ta lưu ở đâu?
JWT Authentication
JWT (JSON Web Token) như tên gọi, là token được lưu dưới dạng text, có thể parse qua JSON và chứa những thông tin liên quan để authenticate.
Mổ xẻ JWT
JWT gồm 3 phần: Header, Payload và Signature.
Header: Chứa thông tin về loại token và thuật toán mã hóa được sử dụng.
Payload: Chứa các thông tin liên quan đến user (ví dụ như id, tên, email, quyền hạn, thời gian hết hạn token, etc.).
Signature: Là một chuỗi được mã hóa bằng thuật toán chữ ký số (signature algorithm) và được tạo ra bằng cách kết hợp header, payload và secret key (khóa bí mật) mà chỉ server biết.
Chúng ta có thể gán thêm information cần thiết vào phần payload bên cạnh những “Claim” cơ bản được định nghĩa trong RFC 7519.
Muốn giấu thông tin khỏi JWT thì làm sao?
Bạn có thể sẽ thắc mắc là một vài thông tin như username, email có thể bị lộ từ việc decode JWT. Vì nó chỉ được Encode chứ không được Encrypt.
Để giấu thông tin khỏi JWT, bạn có thể mã hóa thông tin trước khi đưa vào JWT. Tuy nhiên, JWT thường được sử dụng để xác thực người dùng và không được mã hóa. Thay vào đó, nó được “sign” (ký) để đảm bảo tính toàn vẹn của dữ liệu. (Tham khảo bài viết về HTTP và HTTPS để tìm hiểu thêm về chữ ký nhé).
Bạn cũng có thể tham khảo thêm chi tiết tại RFC-7516 về việc mã hóa các trường trong JWT. Tuy nhiên, hãy nhớ rằng việc mã hóa trong JWT có thể làm tăng độ phức tạp và làm chậm việc xác thực người dùng.
Tham khảo về encrypt - decrypt cho ai quan tâm.
Vậy bây giờ lưu access token ở đâu?
Chúng ta có thể bỏ qua luôn session storage và cả application state, vì chúng quá mỏng manh yếu đuối, đóng tab hoặc ctrl + F5 cái là đi mất rồi.
Local Storage
Đây chính là 1 nơi nguy hiểm, bởi vì local storage có thể được truy cập từ bất kỳ script nào chạy trên cùng 1 domain (và session storage cũng thế nha).
Chúng ta gọi đó là lỗ hổng XSS, tức Cross-Site Scripting, kẻ xấu chỉ cần inject script vào target là đã có thể lấy được token.
Điểm chí mạng thứ 2 đó chính là local storage không có thời gian expire, nó sẽ còn sống mãi. Bình thường con người sống lâu thì mừng, còn token sống lâu thì lại là vấn đề 🤗
Cookie
Một cách khác chính là lưu vào cookie, mặc dù cookie vẫn có thể truy cập từ script trong cùng domain bằng cách gọi document.cookie .
Nhưng nó vẫn an toàn hơn localstorage, tại sao?
Thứ nhất, cookie có thời gian expiration, không sống thọ như local storage
.
Thứ 2, cookie có 1 flag gọi là httpOnly. Flag này sẽ giúp ngăn chặn cookie truy cập bởi client side.
HttpOnly flag in cookie settings
Lúc này khi các bạn truy cập document.cookie
nó chỉ trả về empty mà thôi.
Khoan!
Nếu bật HttpOnly thì cookie cũng sẽ bị dính chưởng lời nguyền CSRF (Cross-Site Request Forgery)
CSRF tức là request được gửi đi sẽ không phải gửi từ domain đã đăng nhập, mà là 1 domain lạ nào đó.
Ví dụ bạn đã đăng nhập facebook, sau đó nhờ bấm vào 1 link "nóng bỏng" share hàng nào đó trên bảng tin, bạn truy cập trang giả mạo facebook-aa.com. Trang này sẽ inject 1 đoạn script gửi request tới facebook.com/change-password?newPassword=ABC
, request này sẽ auto được lấy cookie của facebook.com
từ browser.
Để thoát khỏi CSRF, chúng ta cần thêm 2 flag nữa, chính là SameSite=Strict
, flag này chỉ cho phép gửi cookie đi trên cùng 1 domain đã set mà thôi.
Mà để bật SameSite strict, thì chúng ta cũng phải bật luôn flag Secure, flag này chỉ cho phép gửi cookie đi qua kết nối HTTPS. Xem thêm tại MDN - Samesite cookie flag
Nhược điểm của việc đặt same-site chính là không bật được tính năng cross-site như social login các thứ cần kết nối tới 1 service 3rd party.
Tất cả biện pháp trên cũng không đảm bảo 100% tránh được CSRF, cho nên chúng ta vẫn cần implement 1 cơ chế nào đó để tránh CSRF phía server.
Tổng kết lại 1 chút nào:
Implement với Cookie
Cách set HttpOnly phía server
Tất nhiên rồi, khi bạn bật mode HttpOnly, thì client sẽ không có quyền truy cập vào cookie, do đó chúng ta phải thực hiện việc set cookie thông qua backend.
Với nodejs, chuyện này khá dễ:
const app = express();
app.get("/", (req, res) => {
const token = jwt.sign(payload, SECRET);
res.cookie("accessToken", token, {
maxAge: 60 * 60 * 1000, // 1 hour
httpOnly: true,
secure: true,
sameSite: "strict",
});
res.send("Cookie set!");
});
### Client gửi token đi thế nào?
Bạn có thể tự hỏi, làm sao client có thể lấy được token, trong khi cookie đã không còn xuất hiện trong con mắt của client script nữa?
Đáp án là header này, hãy thêm nó vào middleware gửi request phía client.
Access-Control-Allow-Credentials: true
Khi header này được set true, trình duyệt sẽ tự động gửi bất kỳ cookie nào có thuộc tính HttpOnly mà có domain là domain đang gửi request tới. (Tham khảo tại MDN)
Với axios, chúng ta có thể enable bằng cách set withCredentials: true
:
axios.get("https://api.domainA.com", { withCredentials: true });
Nhưng ở đây lại đẻ ra 1 của nợ nữa, đó chính là CORS, ví dụ bạn đăng nhập và lưu cookie ở auth.domainA.com
, nhưng lại cần gửi request tới api.domainA.com
thì dễ bị chặn lắm.
Thì chúng ta tiếp tục phải enable CORS phía server.
Enable CORS
CORS - Cross-Origin Resource Sharing, là một tính năng bảo mật của trình duyệt giúp ngăn chặn hoặc giới hạn những resource được cho phép theo origin (field này trình duyệt tự thêm vô).
Trình duyệt sẽ tự động thêm field origin vào
Flow nôm na là nếu server đánh dấu origin là được phép (thông qua header), thì trình duyệt sẽ gửi request đi, còn nếu server không cho, thì browser sẽ chặn request lại.
Do đó mà dev có thể bypass bằng 1 extension của trình duyệt 😜, tuy nhiên ai lại bắt user đi cài extension bao giờ, do đó ta phải implement nó.
Nodejs:
const cors = require("cors");
app.use(
cors({
origin: ["https://domainA.com", "https://domainB.com/"],
})
);
Chống CSRF
Kĩ thuật này khá đơn giản, với mỗi form request, chúng ta generate 1 csrf token dựa vào session hiện tại, client gán nó vào 1 hidden input, và server sẽ xử lý field này bằng 1 thuật toán nào đó, nếu hợp lệ thì cho qua.
Ví dụ code ở client:
<form action="/submit-form" method="POST">
<input type="hidden" name="_csrf" value="{{ csrfToken }}" />
<label for="username">Username:</label>
<input type="text" id="username" name="username" />
<label for="password">Password:</label>
<input type="password" id="password" name="password" />
<button type="submit">Submit</button>
</form>
Hầu hết các framework đều support CSRF rồi, xưa mình code laravel PHP thì nó support middleware có sẵn dễ như ăn bánh.
Tổng kết
Hoooo, mình không nghĩ là bài này lại dài tới vậy, coppy mà mỏi hết cả tay 🙂)).
Hi vọng sau bài này bạn sẽ có 1 cái nhìn tổng quan về JWT authenticaion và những vấn đề bên cạnh như CORS và bảo mật các request với CSRF.
Link tham khảo
Happy coding!