JWT คืออะไร? + ลองทำ JWT Authentication ด้วย Express.js
JWT หรือ Token คืออะไร บทความนี้จะมาอธิบายที่มาของ JWT ว่าคืออะไร มีหน้าตาเป็นอย่างเรา เราสามารถนำไปใช้ทำอะไรได้บ้าง?
ตัวบทความ ผมนำมาแก้ไขจากบทความเดิมที่เคยเขียนไว้เมื่อ 7 ปีที่แล้ว (ใน บทความใช้ตัวอย่างของ Hapi.js)

JWT คืออะไร?
JWT ย่อมาจาก JSON Web Token เป็นมาตรฐานนึง ที่ให้เราสามารถที่จะแชร์ information ระหว่างกันได้ เช่น Client <-> Server หรือ Server <-> Server ซึ่งภายในตัว JWT จะเป็นข้อมูลรูปแบบ JSON ที่ทำการ signed ด้วย cryptographic algorithm
ส่วนประกอบของ JWT มีด้วยกัน 3 ส่วนคือ
- Header
- Payload
- Signature
ซึ่งทั้ง 3 ส่วน จะถูกแบ่งด้วยการใช้ จุด (dots) ฉะนั้นตัว JWT จึงมีหน้าตาแบบนี้
xxxxxx.yyyyyy.zzzzzz
ตัวอย่างแบบ JWT จริงๆ
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

ซึ่งถ้าเราดูทีละส่วน เราจะแบ่งเป็นแบบนี้
Header
ส่วนนี้จะแบ่งเป็น 2 ส่วนคือ ตัว token เป็นชนิดใด และใช้ hashing algorithm อะไร เช่น HMAC, SHA256 หรือ RSA ตัวอย่าง
{
"alg": "HS256",
"typ": "JWT"
}
Payload
เป็นส่วนที่ประกอบไปด้วยข้อมูลที่เราต้องการจะเก็บหรือระบุตัวตนคนนั้นๆ ข้อมูล info ต่างๆ โดยต้องไม่เป็นข้อมูลที่เป็น sensitive data เด็ดขาด เพราะข้อมูล JWT ใครๆ ก็สามารถอ่านได้ แม้ว่าจะ encrypted ไว้ก็ตาม หน้าตา payload ก็จะประมาณนี้
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
Signature
ตัว signature เอาไว้สำหรับ verify ว่าตัว JWT นั้นๆ ไม่ได้ถูกแก้ไขมาระหว่างทาง เพราะว่าโดยปกติแล้ว จะไม่มีใครรู้ secret key นอกจากตัวเรา ฉะนั้น ก็ไม่มีทางที่จะแก้ไขข้อมูลได้เลย เพราะใช้คนละ secret ค่าที่ได้ก็จะเปลี่ยน
ตัว JWT Token ใครๆก็สามารถอ่านข้อมูลได้ และแก้ไข payload / claims ของเราได้เช่นกัน แต่ว่าจะไม่สามารถ verify ผ่าน เพราะว่า signature ไม่ตรงกัน นอกจากจะรู้ secret key ของเรา
การสร้าง signature จำเป็นต้องใช้ส่วนประกอบหลายๆ อย่างเข้าด้วยกัน คือ
- header ที่ encoded แล้ว
- payload ที่ encoded แล้ว
- secret key - ค่าที่เรากำหนดไว้
- algorithm ที่ใช้
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
เมื่อทั้ง 3 ส่วนรวมร่างกันเสร็จแล้ว ก็จะได้เป็น JWT Token ตามรูปแบบก่อนหน้านี้นั่นเอง นั่นก็คือ
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
JSON Web Token ทำงานยังไง?
โดยปกติในการ Authentication เมื่อผู้ใช้งานทำการ Login เข้าสู่ระบบ ฝั่ง Web Server จะทำการ generate JWT Token และส่งกลับไปให้ Client
เมื่อผู้ใช้ (Client) ต้องการเข้าถึง route api ต่างๆ (สมมติเป็น private route) ก็จะส่ง JWT มาด้วย โดยปกติจะแนบเป็น Authorization header โดยใช้ schema Bearer แบบนี้
Authorization: Bearer <token>
ฝั่ง Web Server ก็จะทำการ verify ว่าตัว Token ที่ส่งมาเนี่ยเทียบแล้ว signature ถูกต้อง
ลงมือทำ
เพื่อให้เห็นภาพ เราลองมาลงมือทำดีกว่า โดยตัวอย่าง จะใช้เป็น Node.js + Express และ JSON Web Token library นะครับ
ทำการสร้างโปรเจ็คขึ้นมา สมมติผมตั้งชื่อว่า hello-jwt
โดยใช้ express-generator
# สร้างโฟลเดอร์
mkdir hello-jwt && cd hello-jwt
# init project
npm init -y
ติดตั้ง Express และ JWT library
npm install express jsonwebtoken
สร้างไฟล์ app.js
ขึ้นมา โดยตัวอย่างผมมี route 2 แบบคือ
GET /protected
- เป็น route ที่ต้องใช้ token ในการเข้าถึงGET /token
- เพื่อทำการขอ Token (ปกติ จะเป็นพวก /login เพื่อใช้ username / password ไปตรวจเช็คว่าเป็นผู้ใช้จริงๆ มั้ย แล้วถึงจะ generate token ให้)
const express = require('express')
const app = express()
app.get('/', (req, res) => res.json({ message: 'Hello JWT' }))
app.get('/token', (req, res) => {
res.json({
token: 'token',
})
})
app.get('/protected', (req, res) => {
// check token
res.status(401).json({
message: 'Unauthorized',
})
})
app.listen(5000, () => console.log('Application is running on port 5000'))
การ Generate Token (sign)
เราจะ generate token ด้วยการเรียก function jwt.sign()
const jwt = require('jsonwebtoken')
const payload = { message: 'your data' }
// ตัวอย่างเฉยๆ ในการใช้งานจริง ไม่ควรประกาศ secretKey ไว้ในโค๊ด แต่ควรจะโหลดจาก
// environment variables เช่น .env แทน
const secretKey = 'secretKey'
// sign ด้วย default HMAC SHA256
const token = jwt.sign(payload, secretKey)
นอกจากนี้ เรายังกำหนด ให้ token มีอายุเท่าไหร่ ตามที่เราต้องการก็ได้ กรณีที่ token หมดอายุ แม้ว่าเราจะ verify ว่า signature ตรง แต่ตัว Token ก็ไม่สามารถใช้งานได้
// expire 1 ชั่วโมง
jwt.sign(payload, secretKey, { expiresIn: 60 * 60 });
// อีกแบบ
jwt.sign(payload, secretKey, { expiresIn: '1h' });
// ใช้แบบ exp ใน payload
jwt.sign({
exp: Math.floor(Date.now() / 1000) + (60 * 60),
data: payload
}, secretKey);
ทำการแก้ไข route GET /token
ให้ generate token เป็น response กลับไป
app.get('/token', (req, res) => {
const payload = {
id: 1,
displayName: 'John Doe',
role: 'admin',
}
const token = jwt.sign(payload, secretKey, { expiresIn: '1hr' })
res.json({
token,
})
})
ลอง restart / start server ใหม่อีกครั้ง ตัว Web Server ใช้ port 5000 จะเป็น http://localhost:5000
node app.js
ทดสอบโดยการเรียก GET http://localhost:5000/token
เพื่อดู token ที่ส่งกลับมา
curl http://localhost:5000/token
# result
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiZGlzcGxheU5hbWUiOiJKb2huIERvZSIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTY5MzczNzcyMiwiZXhwIjoxNjkzNzQxMzIyfQ.eZEbU4SOUkjpp8Nsc-wL5go0ql4kCGPChsODnbZSDqw"}%
ของนำ JWT ที่ได้ ไปใส่ที่เว็บ jwt.io เพื่อลองเช็คว่า JWT ของเราถูกต้องหรือไม่ โดยเอา Token ไปใส่ที่ช่อง Encoded

จะเห็นว่ามันขึ้นว่า Invalid Signature

เหตุผลที่ Invalid Signature เนื่องจาก ตัวเว็บ เอา Token ของเราไปเปรียบเทียบกับ Secret Key ที่เป็น default ของเว็บ คือ your-256-bit-secret
ให้เราลองเปลี่ยนเป็น secret ที่เราใช้ตอน generate token แล้วลอง paste ตัว Token ไปเช็คใหม่ จะต้องผ่าน และขึ้นว่า Signature Verified
ทีนี้มาดูที่ตัวโค๊ดบ้าง เราจะ verify แบบเว็บนี้ยังไง? ก็คือใช้ function verify()
นั่นเอง
const decoded = jwt.verify(token, secretKey)
ไปแก้ไขส่วน route /protected
ให้เช็ค token ถ้าผ่าน ก็ให้ return Hello Message ถ้าตัว Token ไม่ถูกต้อง หรือหมดอายุ ให้ขึ้น Unauthorized พร้อมเหตุผล (ทดสอบหมดอายุ เราอาจจะแก้ไขตรง generate token ให้เปลี่ยน expires เป็น 1 นาทีพอ)
app.get('/protected', (req, res) => {
const token = req.headers.authorization.split(' ')[1]
try {
const decoded = jwt.verify(token, secretKey)
res.json({
message: 'Hello! You are authorized',
decoded,
})
} catch (error) {
res.status(401).json({
message: 'Unauthorized',
error: error.message,
})
}
})
ทดสอบโดยการเรียก GET /protected
พร้อมส่ง token แนบไป Authorization Header ด้วย
curl -i -H "authorization: <TOKEN>" http://localhost:5000/protected"
หรือใครไม่ถนัด cURL ก็ใช้ Postman ก็ได้เช่นกัน

ซึ่งถ้าเราใช้ Token ที่หมดอายุก็ได้ response เป็น
{
"message": "Unauthorized",
"error": "jwt expired"
}
Error อื่นๆ เช่น
{
"message": "Unauthorized",
"error": "invalid token"
}
// ผิด format หรือลืมส่ง token
{
"message": "Unauthorized",
"error": "jwt malformed"
}
// invalid signature อาจจะ secret ผิด หรือแอบเปลี่ยน payload
{
"message": "Unauthorized",
"error": "invalid signature"
}
สรุป
บทความนี้ก็เป็นตัวอย่างการใช้งาน JWT คร่าวๆนะครับ ให้รู้ที่มาของ JWT ว่าสร้างยังไง การ sign และการ verify ทำได้อย่างไรบ้าง? หากติดปัญหา ลองดูตัว Source Code ที่ผมอัพลง Github เพิ่มเติมนะครับ
Source CodeReferences

