เรียนรู้การใช้ JWT กับ Passport.js Authentication

NottDev
4 min readDec 5, 2019

--

ในปัจจุบันนี้ Web App และ Mobile App ส่วนใหญ่นั้นจะมีการยืนยันตัวตนก่อนเข้าระบบ ซึ่งมีวิธีการเข้าสู่ระบบที่หลากหลายและแตกต่างกัน เช่น Facebook , Google, Twitter หรือ Email และ Password เป็นต้น

ก่อนจะไปเริ่มโค้ดกัน เรามาทำความเข้าใจเกี่ยวกับ Passport.js และ JWT กันก่อนว่าคืออะไร

Passport.js

Passport is Simple, unobtrusive authentication for Node.js

ตามคำนิยามแปลได้ว่า “การยืนยันตัวตนที่ง่ายและไม่ยุ่งยาก สำหรับ Node.js”

Passport.js คือ Authentication middleware สำหรับ Node.js ที่มีความสามารถในการรองรับการยืนยันตัวตนที่หลากหลายและสามารถทำได้ง่าย โดยค่าเริ่มต้น จะเก็บข้อมูล user object ไว้ใน session

JWT

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

JWT ย่อมาจาก JSON Web Token เป็นมาตรฐานเปิด (RFC 7519) ที่เข้ามา claims ความปลอดภัยของการส่งข้อมูลระหว่างสองฝ่าย โดยที่ถูกออกแบบไว้ว่า จะต้องมีขนาดที่กระทัดรัด (Compact) และเก็บข้อมูลภายในตัว (Self-contained)

เนื้อหาในบทความประกอบด้วย

  1. Setup Simple API with Express
  2. Setup dependencies
  3. Setup local passport strategy
  4. Using passport authenticate & Sign JWT
  5. Using passport middleware for route user

เริ่มกันเลย!!!

ต่อไปนี้จะเป็นการสาธิตการใช้งาน Passport และ JWT สำหรับสร้างการยืนยันตัวตนแบบง่าย ด้วย email/password โดยสามารถค่อยๆทำตามทีละ step ได้เลยครับ และตอนท้ายจะมี source code แจกให้ไว้ทำความเข้าใจอีกทีครับ ไม่พูดพร่ำทำเพลงมาก ลุยโล๊ดดด++

Project Directory Structure

passport-jwt/
+-- configs
¦ +-- passport.js
+-- routes
¦ +-- auth.js
¦ +-- users.js
+-- app.js
+-- package.json

Setup Simple API with Express

ติดตั้ง express ด้วยคำสั่งต่อไปนี้

yarn add express

จากนั้นเขียนโค้ดสำหรับ Start Web Server (ไม่ได้ลงลึกเรื่อง Express)

// app.jsconst express = require('express'),
app = express(),
port = process.env.PORT || 3000
// Set Parses JSON
app.use(express.json())
// Error Handler
app.use((err, req, res, next) => {
let statusCode = err.status || 500
res.status(statusCode)
res.json({
error: {
status: statusCode,
message: err.message
}
});
});
// Start Server
app.listen(port, ()=>console.log(`Server is running on port ${port}`))

Setup dependencies

ติดตั้ง dependencies ทั้งหมดที่ต้องใช้

yarn add passport passport-local passport-jwt jsonwebtoken

รายละเอียดแต่ละ modules ดังนี้

  • passport: สำหรับเป็น Authentication Middleware
  • passport-local: สำหรับการยืนยันตัวตนด้วย email/password
  • passport-jwt: สำหรับการยืนยันตัวตนด้วย JSON web token
  • jsonwebtoken: สำหรับจัดการเกี่ยวกับ Token

เรามาทำความเข้าใจกระบวนการทำงานภาพรวมกันสักหน่อย

  • เมื่อ User ทำการ login เข้ามาสำเร็จ backend จะทำการ signed token และส่งค่ากลับมาให้
  • Client จะเก็บ token ไว้ (ส่วนใหญ่จะเก็บไว้ใน localStorage) และเมื่อมี request ก็จะส่งมาด้วยทุกครั้ง เพื่อใช้ในการยืนยันตัวตน
  • ทุก request จะผ่าน middleware เพื่อนำ token ที่ส่งมา มาตรวจสอบเพื่อยืนยันตัวตนและจะอนุญาตเฉพาะ request ที่ยืนยันตัวตนสำเร็จเท่านั้น

Setup local passport strategy

เริ่มจากการ setup การใช้ local passport strategy สำหรับกำหนดเงื่อนไขการตรวจสอบการยืนยันตัวตนด้วย email/password

// configs/passport.jsconst passport = require('passport’),
LocalStrategy = require('passport-local').Strategy
passport.use(new LocalStrategy({
usernameField: 'email',
passwordField: 'password'
},
(email, password, cb) => {
//this one is typically a DB call. Assume that the returned user object is pre-formatted and ready for storing in JWT

return UserModel.findOne({email, password})
.then(user => {
if (!user) {
return cb(null, false, {message: 'Incorrect email or password.'})
}
return cb(null, user, {message: 'Logged In Successfully'}) })
.catch(err => cb(err))
}
));

จากนั้น require มันในไฟล์ /app.js เพื่อเรียกใช้งาน

// app.jsconst express = require('express'),
app = express(),
port = process.env.PORT || 3000
// Set Parses JSON
app.use(express.json())
// Import passport
require('./configs/passport');
...

Using passport authenticate & Sign JWT

ต่อมาเราจะมาเขียนโค้ดฟังก์ชันสำหรับการ login ในไฟล์ routes/auth.js โดยจะมีการเรียกใช้ฟังก์ชัน passport.authentication(‘local’) ซึ่งฟังก์ชันนี้ใช้สำหรับการตรวจสอบการยืนยันตัวตนของ user ด้วยวิธีการแบบ local strategy นั้นคือใช้ email/password และมีการ handle error หากข้อมูลไม่ถูกต้องหรือเกิดข้อผิดพลาด

// routes/auth.jsconst router = require('express').Router(),
jwt = require('jsonwebtoken')
passport = require('passport')
/* POST login. */
router.post('/login', (req, res, next) => {

passport.authenticate('local', {session: false}, (err, user, info) => {
if (err) return next(err) if(user) {
const token = jwt.sign(user, 'your_jwt_secret')
return res.json({user, token})
} else {
return res.status(422).json(info)
}
})(req, res, next);});

สำหรับการตั้งค่า {session: false} ในฟังก์ชัน passport.authenticate() หมายถึงเราไม่ต้องการเก็บ user ไว้ใน session โดยเราสร้างและส่งค่าเป็น JSON web token ที่มีการ sign กับ user object ให้กับ client เท่านั้น ซึ่งใน user object นั้นไม่ควรเป็นข้อมูลสำคัญ เช่น password ควรเป็นข้อมูลที่สามารถช่วยในการยืนยันตัวตนได้ เช่น id, username หรือ email เป็นต้น

แนวคิดคือ สำหรับเก็บข้อมูลที่น้อยที่สุดที่สามารถใช้ได้ โดยไม่ต้องดึงข้อมูลจากฐานข้อมูลในทุกครั้งที่ยืนยันตัวตน

ในส่วนถัดมาเราจะทำการสร้าง middleware สำหรับตรวจสอบ Token สำหรับ request ที่ต้องการเข้าถึง path url ที่ต้องมีการยืนยันตัวตนก่อน เช่น /user/profile เป็นต้น ซึ่งส่วนนี้เราจะใช้ passport-jwt strategy กันครับ โดยให้เพิ่มโค้ดในไฟล์ /configs/passport.js

// configs/passport.js...
const passportJWT = require("passport-jwt"),
JWTStrategy = passportJWT.Strategy,
ExtractJWT = passportJWT.ExtractJwt
...
passport.use(new JWTStrategy({
jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken(),
secretOrKey : 'your_jwt_secret'
},
(jwtPayload, cb) => {
//find the user in db if needed. This functionality may be omitted if you store everything you'll need in JWT payload.

return UserModel.findOneById(jwtPayload.id)
.then(user => {
return cb(null, user);
})
.catch(err => {
return cb(err);
});
}
));

สำหรับ ExtractJWT.fromAuthHeaderAsBearerToken() คือ การกำหนดให้ client จะต้องส่ง JWT Token โดยตั้งค่า Header ให้มี Key เป็น Authorization และ Value เป็น Bearer <token>

เพื่อที่จะทดสอบ protected route เราจะสร้าง route user ขึ้นมา โดยการที่จะเข้าถึงได้นั้น client จะต้องส่ง JWT Token ที่ถูกต้องมากับ request ด้วย

// routes/user.jsconst express = require('express').Router()

/* GET users listing. */
router.get('/', (req, res, next) => {
res.send('respond with a resource');
});

/* GET user profile. */
router.get('/profile', (req, res, next) => {
res.send(req.user);
});

module.exports = router;

จากโค้ดด้านบน อธิบายดังนี้
- GET /user/ หากเข้า url นี้จะตอบกลับด้วยข้อความ “respond with a resource”
- GET /user/profile หากเข้า url นี้จะตอบกลับด้วย user object

Using passport middleware for route user

และขั้นตอนสุดท้าย ทำการเพิ่ม route auth และ route user ในไฟล์ /app.js และเพิ่มการเรียกใช้ passport authentication middleware ให้กับ route user (/user/*) เพื่อตรวจสอบการยืนยันตัวตนทุกครั้งที่มี Request

// app.js...const auth = require('./routes/auth');
const user = require('./routes/user');
app.use('/auth', auth)
app.use('/user', passport.authenticate('jwt', {session: false}), user)
...

เพียงเท่านี้ก็เป็นอันเสร็จพิธีครับ สำหรับการสร้างระบบ Authentication คูลๆ ด้วย JWT + Passport ซึ่งสามารถใช้ได้ท้ัง Mobile App และ Modern Web App เลยทีเดียว และไม่ต้องเก็บข้อมูล user ใน Session อีกต่อไป ดีงามพระรามแปดดด++

สำหรับใครที่หลุดหรืออ่านแล้วไม่ค่อยเข้าใจ สามารถโหลด source code ไปทำความเข้าใจเพิ่มเติมได้ครับ ซึ่งผมจะปรับโค้ดส่วนที่เป็นการ query ข้อมูลจาก Database เป็นการเช็คเงื่อนไขด้วย data mock และสามารถอ่านควบคู่ไปกับ บทความนี้ ซึ่งผมก็แปลของเค้ามาอีกทีครับ

Source Code Github

ทดสอบกับ Postman

POST /auth/login
GET /user/profile

JWT helps to organize authentication without storing the authentication state in any storage be it a session or a database

บทความสำหรับสร้าง Config Middleware for Passport Authentication

Reference:

(ข้อมูลอาจมีข้อผิดพลาด ถ้าจะเอาบทความนี้ไปอ้างอิงที่อื่นให้ตรวจสอบให้ดีก่อนนะครับ ขอบคุณครับ)

สำหรับวันนี้ ต้องขอลาไปก่อน สวัสดีครับ NottDev :)

--

--