Categories
Computer

ใช้ HTTPOnly Cookies บน Node.js ด้วย Express สำหรับ Access Token

เดิมทีเวลาที่เราใช้งานเพื่อเข้าสู่ระบบ เราจะเขียนโค้ดเพื่อขอ Access Token จากเซิร์ฟเวอร์แล้วนำมาเก็บไว้บน Client เพื่อนำมาใช้งานระบบที่จำเป็น โดยในบทความบนเว็บที่เกี่ยวกับการทำระบบ Authentication โดยใช้ไลบรารี Apollo GraphQL นั้น เราจะเก็บข้อมูล Access Token ที่สร้างขึ้นไว้บน LocalStorage เพื่อนำมาใช้งานต่อครับ

อย่างไรก็ดี การเก็บข้อมูลไว้ในนี้ก็มีปัญหาเรื่องความปลอดภัย ในบทความนี้จะแนะนำวิธีการเก็บข้อมูล Access Token ที่ปลอดภัยกว่าการเก็บบน LocalStorage ครับ

LocalStorage

ก่อนอื่น เรามาพูดถึง LocalStorage ก่อน ตัว LocalStorage (หรือเรียกว่า window.localStorage) เป็นส่วนหนึ่งของ HTML5 Web Storage ที่ทำหน้าที่เก็บข้อมูลใส่ไว้บนเว็บเบราวเซอร์ทางฝั่ง Client ที่ข้อมูลยังคงอยู่ ถึงแม้ว่าเราจะปิดหน้าเว็บเบราวเซอร์นั้น ๆ ออกไป หรือข้อมูลหมดอายุตามที่เรากำหนดไว้เอง

การใช้งาน LocalStorage ทำได้โดยการเก็บข้อมูลตามด้านล่างนี้ครับ

localStorage.setItem('token', < Access Token ที่ได้ >);

แล้วเราสามารถเรียกได้โดยการพิมพ์คำสั่ง

let token = localStorage.getItem('token');

จากนั้นข้อมูล Access Token ที่เก็บไว้ในเว็บเบราวเซอร์ก็จะคืนค่าอยู่ในตัวแปร token ครับ ฟังดูแล้วง่ายกว่าที่ดีคิดไว้ เพราะใช้คำสั่งตามที่เขียนไว้ข้างบนนี้ ข้อมูลก็ออกมาได้แล้วครับ อย่างไรก็ดี การเก็บข้อมูลไว้ใน LocalStorage มีข้อเสีย เนื่องมาจาก

  • ข้อมูลที่เก็บ สามารถเก็บในรูปแบบ String เท่านั้น
  • มีขนาดที่จำกัดเพียงแค่ 5MB (แต่ข้อมูลข้างบนไม่ถึง 5MB :P)
  • มีปัญหาเรื่องความปลอดภัย เนื่องมาจากอ่านข้อมูลได้ง่ายเพียงใช้คำสั่งเดียวตามที่กล่าวไว้ข้างบน และสามารถดึกข้อมูลได้โดย Cross site scripting (XSS) ครับ
  • อ่านรายละเอียดเพิ่มเติมได้ในหน้าเว็บ dev.to ที่มีคนหนึ่งได้เขียนไว้

ดังนั้นแล้ว ข้อมูลที่เก็บไว้ใน LocalStorage ไม่ควรจะเก็บข้อมูลที่เป็นความลับครับ แล้วเราจะเก็บข้อมูลไว้ที่ไหนดีล่ะ?

HTTPOnly Cookies

HTTPOnly Cookies เป็นการตั้งค่าอันหนึ่งที่พบได้ระหว่าการตั้งค่า Set-Cookie HTTP Response Header ที่พบได้ตั้งแต่ Internet Explorer 6 SP1 (เก่ามากและ) โดยข้อมูลนี้เอามาจากบทความในหน้าเว็บมูลนิธิ OWASP ครับ

สำหรับข้อมูลที่เป็นความลับ หรือข้อมูลที่ Sensitive มากกว่าปกติ ยกตัวอย่างเช่นข้อมูล Access Token, User ID, Session ID, ข้อมูลบัตรเครดิต และอื่น ๆ ที่เราต้องการให้แชร์บน Facebook แบบ Public นั้น เราเก็บช้อมูลใน HTTPOnly Cookies ที่ทำได้ตามขั้นตอนด้านล่างนี้

  1. เมื่อผู้ใช้เข้าสู่ระบบ เซิร์ฟเวอร์จะสร้างข้อมูล Session ที่จำเป็นขึ้นมาสำหรับการใช้งาน
  2. นำข้อมูลที่สร้างขึ้นเก็บอยู่ในรูปแบบ Cookie โดยกำหนดค่าคุกกี้ให้เป็น HTTPOnly เท่านั้น

ไลบรารีที่รองรับ HTTPOnly Cookies มีหลากหลายไลบรารีมาก ผู้อ่านสามารถหาเพิ่มเติมได้ในอินเตอร์เน็ต แต่ในตัวอย่างนี้ เราจะใช้ไลบรารีที่นิยมอันหนึ่งที่มีชื่อว่า Express ที่อยู่ในรูปแบบภาษา JavaScript ที่ทำงานบน Node.js ครับ

ตัวอย่างของการสร้าง HTTPOnly Cookies ทำได้ตามด้านล่างนี้ครับ แต่ก่อนอื่น เราต้องติดตั้งไลบรารีเหล่านี้ก่อนครับ

yarn add express dayjs cookie-parser jsonwebtoken

เมื่อติดตั้งเสร็จแล้ว เราพิมพ์ส่วนนี้เพิ่มเติมลงไปใน package.json

{  [...]  "type": "module",  [...]}

เพื่อให้ใช้งานตามตัวอย่างด้านล่างนี้ได้ครับ

import http from 'http';
import express from 'express';
import dayjs from 'dayjs';
import cookieParser from "cookie-parser";
import jwt from 'jsonwebtoken';
const app = express();
const httpServer = http.createServer(app);

[...]

app.use(cookieParser());

[...]

app.post('/login', (req, res, next) => {
[...]
let token = jwt.sign(payload, jwt_secret, { expiresIn: jwt_expire });
res.cookie("authorization", token, {
secure: true,
httpOnly: true,
expires: dayjs().add(1, "days").toDate(),
sameSite: 'Strict'
});
[...]
});[

...]

เมื่อดูตัวอย่างจากข้างบนนี้แล้ว ดูตรงส่วนคำสั่ง

app.use(cookieParser());

คำสั่งนี้เป็นการเกิดใช้งานตัว cookie-parser Middleware ที่เป็นส่วนหนึ่งของไลบรารี Express ที่อนุญาตให้เราเรียกใช้งาน Cookie header เพื่อดึงข้อมูล HTTPOnly Cookies โดยพิมพ์ว่า req.cookies ครับ

ในหลายบรรทัดต่อมา ตรงส่วนคำสั่ง

res.cookie("token", token,    
secure: true,
httpOnly: true,
expires: dayjs().add(1, "days").toDate(),
sameSite: 'Strict'
});

คำสั่งนี้เป็นคำสั่งที่เรากำหนดค่าอะไรก็ตามลงไปใน Cookie ที่เราได้กำหนดไว้ คือ เรากำหนดค่า Access Token ลงไปใน Cookie token ครับ ส่วนการตั้งค่าที่อยู่ในปีกกานั้นเป็นการตั้งค่า Cookie นี้ให้

  • secure เป็นการตั้งค่าให้ใช้งานผ่าน HTTPS เท่านั้น
  • httpOnly เป็นการตั้งค่าให้ cookie ตัวนี้เรียกใช้งานโดยเว็บเซิร์ฟเวอร์ได้เท่านั้น
  • expires เป็นการตั้งค่าระยะเวลาหมดอายุ ตามคำสั่งที่เขียนข้างบนกำหนดให้มีอายุ 1 วัน
  • sameSite ตั้งค่าให้เพิ่ม SameSite ใน Set-Cookie HTTP Header มีด้วยกันสองแบบได้แก่ Strict ที่ผ่านทางเว็บไซต์เราเท่านั้น หรือ Lax ที่ส่ง Cookie ผ่านเว็บไซต์อื่นได้ ผ่าน HTTP Get บน Address Bar (เช่นการกด Link)

ส่วนการตั้งค่าเพิ่มเติมที่เราไม่ได้กำหนดค่าไว้ มีได้แก่

  • domain กำหนดโดเมนเนม ในที่นี้ให้โดเมนเนมเดียวกันกับ app
  • encode เป็นฟังก์ชันที่กำหนด encoding เป็นต้น โดยการตั้งต่าเพิ่มเติมนี้ ผู้อ่านสามารถเข้าไปดูได้ใน Reference ของ Express

เมื่อเราเรียกใช้งานผ่านคำสั่งอย่าง fetch, XMLHttpRequest หรืออื่น ๆ เราดู HTTPOnly Cookies ได้ที่หน้า Developer Tools -> Application -> Storage -> Cookies -> ที่อยู่เว็บไซต์ -> เราจะพบ HTTPOnly Cookies ที่เราได้สร้างขึ้นครับ

HTTPOnly Cookies บน Developer Tools

HTTPOnly Cookies บน Developer Tools (ดูรูปเต็ม)

ดูตรง Authorization เราจะพบว่า Cookie ที่สร้างนี้อันนี้เป็น HTTPOnly Cookies ครับ แล้วเวลาที่ใช้งานจริง เราจะเรียกใช้งานอย่างไรดีล่ะ?

เราเรียกใช้งานได้โดยผ่านการพิมพ์

app.get('/isloggedin', (req, res, next) => {
  
  [...]

  let token = req.cookies.authorization;

  [...]

});

สังเกตตรง req.cookies.authorization อันนี้แหละ เป็นการเรียกใช้งาน HTTPOnly Cookies ที่เราสร้างชึ้นครับ อย่างไรก็ดี กรณีที่เราใช้งานผ่าน Apollo Server GraphQL เรายังเรียก HTTPOnly Cookies ได้อยู่ไหม คำตอบคือ ทำได้สบายมาก เพียงแต่เราต้องมาพิมพ์คำสั่งใน context ที่สร้างขึ้น

เราพิมพ์โค้ดได้ตามด้านล่างนี้

import { ApolloServer } from 'apollo-server-express';
import { ApolloServerPluginDrainHttpServer } from 'apollo-server-core';

[...]

const apollo_server = new ApolloServer({ 
  typeDefs,
  resolvers,
  plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
  context: ({ req }) => {
    let token = req.cookies.authorization || '';
    return {
      token
    };
  }
});

[...]

apollo_server.applyMiddleware({ "app": express_app });

[...]

สังเกตตรง context ที่อยู่ใน new ApolloServer({ … }) อันนี้แหละมีส่วนที่เราเรียกใช้งาน HTTPOnly Cookies ครับ ส่วนไลบรารีอื่น ผู้อ่านสามารถหาอ่านได้ในอินเตอร์เน็ตเพิ่มเติมครับ

ประโยชน์การใช้ HTTPOnly Cookies

ประโยชน์ของการใช้งาน HTTPOnly Cookies ตามที่เขียนในเว็บ CodingHorror หรืออื่น ๆ ได้แก่

  1. HTTPOnly Cookies จำกัดให้เรียกใช้งานผ่านเว็บเซิร์ฟเวอร์เท่านั้น เราจะใช้งานผ่าน document.cookie ไม่ได้
  2. ป้องกันการเกิด Cross-site Scripting (XSS) ดังนั้นแล้ว เวลาที่เรียกใช้งาน fetch, XMLHttpRequest หรืออื่น ๆ จะกระทำได้โดยผ่านโดเมนเดียวกันกับเว็บเซิร์ฟเวอร์เท่านั้น
  3. ตามที่เขียนข้างบนนี้มีเขียนให้ใช้ Same-site Cookie ที่ใช้งานได้เฉพาะเว็บเราเท่านั้น ทำให้เรียกใช้ผ่านเว็บอื่นไม่ได้ ส่วนนี้จะป้องกัน Cross site request forgery (CSRF) ครับ

และอื่น ๆ

สรุป

จากตัวอย่างจะพบว่าเราใช้งาน HTTPOnly Cookies โดยใช้งานบน Node.js ที่ใช้ไลบรารี Express ได้เพียงไม่กี่คำสั่งเท่านั้น เมื่อใช้งานแล้วทำให้เว็บไซต์ของเราปลอดภัยขึ้นมากกว่าเดิมครับ อย่างไรก็ดี อันนี้เป็นส่วนหนึ่งที่ทำให้เว็บของเราปลอดภัยตรับ จะต้องพิจารณาการเขียนโค้ดส่วนอื่นร่วมด้วยว่ามีช่องโหว่หรือไม่ครับผม

By Kittisak Chotikkakamthorn

อดีตนักศึกษาฝึกงานทางด้าน AI ที่ภาควิชาวิศวกรรมไฟฟ้า มหาวิทยาลัย National Chung Cheng ที่ไต้หวัน ที่กำลังหางานทางด้าน Data Engineer ที่มีความสนใจทางด้าน Data, Coding และ Blogging / ติดต่อได้ที่: contact [at] nickuntitled.com