Thiết kế db cho authentication

Thiết kế db cho  authentication

Phần 1 : Cách lưu trữ password.

Nguồn: http://www.vertabelo.com/blog/technical-articles/how-to-store-authentication-data-in-a-database-part-1

Nhu cầu đặt ra là làm cách nào để lưu trữ dữ liệu authentication một cách bảo mật khi mà đây là những dữ liệu khá nhạy cảm của user. Rõ ràng là ta không thể lưu password dưới dạng plain text được vì giả sử hệ thống bị tấn công thì sẽ làm lộ ra tài khoản của users. Ngay cả việc mã hóa 2 chiều cũng không được dùng vì vẫn có khả năng bị giải mã.

Thay vào đó thì ta nên chọn những thuật toán mã hóa 1 chiều (hash) cho việc mã hóa password. Hai tiêu chí cần phải có cho một thuật toán mã hóa password bao gồm :

  • Với 2 đầu vào khác nhau không bao giờ cho ra cùng 1 kết quả hash
  • Khó khăn trong việc giải mã (chúng ta không cần phải giải mã password để làm gì cả)

Một số thuật toán thường được dùng như SHA-2, SHA-3. Ngoài ra thì còn MD5 và SHA-1. Nhưng MD5 và SHA-1 đã được report là có khả năng vi phạm 2 tiêu chí trên nên ta không nên sử dụng.

Nhưng khi user login thì ta phải so sánh password để chứng thực. Vậy làm cách nào so sánh được password khi mà ta không lưu trữ password dưới dạng plain text hoặc một chuỗi có thể giải mã được? Ta có thể thực hiện như sau :

Đến lúc này thì mọi thứ có thể hoạt động ổn định. Nhưng có 1 vấn đề nảy sinh, đó là nếu có nhiều users đặt cùng 1 password thì hash code sẽ generate ra giống nhau. Nên giả sử hacker hack được password của 1 user thì cũng sẽ có khả năng tấn công những users khác có cùng password. Một vấn đề khác nữa đó là user thường đặt những mật khẩu ngắn cho dễ nhớ nên rất dễ bị tấn công bằng brute-force. Để ngăn chặn những vấn đề đó thì có 1 kỹ thuật gọi là Salting. Salt là một chuỗi string dài và được tạo ra một cách ngẫu nhiên (không nên dùng username làm salt). Chuỗi salt này sẽ được cộng vào password trước khi hash. Khi đó với 2 passwords giống nhau sẽ cho ra kết quả hash khác nhau do được cộng với 2 chuỗi salt khác nhau. Ngoài ra, salt còn giúp biến một password ngắn thành password dài làm cho việc tấn công bằng brute-force trở nên khó khăn hơn rất nhiều.

Vậy tổng kết lại ta sẽ có thiết kế DB như hình dưới đây.

  • password : dùng để lưu trữ password đã được hash của user.
  • password_salt : dùng để lưu trữ chuỗi salt đã được apply cho password của user. Cột này có thể null vì ta có thể bỏ qua việc salting trong quá trình development.
  • password_hash_algorithm : dùng để lưu thuật toán dùng để hash. Cột này sẽ giúp ta thuận lợi hơn khi mà trong quá trình development ta có thể bỏ qua việc hashing, hoặc khi migrate từ một hệ thống cũ nào đó và phải giữ lại password đã được hash của user (khi user đăng nhập lại trên hệ thống mới thì sẽ force user update lại password của họ và lưu trữ lại theo thuật toán hash của hệ thống mới). Và lý do thứ 3 là tạo điều kiện thuận lợi cho việc thay đổi thuật toán hash khi mà thuật toán hash bị outdated.

Phần 2 : Xác nhận bằng email và khôi phục password.

Nguồn : http://www.vertabelo.com/blog/technical-articles/how-to-store-authentication-data-in-a-database-part-2-email-confirmation-and-recovering-passwords

Đối với một hệ thống, sau khi user đăng kí thì hệ thống nên gửi đến user một email để xác nhận quá trình đăng kí. Tại sao lại phải lằng nhằng như vậy ? Hai trong số những lý do được đưa ra đó là :

  • Vì email là kênh giao tiếp chính giữa hệ thống và user nên ta cần đảm bảo email được cung cấp bởi user là email thật và user thật sự có quyền truy cập vào email đó.
  • Đối với những hệ thống mà email được gắn với duy nhất 1 tài khoản thì xác thực bằng email sẽ ngăn chặn hành vi dùng email của người khác để đăng ký tài khoản.

Vậy việc xác thực bằng email được thực hiện như thế nào?

Sau khi user đăng ký, hệ thống sẽ phát sinh một chuỗi string dài, ngẫu nhiên và duy nhất đối với từng tài khoản (được gọi là activation token). Chuỗi token này sẽ được lưu xuống DB và đồng thời được gửi kèm với một đường link xác nhận đến email được cung cấp bởi user. Ví dụ :

https://www.mysite.com/activate?code=HbhUPq3i8w90Kdv4QtwiT2cVk3YoLq

Khi user click vào link trên thì hệ thống sẽ xác nhận rằng user đã đăng ký thành công và tiến hành update activation token của tài khoản đó về null.

Nếu trong 1 khoản thời gian nào đó (ví dụ 72 giờ), user không click vào link xác thực tài khoản thì hệ thống sẽ tự động xóa thông tin đăng ký của user đó. Việc này có thể thực hiện bằng 1 cron job chạy định kỳ hoặc thực hiện khi user đó thực hiện 1 request nào đó.

Ngoài ra hệ thống cũng nên chặn email từ một số trang fake inbox (fakeinbox.com, mailinator.com, maildrop.cc, ….) nhằm đảm bảo những email được cung cấp bởi user không phải là những email dùng một lần.

Câu hỏi được đặt ra là ta sẽ thiết kế DB như thế nào ?

Hình bên dưới sẽ trả lời câu hỏi đó :

So với cách thiết kế cũ thì ta sẽ thấy có một số sự khác biệt. Bảng user_account có thêm một số trường mới (registration_timeemail_confirmation_tokenuser_account_status_idpassword_reminder_tokenpassword_reminder_expire) và 1 bảng mới đó là user_account_status. Hai trường password_reminder_tokenpassword_reminder_expire sẽ được đề cập trong phần tiếp theo.

  • registration_time : dùng để lưu thời gian đăng ký của user để xác định xác nhận của user có bị quá hạn hay chưa (e.g. > 72 giờ)
  • email_confirmation_token : dùng để lưu trữ chuỗi activation token.
  • user_account_status_id : status của tài khoản (e.g. EMAIL_CONFIRMED, EMAIL_NON_CONFIRMED, …)

Khôi phục password là việc cho phép user update lại mật khẩu của họ trong trường hợp user quên password để đăng nhập vào hệ thống.

Cũng tương tự như việc xác thực bằng email, một password reminder token sẽ được generate ra, lưu trữ vào DB và gửi tới email của user. Token này cũng sẽ có 1 khoảng thời gian có hiệu lực giới hạn, khoảng thời gian này không nên quá dài để tránh brute-force (e.g. 30 phút). Đường link được gửi tới email của user có dạng :

https://www.mysite.com/resetPassword?code=b6dcQVNw6REQ8rUs6TyKYtU48plQ9o

Khi user click link này, hệ thống sẽ kiểm tra hiệu lực của token. Nếu hợp lệ thì user sẽ được update lại mật khẩu mới của họ.

Đối với việc khôi phục mật khẩu này thì ta hãy để ý lại 2 trường chưa được đề cập trong hình ở bên trên :

  • password_reminder_token : dùng để lưu trữ token dành cho việc khôi phục mật khẩu.
  • password_reminder_expire : thời điểm mà token sẽ bị hết hiệu lực.

P/s : Lưu ý : Phần in nghiêng là ý kiến riêng của dịch giả, không phải từ nguồn.


Phần 3: Đăng nhập bằng Facebook, Google, Twitter, …

Nguồn : http://www.vertabelo.com/blog/technical-articles/how-to-store-authentication-data-in-a-database-part-3-logging-in-with-external-services

Đối với nhiều ứng dụng hiện nay thì xu hướng đang là tích hợp việc đăng nhập bằng nhiều kênh khác nhau như Facebook, Google, Twitter, … bên cạnh chức năng đăng nhập mặc định của hệ thống. Việc đăng nhập thông qua kênh trung gian này có nhiều điểm thuận lợi :

  • Việc xử lý thông tin đăng nhập nhạy cảm được giao cho bên trung gian (tin cậy) xử lý.
  • Tạo sự thuận tiện cho user trong việc đăng nhập, do họ không cần phải nhập email, password, xác thực qua email,… đối với việc đăng nhập thông thường.
  • Hệ thống có thể biết thêm được nhiều thông tin của user hơn mà không cần user phải cung cấp (chỉ cần user đồng ý cho bên trung gian cung cấp thông tin).

Hầu hết các trang trung gian hiện nay đều dùng giao thức OAuth 2.0 cho việc đăng nhập (ngoại trừ Twitter vẫn đang dùng OAuth 1.0). Với giao thức này sẽ cho phép user tùy chọn những thông tin mà bên trung gian sẽ cung cấp cho hệ thống (e.g. username, email, fullname, first name, last name, gender, …). Chúng ta sẽ có 1 phần riêng cho giao thức OAuth này sau, để trả lời những câu hỏi như : OAuth hoạt động như thế nào ? Bằng cách nào OAuth có thể bảo mật được thông tin đăng nhập của user ? Bằng cách nào hệ thống có thể lấy được những thông tin mà user cho phép bên trung gian cung cấp ? ….

Bây giờ ta sẽ tập trung vào cách thiết kế DB cho việc lưu trữ thông tin đăng nhập của user kể cả việc đăng nhập trực tiếp và thông qua kênh trung gian.

Như cách thiết kế DB bên trên thì ta nhận thấy một số trường liên quan đến việc đăng nhập trực tiếp sẽ trở nên thừa nếu user đăng nhập thông qua kênh trung gian (e.g. passwordpassword_saltemail_confirmation_tokenpassword_reminder_token, …). Còn một số trường sẽ là chung cho cả 2 cách đăng nhập (e.g. first_namelast_nameemail, …). Do đó ta sẽ tiến hành tách bảng user_account ra thành 2 bảng là user_account và user_profile. Trong đó user_account sẽ chứa những thông tin dành cho kênh đăng nhập trực tiếp và user_profile sẽ lưu trữ thông tin cá nhân của user.

Vậy thì đăng nhập thông qua kênh trung gian ta cần lưu trữ những gì ?

Giao thức OAuth sẽ trả về cho hệ thống một Id, để định danh một user. Nên ta có thể thêm trường facebook_id, google_id, twitter_id vào bảng user_profile để lưu trữ. Nhưng ở đây ta sẽ không thực hiện việc đó bởi vì sẽ khó khăn trong việc mở rộng nếu như sau này hệ thống muốn thông qua 1 kênh trung gian nào khác nữa (điều này không là vấn đề đối với NoSQL).

Cách ta sẽ hiện thực ở đây sẽ là tạo những bảng riêng dành cho các kênh đăng nhập trung gian : facebook_account, google_account, twitter_account, …. Những bảng đó sẽ có dạng như sau :

Trong đó :

  • user_profile_id : là khóa ngoại tới bảng user_profile
  • facebook_id : là id được trả về từ facebook

Và đây là kết quả cuối cùng :


Phần 4 : OAuth 2.0

Nguồn :

Mục đích cuối cùng của OAuth là để nhận được access_token từ đó với mỗi request tới server ta đều gửi kèm access_token trong HTTP header cho việc chứng thực. Luôn đảm bảo rằng mọi request đều được thực hiện thông qua HTTPS nhằm ngăn chặn việc bị can thiệp để đánh cắp và thay đổi dữ liệu.

Một request đến server sau khi có access_token sẽ có dạng :

curl -H "Authorization: Bearer RsT5OjbzRn430zqMLgV3Ia" \
https://api.oauth2server.com/1/me

Trong đó chuỗi RsT5OjbzRn430zqMLgV3Iachính là access_token nhận được về từ bên dịch vụ trung gian.

Luồng thực hiện của OAuth 2.0 :

  • client_id : id mà service trung gian (Facebook, Google, …) cung cấp cho ứng dụng đăng ký.
  • client_secret : một chuỗi token mà service trung gian cung cấp cho ứng dụng đăng ký (ta có thể xem nó như password cho việc request authorization_code). (KHÔNG BAO GIỜ được để mất client_secret. E.g. Không để client_secret ở client side code)
  • redirect_uri : URL mà ta muốn redirect tới sau khi user đăng nhập thông qua dịch vụ trung gian (Facebook, Google, …).
  • scope : danh sách các quyền mà user đồng ý cho dịch vụ trung gian cung cấp cho hệ thống, các quyền được ngăn cách bằng dấu phẩy.
  • grant_type : có 3 loại authorization_code, password, client_credentials nhằm mục đích khai báo cách thức để lấy access_token. Chi tiết từng loại sẽ được định nghĩa bên dưới.
  • authorization_code : một chuỗi token nhận được về từ dịch vụ trung gian và được dùng như một phần để yêu cầu access_token từ bên trung gian (trừ trường hợp chọn grant_type là client_credentials)
  • access_token : kết quả cuối cùng của OAuth, được dùng để gửi kèm trong header của mỗi request đến server để chứng thực.

Trong sơ đồ ở trên, bỏ qua việc user request trang login của dịch vụ trung gian ở bước đầu, thì ta thấy OAuth 2.0 sẽ có 3 luồng công việc chính theo thứ tự sau :

1. Authorization : Ở bước này, mục đích chính là thông báo với dịch vụ trung gian rằng dưới danh nghĩa của user sẽ cung cấp những thông tin gì cho ứng dụng nào. Kết quả trả về từ dịch vụ trung gian sẽ là 1 authorization_codedùng cho việc đổi lấy access_token sau này.

Một request authorization sẽ có dạng :

https://oauth2server.com/auth?response_type=code&
client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&scope=photos&state=1234zyx

Trong đó :

  • response_type : (= code ) thể hiện rằng request yêu cầu nhận về authorization_code. (= token nếu muốn nhận về access_token trực tiếp không qua bước 3 bên dưới, và cách này KHÔNG ĐƯỢC KHUYẾN KHÍCH)
  • state : một chuỗi string bất kỳ. Chuỗi này sẽ được gửi trả về khi response và hệ thống / ứng dụng nên kiểm tra state lúc gửi đi và lúc nhận về có khớp hay không nhằm đảm bảo response trả về là cho đúng request gửi đi.

2. Redirect tới URL định sẵn : URL này khá là quan trọng khi mà dịch vụ bên thứ 3 sẽ kiểm tra xem URL có khớp với URL được khai báo cho ứng dụng tương ứng hay không, và dùng để trả lại authorization_code cho đúng ứng dụng chứ không phải một bên nào khác. Nên URL này phải khớp với URL đã đăng ký với dịch vụ trung gian.

Một URL của ứng dụng sẽ có dạng :

fb00000000://authorize?code=AUTHORIZATION_CODE&state=1234zyx

3. Lấy access token : Khi đã có được authorization_code thì bước cuối cùng sẽ là request lấy access_tokenauthorization_code sẽ được gửi kèm theo trong request đến dịch vụ trung gian để bên đó chứng thực việc request access_token được thực hiện từ bên tin cậy.

Một request lấy access token sẽ có dạng :

POST https://api.oauth2server.com/token
grant_type=authorization_code&
code=AUTH_CODE_HERE&
redirect_uri=REDIRECT_URI&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET

Nếu thành công kết quả trả về sẽ là :

{
"access_token":"RsT5OjbzRn430zqMLgV3Ia",
"expires_in":3600
}

Nếu không thành công thì kết quả sẽ là :

{
"error":"invalid_request"
}

Grant type :

Có 3 dạng grant_type chính :

  • authentication_code : thông báo cách lấy access_token để access vào data của user bằng hình thức gửi kèm authentication_code
POST https://api.oauth2server.com/token
grant_type=authorization_code&
code=AUTH_CODE_HERE&
redirect_uri=REDIRECT_URI&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET
  • password : thông báo cách lấy access_token để access vào data của user bằng hình thức gửi kèm username và password (thường được dùng trong trường hợp hệ thống và dịch vụ chứng thực cùng thuộc 1 hệ thống)
POST https://api.oauth2server.com/token
grant_type=password&
username=USERNAME&
password=PASSWORD&
client_id=CLIENT_ID
  • client_credentials : Trong một số trường hợp hệ thống / ứng dụng cần thay đổi thông tin của nó. Khi đó nó sẽ gửi 1 request tới dịch vụ trung gian để lấy với grant_type này thông báo lấy access_token để access vào data của hệ thống / ứng dụng đã đăng ký.
POST https://api.oauth2server.com/token
grant_type=client_credentials&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET

PKCE — Proof Key for Code Exchange:

Phần này nằm ngoài scope đối với những ai chỉ quan tâm đến cách hiện thực login thông qua dịch vụ trung gian vì lý do là nhiều khả năng Facebook / Google SDK đã xử lý việc này rồi. Nên nếu ai quan tâm đến cách OAuth hoạt động thì có thể đọc tiếp để hiểu rõ hơn vấn đề và cách giải quyết dưới đây.

Có 1 vấn đề nảy sinh trong trường hợp đăng nhập bằng native app. Đó là giả sử có 2 ứng dụng được cài trên cùng 1 điện thoại, cùng đăng kí 1 redirect URL thì khi đó authorization_code có khả năng sẽ bị gửi đến sai ứng dụng (bước số 4 bên dưới).

Để giải quyết vấn đề này thì có 1 cách đó là cả client và server authentication (dịch vụ trung gian) đều phải hiện thực PKCE (Proof Key for Code Exchange). Và các bước thực hiện là theo thứ tự bên dưới :




Với cách này thì authentication_code có thể bị lộ nhưng vẫn đảm bảo access_token được gửi đến đúng địa chỉ.