JWT คืออะไร? + ลองทำ JWT Authentication ด้วย Express.js

JWT Sep 3, 2023

JWT หรือ Token คืออะไร บทความนี้จะมาอธิบายที่มาของ JWT ว่าคืออะไร มีหน้าตาเป็นอย่างเรา เราสามารถนำไปใช้ทำอะไรได้บ้าง?

ตัวบทความ ผมนำมาแก้ไขจากบทความเดิมที่เคยเขียนไว้เมื่อ 7 ปีที่แล้ว (ใน บทความใช้ตัวอย่างของ Hapi.js)

Devahoy - Token และ JWT คืออะไร? + ทำ JWT Authentication ด้วย Hapi.js
เนื่องจากปัจจุบันได้หันมาพัฒนาเว็บไซต์ในรูปแบบ RESTFul API ซึ่งเป็น Web Server ในรูปแบบ stateless สำหรับเป็น API ทั้ง Single Page Application และ Mobile คือไม่มีการจดจำ state ของผู้ใช้แต่ว่าใช้ token base แทน ซึ่งปกติก็ใช้แต่ตัว jwt.io(https://jwt

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
หน้าตา โครงสร้างของ JWT

ซึ่งถ้าเราดูทีละส่วน เราจะแบ่งเป็นแบบนี้

ส่วนนี้จะแบ่งเป็น 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 นะครับ

GitHub - auth0/node-jsonwebtoken: JsonWebToken implementation for node.js http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html
JsonWebToken implementation for node.js http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html - GitHub - auth0/node-jsonwebtoken: JsonWebToken implementation for node.js http://self-iss…

ทำการสร้างโปรเจ็คขึ้นมา สมมติผมตั้งชื่อว่า 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

JWT.IO
JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.

จะเห็นว่ามันขึ้นว่า 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 Code

References

JWT.IO - JSON Web Tokens Introduction
Learn about JSON Web Tokens, what are they, how they work, when and why you should use them.
Get Started with JSON Web Tokens
All you wanted to know about JSON Web Tokens but were afraid to ask.

Tags

Chai Phonbopit

เป็น Web Dev ทำงานมา 10 ปีหน่อยๆ ด้วยภาษา JavaScript, Node.js, React, Vue และปัจจุบันกำลังสนใจ Web3, Crypto และ Blockchain เขียนบล็อกที่ https://devahoy.com