Token และ JWT คืออะไร? + ทำ JWT Authentication ด้วย Hapi.js
เนื่องจากบทความนี้ ไม่ได้อัพเดทเนื้อหาที่เขียนไว้ตั้งแต่ปี 2016 แนะนำอ่านบทความล่าสุด ที่นี่แทนครับ JWT คืออะไร? + ลองทำ JWT Authentication ด้วย Express.js
เนื่องจากปัจจุบันได้หันมาพัฒนาเว็บไซต์ในรูปแบบ RESTFul API ซึ่งเป็น Web Server ในรูปแบบ stateless สำหรับเป็น API ทั้ง Single Page Application และ Mobile คือไม่มีการจดจำ state ของผู้ใช้แต่ว่าใช้ token base แทน ซึ่งปกติก็ใช้แต่ตัว jwt.io ของ Node.js ในการ sign, verify token แต่ไม่รู้ว่าส่วนประกอบของ token นั้นมันประกอบด้วยอะไรบ้าง วันนี้ก็เลยไปนั่งอ่านแล้วสรุปมาเป็นบทความครับ
Token คืออะไร?
เป็นรหัสชุดนึงที่เอาไว้สำหรับทดแทน session ซึ่งเอาไว้ระบุว่าคนๆนั้นคือใคร ตัวอย่างเช่น Facebook เมื่อล็อคอินเสร็จแล้วจะมี accessToken
เพื่อระบุตัวตนว่าเป็นใคร ซึ่งตัว token เอามาใช้ในการทำ RESTFul API ทดแทนการทำ Web Server แบบเดิมๆ ที่เก็บในรูปแบบ session โดยตัว token จะถูกส่งไปทุกๆ request ผ่าน HTTP Headers
Authorization: Bearer <token>
JWT คืออะไร?
JWT ย่อมาจาก JSON Web Token เป็นรูปแบบหนึ่งที่ใช้ในการสร้างรหัส token จากข้อมูล JSON Data แล้วทำการเข้ารหัสด้วย Base64Url Encoded ซึ่งมีหน้าตาลักษณะประมาณนี้
eyJ0eXAiOiJKV1tiLCJhbGciOiJIUzI1Nis9.eyJpZCI6IjU3NzRhMjM1Zm
I1OTdlMWIxMWY3YzY3ZiIsImVtYWlsIjoiY2hhaUBuZXh0enkuY29tIiwic2
NvcGUiOiJVU0VSIiwiaWF0IjoxNDY3NzgzMTgyLCJleHAiOjE0Njc4Njk40
DJ9.CGXxDtTJD0LBpY7oTbm-ZWB1o6J7isu09ZNk1Q2uTc0
จะเห็นว่าตัวอย่าง JWT ด้านบน จะมีตัวจุด (.) ขั้นไว้ คือ
<base64url-encoded header>.<base64url-encoded payload>.<base64url-encoded signature>
ซึ่งทั้ง 3 ส่วนประกอบไปด้วย
- Header : (คือข้อมูล metadata ของ token ซึ่งบอกว่า เป็น type และใช้ algorithm อะไร)
- Body หรือ Payload หรือ Claims : ข้อมูลทั้งหมดที่เราเอาไว้ sign token
- Signature : ส่วนสำคัญของข้อมูล เป็นการรวมกันของ Header และ Body ใช้ algorithm และ secret key ในการ sign
Header
Header ประกอบไปด้วย 2 ส่วนคือ type เป็น JWT
และ hash algorithm ที่ใช้ เช่น HMAC SHA256 ตัวอย่าง
{
"alg": "HS256",
"typ": "JWT"
}
เมื่อนำข้อมูล header มาเข้ารหัสด้วย Base64Url encoded ก็จะได้ข้อมูลประมาณ
eyJ0eXAiOiJKV1tiLCJhbGciOiJIUzI1Nis9
Payload
Payload ประกอบไปด้วยข้อมูล information ของคนๆนั้น เช่น id, name มีทั้งที่เป็น Private หรือ Public Data ข้อมูล JSON ของ payload มีหน้าตาประมาณนี้
{
"sub": "1",
"name": "Chai Phonbopit",
"role": "admin",
"exp": 1402374336,
"iat": 1402338336
}
sub
: subject เอาไว้สำหรับ authenticate user (เช่น user Id)exp
: คือเวลาหมดอายุของ tokeniat
: Issued at timestamp บอกว่า token สร้างเมือไหร่
และเมื่อนำข้อมูล payload มาเข้ารหัสด้วย Base64Url encoded ก็จะได้ข้อมูลประมาณ
eyJpZCI6IjU3NzRhMjM1ZmI1OTdlMWIxMWY3YzY3ZiIsImVtYWlsIjoiY2hhaUBuZXh0enkuY29tIiwic2NvcGUiOiJVU0VSIiwiaWF0IjoxNDY3NzgzMTgyLCJleHAiOjE0Njc4Njk40DJ9
ไม่ควรเก็บข้อมูลที่เป็น Sensitive Data ไว้ใน token นะครับ เพราะสามารถดูข้อมูลข้างในได้ แม้จะไม่มี secret ก็ตาม
Signature
การจะสร้าง Signature ได้จำเป็นต้องใช้
- header ที่ได้จากการ encoded แล้ว
- payload ที่ได้จากการ encoded แล้ว
- secret key ที่จะใช้
- algorithm ที่จะใช้ เช่น
HMACSHA256
ตัวอย่างของการ gen signature คือ
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secretKey)
เมื่อรวมทั้งสามส่วนมารวมกัน คั่นด้วยเครื่องหมายจุด(.) ก็จะได้ข้อมูลเป็น token นั่นเอง
eyJ0eXAiOiJKV1tiLCJhbGciOiJIUzI1Nis9.eyJpZCI6IjU3NzRhM
jM1ZmI1OTdlMWIxMWY3YzY3ZiIsImVtYWlsIjoiY2hhaUBuZXh0enk
uY29tIiwic2NvcGUiOiJVU0VSIiwiaWF0IjoxNDY3NzgzMTgyLCJle
HAiOjE0Njc4Njk40DJ9.CGXxDtTJD0LBpY7oTbm-ZWB1o6J7isu09ZNk1Q2uTc0
สร้างโปรเจ็คด้วย Hapi.js
ทดลองทำระบบ Authentication API ง่ายๆด้วย
ไฟล์ package.json
{
"name": "hapi-auth-jwt-example",
"version": "1.0.0",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"hapi": "^13.5.0",
"hapi-auth-jwt2": "^7.0.1",
"jsonwebtoken": "^7.0.1"
}
}
สร้างไฟล์ server.js
ด้วย Hapijs แบบง่ายๆ โดยมีแค่ 2 routes คือ
/
: assume ว่าส่งPOST username, password
และได้ token กลับมา/me
: ต้องแนบ token ส่งไปด้วย เพื่อเรียกดูข้อมูลของ user นั้นๆ
'use strict';
const Hapi = require('hapi');
const server = new Hapi.Server();
server.connection({
host: 'localhost',
port: 5000
});
server.route({
method: 'GET',
path: '/',
handler: (request, reply) => {
// Gen token
reply({
token: 'token'
});
}
});
server.route({
method: 'GET',
path: '/me',
handler: (request, reply) => {
// Decoding token
reply(decoded);
}
});
server.start(() => {
console.log('Server is running');
});
สำหรับ hapi-auth-jwt2
เวลาใช้งาน เราต้องทำการ
- Register Plugin ด้วย
server.register()
- กำหนด auth strategy เป็น
jwt
- ทุก route กำหนด authenticated ผ่าน
config
ทำการ register plugin
const HapiAuth = require('hapi-auth-jwt2');
let user = {
id: 1,
name: 'Chai Phonbopit'
};
server.register(HapiAuth, (err) => {
if (err) {
return reply(err);
}
server.auth.strategy('jwt', 'jwt', {
key: 'mysecretKey',
validateFunc: validate
});
server.auth.default('jwt');
});
function validate(decoded, request, callback) {
// ตัวอย่างการ validate คร่าวๆ เฉยๆ
// (ส่วนการใช้งานจริง ขึ้นอยู่กับ logic ของเพื่อนๆครับ)
if (decoded.name === 'Chai Phonbopit') {
return callback(null, true);
} else {
return callback(null, false);
}
}
อธิบายโค๊ดด้านบนคือ
- ตัว
hapi-auth-jwt2
จะมีvalidateFunc
เอาไว้สำหรับทำการ validate jwt ซึ่งค่าที่ได้จากการ validate จะอยู่ในตัวแปรdecoded
ในตัวอย่างคือเช็คว่าdecoded.name
นั้นเท่ากับChai Phonbopit
หรือไม่ ถ้าใช่ก็ returncallback(null, false)
กลับไป แสดงว่าtoken
ถูกต้อง
โดยปกติแล้ว ตรงส่วน
validate
จะเอาไว้เช็คว่า User นี้มีจริงหรือไม่ เช่น ใช้ mongooseUser.find({_id: decoded.id}).exec()
เป็นต้น
ต่อมาแก้ไขตรง route /
เพื่อเวลาเข้าหน้านี้จะให้มันทำการส่ง token ที่ sign ด้วย user
กลับมา เพื่อเอาไว้ใช้ในหน้า /me
นั่นเอง
const JWT = require('jsonwebtoken');
server.route({
method: 'GET',
path: '/',
config: {
auth: false
},
handler: (request, reply) => {
let token = JWT.sign(user, 'mysecretKey', {
expiresIn: '7d'
});
reply({
token: token
});
}
});
โค๊ดด้านบน เราทำการเพิ่ม
config: {
auth: false;
}
เพราะว่า default เรากำหนดว่าให้ server.auth.default('jwt');
ทำให้ทุกๆ request จะเช็ค headers.authorization
ทุกครั้ง เมือเรากดหนด auth: false
path /
ก็จะไม่ต้อง require token นั่นเอง
ต่อมา
let token = JWT.sign(user, 'mysecretKey', {
expiresIn: '7d'
});
ทำการ generate token ด้วย JWT.sign(payload, secretKey, options)
และส่ง token กลับไปให้ User
สุดท้ายของเข้า /me
แบบไม่ได้แนบ token
curl -i http://localhost:5000/me
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Token
content-type: application/json; charset=utf-8
cache-control: no-cache
content-length: 76
Date: Sat, 09 Jul 2016 08:48:54 GMT
Connection: keep-alive
{"statusCode":401,"error":"Unauthorized","message":"Missing authentication"}%
และเข้า /me
โดยการแนบ token ก็จะสามารถ access หน้านั้นได้
curl -i -H "authorization: <TOKEN>" http://localhost:5000/me"
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
cache-control: no-cache
content-length: 66
accept-ranges: bytes
Date: Sat, 09 Jul 2016 08:50:12 GMT
Connection: keep-alive
{"id":1,"name":"Chai Phonbopit","iat":1468053778,"exp":1468658578}%
เป็นอันเรียบร้อย
References:
- Authors
- Name
- Chai Phonbopit
- Website
- @Phonbopit