Devahoy Logo
PublishedAt

NodeJS

Token และ JWT คืออะไร? + ทำ JWT Authentication ด้วย Hapi.js

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

1
Authorization: Bearer <token>

JWT คืออะไร?

JWT ย่อมาจาก JSON Web Token เป็นรูปแบบหนึ่งที่ใช้ในการสร้างรหัส token จากข้อมูล JSON Data แล้วทำการเข้ารหัสด้วย Base64Url Encoded ซึ่งมีหน้าตาลักษณะประมาณนี้

1
eyJ0eXAiOiJKV1tiLCJhbGciOiJIUzI1Nis9.eyJpZCI6IjU3NzRhMjM1Zm
2
I1OTdlMWIxMWY3YzY3ZiIsImVtYWlsIjoiY2hhaUBuZXh0enkuY29tIiwic2
3
NvcGUiOiJVU0VSIiwiaWF0IjoxNDY3NzgzMTgyLCJleHAiOjE0Njc4Njk40
4
DJ9.CGXxDtTJD0LBpY7oTbm-ZWB1o6J7isu09ZNk1Q2uTc0

จะเห็นว่าตัวอย่าง JWT ด้านบน จะมีตัวจุด (.) ขั้นไว้ คือ

Terminal window
<base64url-encoded header>.<base64url-encoded payload>.<base64url-encoded signature>

ซึ่งทั้ง 3 ส่วนประกอบไปด้วย

  1. Header : (คือข้อมูล metadata ของ token ซึ่งบอกว่า เป็น type และใช้ algorithm อะไร)
  2. Body หรือ Payload หรือ Claims : ข้อมูลทั้งหมดที่เราเอาไว้ sign token
  3. Signature : ส่วนสำคัญของข้อมูล เป็นการรวมกันของ Header และ Body ใช้ algorithm และ secret key ในการ sign

Header ประกอบไปด้วย 2 ส่วนคือ type เป็น JWT และ hash algorithm ที่ใช้ เช่น HMAC SHA256 ตัวอย่าง

1
{
2
"alg": "HS256",
3
"typ": "JWT"
4
}

เมื่อนำข้อมูล header มาเข้ารหัสด้วย Base64Url encoded ก็จะได้ข้อมูลประมาณ

1
eyJ0eXAiOiJKV1tiLCJhbGciOiJIUzI1Nis9

Payload

Payload ประกอบไปด้วยข้อมูล information ของคนๆนั้น เช่น id, name มีทั้งที่เป็น Private หรือ Public Data ข้อมูล JSON ของ payload มีหน้าตาประมาณนี้

1
{
2
"sub": "1",
3
"name": "Chai Phonbopit",
4
"role": "admin",
5
"exp": 1402374336,
6
"iat": 1402338336
7
}
  • sub: subject เอาไว้สำหรับ authenticate user (เช่น user Id)
  • exp: คือเวลาหมดอายุของ token
  • iat: Issued at timestamp บอกว่า token สร้างเมือไหร่

และเมื่อนำข้อมูล payload มาเข้ารหัสด้วย Base64Url encoded ก็จะได้ข้อมูลประมาณ

1
eyJpZCI6IjU3NzRhMjM1ZmI1OTdlMWIxMWY3YzY3ZiIsImVtYWlsIjoiY2hhaUBuZXh0enkuY29tIiwic2NvcGUiOiJVU0VSIiwiaWF0IjoxNDY3NzgzMTgyLCJleHAiOjE0Njc4Njk40DJ9

ไม่ควรเก็บข้อมูลที่เป็น Sensitive Data ไว้ใน token นะครับ เพราะสามารถดูข้อมูลข้างในได้ แม้จะไม่มี secret ก็ตาม

Signature

การจะสร้าง Signature ได้จำเป็นต้องใช้

  • header ที่ได้จากการ encoded แล้ว
  • payload ที่ได้จากการ encoded แล้ว
  • secret key ที่จะใช้
  • algorithm ที่จะใช้ เช่น HMACSHA256

ตัวอย่างของการ gen signature คือ

1
HMACSHA256(
2
base64UrlEncode(header) + "." +
3
base64UrlEncode(payload),
4
secretKey)

เมื่อรวมทั้งสามส่วนมารวมกัน คั่นด้วยเครื่องหมายจุด(.) ก็จะได้ข้อมูลเป็น token นั่นเอง

1
eyJ0eXAiOiJKV1tiLCJhbGciOiJIUzI1Nis9.eyJpZCI6IjU3NzRhM
2
jM1ZmI1OTdlMWIxMWY3YzY3ZiIsImVtYWlsIjoiY2hhaUBuZXh0enk
3
uY29tIiwic2NvcGUiOiJVU0VSIiwiaWF0IjoxNDY3NzgzMTgyLCJle
4
HAiOjE0Njc4Njk40DJ9.CGXxDtTJD0LBpY7oTbm-ZWB1o6J7isu09ZNk1Q2uTc0

สร้างโปรเจ็คด้วย Hapi.js

ทดลองทำระบบ Authentication API ง่ายๆด้วย

ไฟล์ package.json

1
{
2
"name": "hapi-auth-jwt-example",
3
"version": "1.0.0",
4
"scripts": {
5
"start": "node server.js",
6
"dev": "nodemon server.js"
7
},
8
"dependencies": {
9
"hapi": "^13.5.0",
10
"hapi-auth-jwt2": "^7.0.1",
11
"jsonwebtoken": "^7.0.1"
12
}
13
}

สร้างไฟล์ server.js ด้วย Hapijs แบบง่ายๆ โดยมีแค่ 2 routes คือ

  • / : assume ว่าส่ง POST username, password และได้ token กลับมา
  • /me : ต้องแนบ token ส่งไปด้วย เพื่อเรียกดูข้อมูลของ user นั้นๆ
1
'use strict'
2
const Hapi = require('hapi')
3
const server = new Hapi.Server()
4
5
server.connection({
6
host: 'localhost',
7
port: 5000
8
})
9
10
server.route({
11
method: 'GET',
12
path: '/',
13
handler: (request, reply) => {
14
// Gen token
15
reply({
16
token: 'token'
17
})
18
}
19
})
20
21
server.route({
22
method: 'GET',
23
path: '/me',
24
handler: (request, reply) => {
25
// Decoding token
26
reply(decoded)
27
}
28
})
29
30
server.start(() => {
31
console.log('Server is running')
32
})

สำหรับ hapi-auth-jwt2 เวลาใช้งาน เราต้องทำการ

  1. Register Plugin ด้วย server.register()
  2. กำหนด auth strategy เป็น jwt
  3. ทุก route กำหนด authenticated ผ่าน config

ทำการ register plugin

1
const HapiAuth = require('hapi-auth-jwt2')
2
3
let user = {
4
id: 1,
5
name: 'Chai Phonbopit'
6
}
7
8
server.register(HapiAuth, (err) => {
9
if (err) {
10
return reply(err)
11
}
12
13
server.auth.strategy('jwt', 'jwt', {
14
key: 'mysecretKey',
15
validateFunc: validate
16
})
17
server.auth.default('jwt')
18
})
19
20
function validate(decoded, request, callback) {
21
// ตัวอย่างการ validate คร่าวๆ เฉยๆ
22
// (ส่วนการใช้งานจริง ขึ้นอยู่กับ logic ของเพื่อนๆครับ)
23
if (decoded.name === 'Chai Phonbopit') {
24
return callback(null, true)
25
} else {
26
return callback(null, false)
27
}
28
}

อธิบายโค๊ดด้านบนคือ

  • ตัว hapi-auth-jwt2 จะมี validateFunc เอาไว้สำหรับทำการ validate jwt ซึ่งค่าที่ได้จากการ validate จะอยู่ในตัวแปร decoded ในตัวอย่างคือเช็คว่า decoded.name นั้นเท่ากับ Chai Phonbopit หรือไม่ ถ้าใช่ก็ return callback(null, false) กลับไป แสดงว่า token ถูกต้อง

โดยปกติแล้ว ตรงส่วน validate จะเอาไว้เช็คว่า User นี้มีจริงหรือไม่ เช่น ใช้ mongoose User.find({_id: decoded.id}).exec() เป็นต้น

ต่อมาแก้ไขตรง route / เพื่อเวลาเข้าหน้านี้จะให้มันทำการส่ง token ที่ sign ด้วย user กลับมา เพื่อเอาไว้ใช้ในหน้า /me นั่นเอง

1
const JWT = require('jsonwebtoken')
2
3
server.route({
4
method: 'GET',
5
path: '/',
6
config: {
7
auth: false
8
},
9
handler: (request, reply) => {
10
let token = JWT.sign(user, 'mysecretKey', {
11
expiresIn: '7d'
12
})
13
14
reply({
15
token: token
16
})
17
}
18
})

โค๊ดด้านบน เราทำการเพิ่ม

1
config: {
2
auth: false
3
}

เพราะว่า default เรากำหนดว่าให้ server.auth.default('jwt'); ทำให้ทุกๆ request จะเช็ค headers.authorization ทุกครั้ง เมือเรากดหนด auth: false path / ก็จะไม่ต้อง require token นั่นเอง

ต่อมา

1
let token = JWT.sign(user, 'mysecretKey', {
2
expiresIn: '7d'
3
})

ทำการ generate token ด้วย JWT.sign(payload, secretKey, options) และส่ง token กลับไปให้ User

สุดท้ายของเข้า /me แบบไม่ได้แนบ token

1
curl -i http://localhost:5000/me
2
3
4
HTTP/1.1 401 Unauthorized
5
WWW-Authenticate: Token
6
content-type: application/json; charset=utf-8
7
cache-control: no-cache
8
content-length: 76
9
Date: Sat, 09 Jul 2016 08:48:54 GMT
10
Connection: keep-alive
11
12
{"statusCode":401,"error":"Unauthorized","message":"Missing authentication"}%

และเข้า /me โดยการแนบ token ก็จะสามารถ access หน้านั้นได้

Terminal window
curl -i -H "authorization: <TOKEN>" http://localhost:5000/me"
Terminal window
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
avatar

Chai Phonbopit

เป็น Web Dev ในบริษัทแห่งหนึ่ง ทำงานมา 10 ปีกว่าๆ ด้วยภาษาและเทคโนโลยี เช่น JavaScript, Node.js, React, Vue และปัจจุบันกำลังสนใจในเรื่องของ Blockchain และ Crypto กำลังหัดเรียนภาษา Rust

Related Posts