Hướng dẫn auth0 nodejs - auth0 nodejs
Tổng quanỨng dụng NodeJS sử dụng JWT (Json Web Token) rất phù hợp cho các ứng dụng cho phép người dùng có thể xác thực từ nhiều thiết bị khác nhau (website, mobile app,...). Show
Ở bài viết này, mình sẽ tạo ra một REST API cho phép người dùng đăng kí tài khoản, đăng nhập và lấy lại mã truy cập khi mã truy cập này hết hạn. Xác thực người dùng sử dụng JWTCơ chế xác thực bằng JWT
Phát sinh ra access token và refresh tokenMột access token và một refresh token (nó là gì mình sẽ nói rõ ở phần dưới) có thể được sinh ra khi gọi thành công đến một API nào đó (chẳng hạn /auth/login) và trả về cho client đồng thời lưu refresh token vào database (hoặc một nơi nào đó).access token và một refresh token (nó là gì mình sẽ nói rõ ở phần dưới) có thể được sinh ra khi gọi thành công đến một API nào đó (chẳng hạn /auth/login) và trả về cho client đồng thời lưu refresh token vào database (hoặc một nơi nào đó). Một access token có thời gian tồn tại nhất định, bạn có thể cài đặt khoảng thời gian này cho nó.access token có thời gian tồn tại nhất định, bạn có thể cài đặt khoảng thời gian này cho nó. Cơ chế refreshing access tokenNhư đã đề cập ở trên thì refresh token là gì, nó dùng để làm gì, mình sẽ giải thích ở dưới đây.refresh token là gì, nó dùng để làm gì, mình sẽ giải thích ở dưới đây. Nếu một ngày đẹp trời (mà cũng có thể không đẹp ) access token của bạn nó bị rơi vào tay của mấy chú hacker, ở đây sẽ phát sinh ra hai vấn đề: ) access token của bạn nó bị rơi vào tay của mấy chú hacker, ở đây sẽ phát sinh ra hai vấn đề:
Khi access token bị lộ, người dùng chỉ cần liên hệ với admin của ứng dung/hệ thống đó để yêu cầu xóa refresh token này đi, người dùng chỉ cần đăng nhập lại và có thể sử dụng tiếp (và lại tiếp tục bị lộ ).access token bị lộ, người dùng chỉ cần liên hệ với admin của ứng dung/hệ thống đó để yêu cầu xóa refresh token này đi, người dùng chỉ cần đăng nhập lại và có thể sử dụng tiếp (và lại tiếp tục bị lộ ).Tóm lại, cơ chế sẽ như sau:
Một refresh token có thể có thời gian tồn tại hoặc không. Nếu không có thời gian tồn tại thì người dùng chỉ cần đăng nhập 1 lần duy nhất và có thể sử dụng app đó từ đời này qua đời khác mà không cần đăng nhập lại (nghe vui chứ ). Hoặc nếu có thời gian tồn tại thì nên để lâu lâu một chút (khoảng 30, 45,... ngày), sau khoảng thời gian đó mới bắt người dùng đăng nhập lại.refresh token có thể có thời gian tồn tại hoặc không. Nếu không có thời gian tồn tại thì người dùng chỉ cần đăng nhập 1 lần duy nhất và có thể sử dụng app đó từ đời này qua đời khác mà không cần đăng nhập lại (nghe vui chứ ). Hoặc nếu có thời gian tồn tại thì nên để lâu lâu một chút (khoảng 30, 45,... ngày), sau khoảng thời gian đó mới bắt người dùng đăng nhập lại.Cách cài đặtCác dependencies cần thiết
Cấu trúc project
Khởi tạo server, khai báo các biến, các router tương ứngỞ file ./app.js./app.js
Ở đây mình khởi tạo server, định nghĩa ra 2 router cơ bản: /auth và /users, các API về authentication và user mình sẽ thêm tiếp vào 2 file ./src/auth/auth.routes và ./src/users/users.routes/auth và /users, các API về authentication và user mình sẽ thêm tiếp vào 2 file ./src/auth/auth.routes và ./src/users/users.routes Đăng kí tài khoảnTrong phần đăng kí tài khoản này, ta yêu cầu client gửi lên hai tham số username và password, ngoài ra cũng có thể thêm các tham số khác như số điện thoại, họ tên,..username và password, ngoài ra cũng có thể thêm các tham số khác như số điện thoại, họ tên,.. Ở file ./src/auth/auth.routes ta khai báo API đăng kí tài khoản như sau:./src/auth/auth.routes ta khai báo API đăng kí tài khoản như sau:
Khi đó API của ta sẽ có dạng: http://example.com/auth/registerhttp://example.com/auth/register Tương ứng với đó, trong ./src/auth/auth.controllers ta xử lý cho API đó như sau:./src/auth/auth.controllers ta xử lý cho API đó như sau:
Ban đầu ta sẽ kiểm tra xem username đó đã tồn tại trong database chưa, nếu chưa có thì ta mới thêm user đó. Ở đây mình dùng bcrypt để hash password và sau đó mới lưu nó vào database. Hai hàm tương ứng trong ./src/auth/users.models.js (getUser() và createUser()) sẽ như sau:./src/auth/users.models.js (getUser() và createUser()) sẽ như sau:
Ở đây mình dùng file .json để lưu data, bạn có thể dùng bất kì một loại database khác (MSSQL, DynamoDB, MongoDB,...) để lưu data, phần xử lý code đó bạn thay vào trong hai hàm này nhé Như vậy là phần code đã xong, giờ dùng postman gọi API và xem kết quả nào: Và cùng xem data đã được thêm vào database chưa nhé: Có rồi ha, mật khẩu cũng đã được hashed đơn giản mà nhỉ postman gọi API và xem kết quả nào: Và cùng xem data đã được thêm vào database chưa nhé: Có rồi ha, mật khẩu cũng đã được hashed đơn giản mà nhỉ Đăng nhậpTương tự như phần đăng ký tài khoản, phần đăng nhập sẽ như sau, ta yêu cầu client gửi lên hai tham số username và passwordusername và password ./src/auth/auth.routes.js
./src/auth/auth.controllers.js
Ban đầu ta cũng kiểm tra username đó đã tồn tại chưa, nếu chưa có thì trả về lỗi ngay. Sau đó ta tiến hành compare giữa password người dùng nhập vào và password của user đó đã được lưu trong database bằng hàm bcrypt.CompareSync(). Nếu đúng thì thực hiện tiếp.password người dùng nhập vào và password của user đó đã được lưu trong database bằng hàm bcrypt.CompareSync(). Nếu đúng thì thực hiện tiếp. Ta khai báo hai biến accessTokenLife (thời gian tồn tại của access token) và accessTokenSecret (khóa bí mật của access token - phải được bảo mật tuyệt đối (như kiểu mật khẩu tài khoản ngân hàng của bạn ý )). Ở đây mình lưu nó trong file .envaccessTokenLife (thời gian tồn tại của access token) và accessTokenSecret (khóa bí mật của access token - phải được bảo mật tuyệt đối (như kiểu mật khẩu tài khoản ngân hàng của bạn ý )). Ở đây mình lưu nó trong file .env
Tiếp theo ta khai báo biến dataForAccessToken để chỉ định những gì sẽ được lưu trong access token, ở đây mình chỉ lưu username (nên lưu càng ít càng tốt, do mình chọn username là khóa chính nên mình lưu username, bạn cũng có thể lưu id của user)dataForAccessToken để chỉ định những gì sẽ được lưu trong access token, ở đây mình chỉ lưu username (nên lưu càng ít càng tốt, do mình chọn username là khóa chính nên mình lưu username, bạn cũng có thể lưu id của user)
Ta sẽ tạo ra một access tokenaccess token
Trong đó hàm generateToken mình khai báo ở ./src/auth/auth.methods.jsgenerateToken mình khai báo ở ./src/auth/auth.methods.js 0Mình sử dụng package jsonwebtoken, trong đó có hai hàm cơ bản là sign() và verify nhưng hai hàm này là hàm callback, để có thể export giá trị trả về của hàm này khá phức tạp nên mình dùng package util để biến hàm callback này trả về một Promise.jsonwebtoken, trong đó có hai hàm cơ bản là sign() và verify nhưng hai hàm này là hàm callback, để có thể export giá trị trả về của hàm này khá phức tạp nên mình dùng package util để biến hàm callback này trả về một Promise. 1Sau đó ta gọi hàm sign đã được "chỉnh sửa" để tạo access tokensign đã được "chỉnh sửa" để tạo access token 2Mặc dù là nó đã trả về một Promise, tuy nhiên mình lại không thích viết kiểu .then() nên mình dùng async/await để lấy kết quả và trả về một access token.Promise, tuy nhiên mình lại không thích viết kiểu .then() nên mình dùng async/await để lấy kết quả và trả về một access token. Bạn có thể tham khảo thêm về callback, promise, async/await ở đây: JavaScript: từ Callbacks đến Promises và Async/Awaitcallback, promise, async/await ở đây: JavaScript: từ Callbacks đến Promises và Async/Await Sau đó nữa, ta sẽ phát sinh ra refresh tokenrefresh token 3Ta dùng hàm generate() của package rand-token để phát sinh ra một token với kích thước tùy chọn. Mỗi tài khoản mình chỉ phát sinh một refresh token, nếu nó đã tồn tại (đã được phát sinh ở lần đăng nhập trước) thì không phát sinh lại, bạn cũng có thể mỗi lần đăng nhập phát sinh ra một refresh token mới toanh. Sau đó ta sẽ update refresh token đó cho user đó ở ./src/users/users.models như sau:generate() của package rand-token để phát sinh ra một token với kích thước tùy chọn. Mỗi tài khoản mình chỉ phát sinh một refresh token, nếu nó đã tồn tại (đã được phát sinh ở lần đăng nhập trước) thì không phát sinh lại, bạn cũng có thể mỗi lần đăng nhập phát sinh ra một refresh token mới toanh. Sau đó ta sẽ update refresh token đó cho user đó ở ./src/users/users.models như sau: 4Ngoài ra bạn cũng có thể sử dụng hàm generateToken() ở trên để phát sinh ra một refresh token nếu muốn nó có thời gian tồn tại như access token (nếu dùng thì nên đặt thời gian này lâu lâu một xíu).generateToken() ở trên để phát sinh ra một refresh token nếu muốn nó có thời gian tồn tại như access token (nếu dùng thì nên đặt thời gian này lâu lâu một xíu). Cuối cùng ta trả về những thứ đó cho client 5Giờ cùng đi xem kết quả sau một khoảng thời gian đọc code hoa cả mắt nhé Giờ có access token và refresh token rồi, nhiệm vụ của bên front-end là lưu nó vào một chỗ nào đó. Giờ có access token và refresh token rồi, nhiệm vụ của bên front-end là lưu nó vào một chỗ nào đó.Phát sinh một access token khi cái cũ hết hạnỞ API này, mình sẽ yêu cầu bên front-end gửi access token ở trong headers và refresh token ở body của requestaccess token ở trong headers và refresh token ở body của request
6
7Ở file ./src/auth/auth.routes.js./src/auth/auth.routes.js 8Tương ứng ở ./src/auth/auth.controllers.js./src/auth/auth.controllers.js 9Ta khai báo hàm decodeToken() để decode access token cũ đã hết hạn trong ./src/auth/auth.methods.js như saudecodeToken() để decode access token cũ đã hết hạn trong ./src/auth/auth.methods.js như sau 0Ta vẫn dùng hàm verify() đã được "sửa" lại ở trên nhưng lần này có thêm thuộc tính ignoreExpiration: true mục đích để dù cho access token đó đã hết hạn nhưng vẫn cho verify. Bạn không được dùng package jwt-decode để decode access token vì nó có thể decode bất kì json token nào mà không cần biết khóa bí mật của access token, những kẻ phá hoại sẽ có thể tạo ra một token có phần payload giống như token của bạn và họ có thể phát sinh ra một access token dựa vào lỗ hổng này (mất công phải liên hệ với quản trị viên của app để xóa refresh token phải hônggg).verify() đã được "sửa" lại ở trên nhưng lần này có thêm thuộc tính ignoreExpiration: true mục đích để dù cho access token đó đã hết hạn nhưng vẫn cho verify. Bạn không được dùng package jwt-decode để decode access token vì nó có thể decode bất kì json token nào mà không cần biết khóa bí mật của access token, những kẻ phá hoại sẽ có thể tạo ra một token có phần payload giống như token của bạn và họ có thể phát sinh ra một access token dựa vào lỗ hổng này (mất công phải liên hệ với quản trị viên của app để xóa refresh token phải hônggg). Khi đã decode được rồi (access token hợp lệ), ta sẽ lấy toàn bộ thông tin của user dựa vào thông tin của user đã lưu vào access token đó. Sau đó kiểm tra xem refresh token mà client gửi lên có giống refresh token đã lưu trong database không, nếu có ta sẽ phát sinh ra một access token mới (giống như lúc login và trả về thôi)access token hợp lệ), ta sẽ lấy toàn bộ thông tin của user dựa vào thông tin của user đã lưu vào access token đó. Sau đó kiểm tra xem refresh token mà client gửi lên có giống refresh token đã lưu trong database không, nếu có ta sẽ phát sinh ra một access token mới (giống như lúc login và trả về thôi) Cùng xem kết quả nào: Phần headers như sau: Và phần body như sau: headers như sau: Và phần body như sau: Xử lý đối với các API yêu cầu xác thựcGiả sử ta có một API để lấy thông tin user ở ./src/users/users.routes.js sau khi đăng nhập như sau:./src/users/users.routes.js sau khi đăng nhập như sau: 1API này sẽ yêu cầu các tham số như sau:
6
3Ta sẽ cần có một middleware trung gian để xác thực có đúng client đã đăng nhập không, mình định nghĩa ở ./src/auth/auth.middlewares.js (Xem thêm middleware ở đây)middleware trung gian để xác thực có đúng client đã đăng nhập không, mình định nghĩa ở ./src/auth/auth.middlewares.js (Xem thêm middleware ở đây) 4Ta cũng sẽ cần access token được đính kèm trong phần headers sau đó ta sẽ verify token đó như sau:access token được đính kèm trong phần headers sau đó ta sẽ verify token đó như sau: Trong ./src/auth/auth.methods.js./src/auth/auth.methods.js 5Lần này access token phải còn thời gian tồn tại mới verify được nhé. Sau đó hàm này cũng trả về phần data được lưu trong đây. Cuối cùng ta sẽ kiểm tra lại một lần nữa user này có tồn tại không, nếu có tồn tại ta sẽ gán nó vào req để sử dụng ở các hàm sau.access token phải còn thời gian tồn tại mới verify được nhé. Sau đó hàm này cũng trả về phần data được lưu trong đây. Cuối cùng ta sẽ kiểm tra lại một lần nữa user này có tồn tại không, nếu có tồn tại ta sẽ gán nó vào req để sử dụng ở các hàm sau. 6kết thúc hàm ta cần sử dụng lệnh next() để chuyển qua hàm tiếp theo, nếu không xử lý sẽ bị treo tại đó.next() để chuyển qua hàm tiếp theo, nếu không xử lý sẽ bị treo tại đó. Cuối cùng ở ./src/users/profile ta cần require đến middleware đó và đặt vào đây:./src/users/profile ta cần require đến middleware đó và đặt vào đây: 7Các API khác yêu cầu đăng nhập trước mới lấy/thêm/sửa/xóa được data thì bạn làm tương tự nhéee Xem kết quả nào (ở đây mình chỉ trả về thông tin user, và mình lấy nó ở req.user)req.user) Khi sai access token hoặc access token hết hạn: Access token hợp lệ: access token hoặc access token hết hạn: Access token hợp lệ: Tổng kếtVậy là xong rồi nè, mình vừa chia sẻ cách mà mình xác thực người dùng sử dụng JWT. Hy vọng bài viết sẽ giúp các bạn mới làm quen với NodeJS có thể tạo ra các API authentication để sử dụng cho các API tiếp theo. Không quên phần code của mình trên github tại đây, các bạn có thể tham khảo. |