สวัสดีครับทุกคน วันนี้เราจะมาพาทุกคนมาลงมือทำระบบ authentication ด้วย JWT Refresh token บน NodeJS กัน
ผู้เขียน Thapanon S.- BorntoDev Co., Ltd.
แต่ก่อนอื่นเราต้องมาทำความรู้จัก JWT และ Concept ของ JWT Refresh token กันก่อน
ผมเชื่อว่าหลายๆคนในที่นี้คงจะรู้จัก JWT หรือ Json web token กันแล้ว ดังนั้นเราก็จะมาสรุปกันแบบสั้น ๆ ให้กับคนที่อาจจะยังไม่รู้มาก่อน
โดยต้องบอกก่อนเลยว่า JWT นั้นเป็นท่ามาตรฐานที่ใช้กันทั่วไปในการทำ Web Authentication โดย JWT จะเป็น json ที่ประกอบไปด้วย 3 ส่วน ได้แก่ Header,Payload และ Signature โดยในแต่ละส่วนจะถูกคั่นด้วย . แบบนี้เลย Header.Payload.Signature แล้วหลักการทำงานของ jwt ก็จะเป็นตาม diagram ด้านล่างเลย และสำหรับใครที่อยากได้ความรู้เกี่ยวกับ JWT แบบเต็มๆ ก็สามารถอ่านต่อได้ที่ บทความ
และจาก Diagram ก็จะเห็นได้ว่าถ้า Access token นั้น Expire หรือถูก Reject ผู้ใช้จะต้อง Login ใหม่
และต่อไปเราก็จะมาเข้าสู่หัวข้อหลักของวันนี้แล้วนั่นคือ JWT Refresh token โดย Concept ของ JWT Refresh token ก็จะเหมือนกับ JWT เลยแต่มีขั้นตอนพิเศษเพิ่มขึ้นมา ตาม Diagram ด้านล่างเลย
จาก Diagram ด้านบน จะเห็นได้ว่าเมื่อผู้ใช้ Login ก็จะได้ Token ไป 2 ตัว นั่นก็คือ Access token กับ Refresh token โดยทั้ง 2 ตัวนี้ก็จะเป็น JWT ทั้งคู่แต่ทำหน้าที่แตกต่างกันโดย
Access token มีหน้าที่อย่างที่เรารู้ ๆ กันนั่นก็คือเอาไว้เพื่อยืนยันตัวตนในการรับข้อมูลที่ถูกป้องกันไว้
แต่ Refresh token จะเอาไว้ใช้เพื่อขอ Access token ใหม่เมื่อของเก่านั้น Expire หรือถูก Reject โดยเมื่อ Access token นั่นใช้ไม่ได้เราก็จะเขียน Code ให้หน้าบ้านนั้นนำ Refresh token มาแลกกับ Access token ใหม่ที่จะเอาไปใช้ในการขอข้อมูลในครั้งต่อ ๆ ไปนั่นเอง
ต่อไปเราจะมาลองลงมือเขียน Code กันนน
“`mkdir example-jwt-refresh-token“`
“`cd example-jwt-refresh-token“`
“`npm init -y “`
“`npm i dotenv express jsonwebtoken“`
แล้วหลังจากนั้นเราก็จะสร้าง file index.js มาและใส่ code ชุดนี้ลงไป
const express = require("express") const jwt = require("jsonwebtoken") require("dotenv").config() const app = express() app.use(express.json()) const port = process.env.PORT || 3000 const users = [ { id: 1, name: "John", refresh: null }, { id: 2, name: "Tom", refresh: null }, { id: 3, name: "Chris", refresh: null }, { id: 4, name: "David", refresh: null }, ] app.get("/", (req, res) => { res.send("Hello World!") }) app.listen(port, () => { console.log(`app listening on port ${port}`) })
และก็สร้าง file .env
ACCESS_TOKEN_SECRET=EiKf9vBVMW0Qiu6EWgzwU7PyCdD0BLxv7ks4kTe4fXvGPDYsS3QT3wugV4ReGopt REFRESH_TOKEN_SECRET=0ueUlWRDDjvu7188rORSqZVuwWUVvJSyPGWw84J3HxgWmW9VKRP4RFzW2Imvb1Jr PORT=80
แล้วเราก็รันตัว file index.js ที่พึ่งเขียนไปเมื่อสักครู่นี้เราก็จะได้ API server ที่สามารถรันได้แล้ว
ต่อไปเราจะสร้าง End point ขึ้นมาเพิ่มอีก 2 จุดได้แก่
POST /auth/login
POST /auth/refresh
เราจะมาเริ่มที่ login กันก่อน โดยในตัวอย่างผมจะไม่ใช้ database แต่จะใช้เป็นค่าจากตัวแปรแทน
โดย end point นี้ เราจะให้ผู้ใช้กรอก name มาเพื่อสร้าง JWT และให้เราสร้าง function มาเพื่อ generate token เอาไว้ก่อนโดยให้ Arguments ที่เราใส่เข้าไปเป็น Object ของ user : { id: number , name: string }
app.post("/auth/login", (req, res) => { const { name } = req.body //find user const user = users.findIndex((e) => e.name === name) if (!name || user < 0) { return res.send(400) } const access_token = jwtGenerate(users[user]) const refresh_token = jwtRefreshTokenGenerate(users[user]) users[user].refresh = refresh_token res.json({ access_token, refresh_token, }) })
ให้เราพัก End point ไว้ก่อน แล้วมาทำ Function สำรับสร้าง Access token กับ Refresh token กัน
const jwtGenerate = (user) => { const accessToken = jwt.sign( { name: user.name, id: user.id }, process.env.ACCESS_TOKEN_SECRET, { expiresIn: "3m", algorithm: "HS256" } ) return accessToken } const jwtRefreshTokenGenerate = (user) => { const refreshToken = jwt.sign( { name: user.name, id: user.id }, process.env.REFRESH_TOKEN_SECRET, { expiresIn: "1d", algorithm: "HS256" } ) return refreshToken }
จาก Code function ด้านบน จะเห็นได้ว่าเราใช้ Lib ที่มีชื่อว่า jsonwebtoken ในการสร้าง Token ออกมาแต่ผมจะทำการตั้งค่าให้อายุการใช้งานและ Secret key ที่ต่างกัน โดย Access token มีระยะเวลา 3 นาที และ refresh token มีอายุ 1 วัน โดยระยะเวลาก็ต้องมาตัดสินใจตามความเหมาะสมอีกทีนะครับ
ต่อไปเราจะมาทำ ?iddleware สำหรับ Validate JWT กัน
const jwtValidate = (req, res, next) => { try { if (!req.headers["authorization"]) return res.sendStatus(401) const token = req.headers["authorization"].replace("Bearer ", "") jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, decoded) => { if (err) throw new Error(error) }) next() } catch (error) { return res.sendStatus(403) } }
จาก Validate Function จะเห็นได้ว่าผมได้เอา “Bearer “ ออกและเอาแต่ token มาเพื่อให้ jsonwebtoken นั่นไป Validate ว่า JWT ของเรานั้นถูกต้องไหมและเรายังสามารถตรวจสอบใน Database เพิ่มเติมได้ว่ามี User นั่นจริงๆ ไหม
หลังจากนั้นก็เอา Function ไปไว้ในส่วนที่เราต้องการจะป้องกัน
app.get("/", jwtValidate, (req, res) => { res.send("Hello World!") })
หลังจากนั้นก็ลอง รันแบบไม่ใส่ Token และแบบใส่ Token ดู
ผลการรันแบบใส่ Token
ผลการรันแบบไม่ใส่ Token
และเมื่อผ่านไป 3 นาที Access token ของเราก็จะหมดอายุ และใช้ขอข้องมูลไม่ได้อีกต่อไป
ต่อมาเราจะมาเขียน end point /auth/refresh ไว้ใช้สำหรับให้เราเอา Refresh token มาแลกกับ Token ชุดใหม่
ก่อนอื่นเราจะมาทำ function สำหรับ Validate Refresh token กัน
const jwtRefreshTokenValidate = (req, res, next) => { try { if (!req.headers["authorization"]) return res.sendStatus(401) const token = req.headers["authorization"].replace("Bearer ", "") jwt.verify(token, process.env.REFRESH_TOKEN_SECRET, (err, decoded) => { if (err) throw new Error(error) req.user = decoded req.user.token = token delete req.user.exp delete req.user.iat }) next() } catch (error) { return res.sendStatus(403) } } }
ในการ Validate Refresh token ผมจะทำงานคล้ายๆ กับ Validate แต่จะแทรก user ไปกับ req ด้วย เพื่อนำไปใช้ Validate user ต่อ ถัดมาเราก็จะมาเขียนตรง End point กัน
app.post("/auth/refresh", jwtRefreshTokenValidate, (req, res) => { const user = users.find( (e) => e.id === req.user.id && e.name === req.user.name ) const userIndex = users.findIndex((e) => e.refresh === req.user.token) if (!user || userIndex < 0) return res.sendStatus(401) const access_token = jwtGenerate(user) const refresh_token = jwtRefreshTokenGenerate(user) users[userIndex].refresh = refresh_token return res.json({ access_token, refresh_token, }) })
ใน Code จะเห็นได้ว่าหลังจะที่ Refresh token นั่น Validate ผ่านแล้วจะมีการเอาข้อมูลของ user นั้นมา Generate Accress token และ Refresh token ตัวใหม่และทำการเก็บ Refresn token ตัวใหม่ไว้แทนแล้วส่ง token ชุดใหม่กลับไปให้ หน้าฝั่งหน้าบ้างของเราเก็บนั่นเอง มาลองยิง api กัน
จะเห็นได้ว่าเราได้ Token ชุดใหม่มาแล้วเมื่อเรายิง API เพื่อของ Token ชุดใหม่ด้วย Refresh token เก่า ผลที่ออกมาก็คือ จะไม่สามารถใช้งานได้นั่นเอง
ต่อมาเราลองเอา Access token ใหม่มายิง API กัน
ก็จะเห็นได้ว่าเราสามารถใช้งานได้แบบปกติเลย