Home > Blog > tech

Authentication และ Authorization คืออะไร? สอน OAuth2, JWT, Session สำหรับ Web Developer 2026

authentication oauth2 jwt guide
Authentication OAuth2 JWT Guide 2026
2026-04-08 | tech | 3500 words

ในโลกของ Web Development ปี 2026 ความปลอดภัยของระบบเป็นสิ่งที่สำคัญที่สุด ไม่ว่าจะสร้างเว็บแอปขนาดเล็กหรือระบบ Enterprise ขนาดใหญ่ คุณจะต้องเข้าใจเรื่อง Authentication และ Authorization อย่างลึกซึ้ง เพราะนี่คือด่านแรกที่ปกป้องข้อมูลผู้ใช้และทรัพยากรของระบบทั้งหมด

บทความนี้จะพาคุณเรียนรู้ตั้งแต่พื้นฐานจนถึงขั้นสูง ครอบคลุม Session-based Auth, Token-based Auth ด้วย JWT, มาตรฐาน OAuth 2.0, OpenID Connect, การทำ Social Login, Multi-Factor Authentication, Passkeys/WebAuthn และ Best Practices ที่นักพัฒนาทุกคนต้องรู้ในยุคปัจจุบัน

Authentication vs Authorization — ต่างกันอย่างไร?

Authentication (AuthN) คือกระบวนการยืนยันตัวตนว่า "คุณคือใคร" เปรียบเทียบง่ายๆ ก็เหมือนการแสดงบัตรประชาชนเพื่อพิสูจน์ว่าคุณเป็นคนที่อ้างว่าเป็น ตัวอย่างเช่น การใส่ Username และ Password เพื่อ Login เข้าระบบ การสแกนลายนิ้วมือ หรือการยืนยันผ่าน OTP

Authorization (AuthZ) คือกระบวนการตรวจสอบสิทธิ์ว่า "คุณมีสิทธิ์ทำอะไรได้บ้าง" หลังจากระบบรู้แล้วว่าคุณเป็นใคร ขั้นต่อไปคือการตรวจสอบว่าคุณมีสิทธิ์เข้าถึงทรัพยากรใดได้บ้าง เช่น ผู้ใช้ทั่วไปดูข้อมูลได้แต่แก้ไขไม่ได้ Admin สามารถจัดการผู้ใช้คนอื่นได้ เป็นต้น

หัวข้อAuthenticationAuthorization
คำถามคุณคือใคร?คุณทำอะไรได้บ้าง?
เกิดเมื่อไหร่ก่อน Authorizationหลัง Authentication
ตัวอย่างLogin ด้วย Email/Passwordตรวจสอบ Role/Permission
โปรโตคอลOpenID Connect, SAMLOAuth 2.0, RBAC, ABAC
ข้อมูลที่ใช้Credentials (รหัสผ่าน, Biometric)Policies, Roles, Permissions
จำง่ายๆ: Authentication = ตรวจบัตร (คุณเป็นใคร) | Authorization = ตรวจสิทธิ์ (คุณเข้าห้องไหนได้บ้าง) ทั้งสองต้องทำงานร่วมกันเสมอ Authentication ต้องมาก่อน Authorization เสมอ

Session-Based Authentication

Session-based Authentication เป็นวิธีดั้งเดิมที่ใช้กันมานานตั้งแต่ยุคแรกของเว็บ หลักการทำงานคือ เมื่อผู้ใช้ Login สำเร็จ เซิร์ฟเวอร์จะสร้าง Session ขึ้นมาและเก็บข้อมูลไว้ฝั่ง Server (ในหน่วยความจำ ฐานข้อมูล หรือ Redis) จากนั้นส่ง Session ID กลับไปให้ Browser เก็บไว้ใน Cookie

ขั้นตอนการทำงาน

  1. ผู้ใช้ส่ง Username และ Password ไปที่เซิร์ฟเวอร์
  2. เซิร์ฟเวอร์ตรวจสอบ Credentials ถ้าถูกต้องจะสร้าง Session Object และเก็บไว้ใน Session Store
  3. เซิร์ฟเวอร์ส่ง Session ID กลับมาใน HTTP Response Header แบบ Set-Cookie: session_id=abc123; HttpOnly; Secure
  4. Browser จะแนบ Cookie นี้ไปกับทุก Request อัตโนมัติ
  5. เซิร์ฟเวอร์ดึง Session จาก Session Store เพื่อระบุตัวตนผู้ใช้
# Python Flask — Session-based Auth
from flask import Flask, session, request, jsonify
from werkzeug.security import check_password_hash
import secrets

app = Flask(__name__)
app.secret_key = secrets.token_hex(32)

# Session configuration
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SECURE'] = True
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
app.config['PERMANENT_SESSION_LIFETIME'] = 3600  # 1 hour

@app.route('/login', methods=['POST'])
def login():
    data = request.get_json()
    user = find_user(data['email'])
    if user and check_password_hash(user['password_hash'], data['password']):
        session['user_id'] = user['id']
        session['role'] = user['role']
        session.permanent = True
        return jsonify({"message": "Login successful"})
    return jsonify({"error": "Invalid credentials"}), 401

@app.route('/profile')
def profile():
    if 'user_id' not in session:
        return jsonify({"error": "Not authenticated"}), 401
    user = get_user_by_id(session['user_id'])
    return jsonify({"user": user})

@app.route('/logout', methods=['POST'])
def logout():
    session.clear()
    return jsonify({"message": "Logged out"})

ข้อดีและข้อเสียของ Session-based Auth

ข้อดีข้อเสีย
เซิร์ฟเวอร์ควบคุมได้ทั้งหมด สามารถยกเลิก Session ได้ทันทีต้องเก็บข้อมูลไว้ฝั่งเซิร์ฟเวอร์ ใช้หน่วยความจำมากขึ้น
ไม่ต้องส่งข้อมูลผู้ใช้ไปกับทุก Requestไม่เหมาะกับระบบที่มีหลาย Server (ต้องใช้ Shared Session Store)
Cookie มี Flag ป้องกันการโจรกรรม (HttpOnly, Secure)มีปัญหาเรื่อง CSRF ถ้าไม่ตั้งค่า SameSite Cookie
เหมาะกับ Server-side Rendered Applicationsไม่เหมาะกับ Mobile Apps หรือ Third-party APIs

Token-Based Authentication (JWT)

JSON Web Token (JWT) คือมาตรฐานเปิด (RFC 7519) สำหรับการส่งข้อมูลระหว่างสองฝ่ายอย่างปลอดภัยในรูปแบบ JSON Object ที่ถูก Sign ด้วย Digital Signature JWT เป็นที่นิยมอย่างมากในการทำ Authentication สำหรับ SPA (Single Page Application) และ Mobile Apps

โครงสร้างของ JWT

JWT ประกอบด้วย 3 ส่วนคั่นด้วยจุด: header.payload.signature

// ส่วนที่ 1: Header — ระบุ Algorithm ที่ใช้ Sign
{
  "alg": "HS256",
  "typ": "JWT"
}

// ส่วนที่ 2: Payload — ข้อมูลที่ต้องการส่ง (Claims)
{
  "sub": "1234567890",         // Subject (User ID)
  "name": "สมชาย ใจดี",
  "email": "somchai@example.com",
  "role": "admin",
  "iat": 1672531200,           // Issued At
  "exp": 1672534800,           // Expiration
  "iss": "myapp.com"           // Issuer
}

// ส่วนที่ 3: Signature — ป้องกันการปลอมแปลง
HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret_key
)
สำคัญมาก: JWT Payload ถูก Base64 Encode เท่านั้น ไม่ได้เข้ารหัส! ใครก็สามารถ Decode อ่านข้อมูลใน Payload ได้ อย่าใส่ข้อมูลลับเช่น รหัสผ่าน หมายเลขบัตรเครดิต ใน JWT เด็ดขาด Signature มีไว้ป้องกันการแก้ไข ไม่ใช่ป้องกันการอ่าน

JWT Auth Flow

  1. ผู้ใช้ Login ด้วย Email/Password
  2. เซิร์ฟเวอร์ตรวจสอบ ถ้าถูกต้องจะสร้าง JWT (Access Token + Refresh Token)
  3. ส่ง Token กลับให้ Client เก็บ
  4. Client แนบ Token ในทุก Request: Authorization: Bearer <token>
  5. เซิร์ฟเวอร์ Verify Signature และตรวจสอบ Claims (exp, iss, etc.)

การสร้าง JWT ด้วย Node.js

// Node.js — JWT Authentication
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');

const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET;
const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET;

// สร้าง Tokens
function generateTokens(user) {
  const accessToken = jwt.sign(
    { userId: user.id, email: user.email, role: user.role },
    ACCESS_SECRET,
    { expiresIn: '15m', issuer: 'myapp.com' }
  );

  const refreshToken = jwt.sign(
    { userId: user.id, tokenVersion: user.tokenVersion },
    REFRESH_SECRET,
    { expiresIn: '7d', issuer: 'myapp.com' }
  );

  return { accessToken, refreshToken };
}

// Login Endpoint
app.post('/api/login', async (req, res) => {
  const { email, password } = req.body;
  const user = await User.findOne({ email });

  if (!user || !await bcrypt.compare(password, user.passwordHash)) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  const tokens = generateTokens(user);

  // ส่ง Refresh Token เป็น httpOnly Cookie
  res.cookie('refreshToken', tokens.refreshToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'Strict',
    maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
    path: '/api/refresh'
  });

  res.json({ accessToken: tokens.accessToken });
});

// Middleware ตรวจสอบ JWT
function authMiddleware(req, res, next) {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'No token provided' });
  }

  try {
    const token = authHeader.split(' ')[1];
    const decoded = jwt.verify(token, ACCESS_SECRET, { issuer: 'myapp.com' });
    req.user = decoded;
    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired' });
    }
    return res.status(403).json({ error: 'Invalid token' });
  }
}

// Protected Route
app.get('/api/profile', authMiddleware, (req, res) => {
  res.json({ userId: req.user.userId, role: req.user.role });
});

การสร้าง JWT ด้วย Python

# Python FastAPI — JWT Authentication
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import jwt, JWTError
from passlib.context import CryptContext
from datetime import datetime, timedelta
import os

app = FastAPI()
security = HTTPBearer()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

SECRET_KEY = os.getenv("JWT_SECRET_KEY")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE = 15  # minutes

def create_access_token(data: dict):
    to_encode = data.copy()
    expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE)
    to_encode.update({"exp": expire, "iss": "myapp.com"})
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
    try:
        payload = jwt.decode(
            credentials.credentials, SECRET_KEY,
            algorithms=[ALGORITHM], options={"verify_iss": True},
            issuer="myapp.com"
        )
        return payload
    except JWTError:
        raise HTTPException(status_code=403, detail="Invalid token")

@app.post("/login")
async def login(email: str, password: str):
    user = await get_user(email)
    if not user or not pwd_context.verify(password, user.password_hash):
        raise HTTPException(status_code=401, detail="Invalid credentials")
    token = create_access_token({"sub": str(user.id), "role": user.role})
    return {"access_token": token, "token_type": "bearer"}

@app.get("/profile")
async def profile(payload: dict = Depends(verify_token)):
    return {"user_id": payload["sub"], "role": payload["role"]}

Refresh Token — ต่ออายุ Access Token

Access Token มีอายุสั้น (เช่น 15 นาที) เพื่อลดความเสี่ยงหากถูกขโมย แต่ผู้ใช้ไม่อยากต้อง Login ทุก 15 นาที ดังนั้นจึงใช้ Refresh Token ที่มีอายุยาวกว่า (เช่น 7 วัน หรือ 30 วัน) เพื่อขอ Access Token ใหม่โดยไม่ต้องใส่รหัสผ่านซ้ำ

// Refresh Token Endpoint
app.post('/api/refresh', async (req, res) => {
  const refreshToken = req.cookies.refreshToken;
  if (!refreshToken) return res.status(401).json({ error: 'No refresh token' });

  try {
    const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
    const user = await User.findById(decoded.userId);

    // ตรวจสอบ Token Version (ป้องกัน Token ที่ถูก Revoke)
    if (!user || user.tokenVersion !== decoded.tokenVersion) {
      return res.status(403).json({ error: 'Token revoked' });
    }

    const tokens = generateTokens(user);

    res.cookie('refreshToken', tokens.refreshToken, {
      httpOnly: true, secure: true, sameSite: 'Strict',
      maxAge: 7 * 24 * 60 * 60 * 1000, path: '/api/refresh'
    });

    res.json({ accessToken: tokens.accessToken });
  } catch (err) {
    return res.status(403).json({ error: 'Invalid refresh token' });
  }
});

// Revoke All Tokens (เช่น เมื่อเปลี่ยนรหัสผ่าน)
app.post('/api/revoke-tokens', authMiddleware, async (req, res) => {
  await User.findByIdAndUpdate(req.user.userId, {
    $inc: { tokenVersion: 1 }
  });
  res.json({ message: 'All tokens revoked' });
});

การเก็บ Token อย่างปลอดภัย

วิธีเก็บข้อดีข้อเสียเหมาะกับ
httpOnly CookieJavaScript อ่านไม่ได้ ป้องกัน XSSต้องระวัง CSRFWeb Apps (แนะนำ)
localStorageง่ายต่อการใช้งานเสี่ยง XSS มากไม่แนะนำ
sessionStorageหายเมื่อปิด Tabเสี่ยง XSS เหมือนกันไม่แนะนำ
In-Memory Variableปลอดภัยที่สุดจาก XSSหายเมื่อ Refresh หน้าSPA (ร่วมกับ Refresh Cookie)
Best Practice: เก็บ Access Token ไว้ใน Memory (JavaScript variable) และเก็บ Refresh Token ใน httpOnly Cookie เมื่อ Access Token หมดอายุหรือหน้าถูก Refresh ให้เรียก Refresh Endpoint เพื่อขอ Token ใหม่

OAuth 2.0 — มาตรฐานการ Authorization

OAuth 2.0 เป็นมาตรฐานเปิด (RFC 6749) ที่ออกแบบมาเพื่อให้แอปพลิเคชันสามารถเข้าถึงทรัพยากรของผู้ใช้โดยไม่ต้องรู้รหัสผ่าน ตัวอย่างที่เห็นบ่อยคือ "Login ด้วย Google" หรือ "Login ด้วย Facebook" ซึ่งแอปของเราสามารถเข้าถึงข้อมูลบางส่วนของผู้ใช้จาก Google/Facebook ได้โดยผู้ใช้ให้ความยินยอม

บทบาทใน OAuth 2.0

Authorization Code Flow (แนะนำสำหรับ Web Apps)

นี่คือ Flow ที่ปลอดภัยที่สุดและแนะนำให้ใช้กับ Server-side Web Applications ทุกกรณี

// ขั้นตอน Authorization Code Flow:
// 1. Client Redirect ผู้ใช้ไป Authorization Server
const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?
  client_id=${CLIENT_ID}&
  redirect_uri=${REDIRECT_URI}&
  response_type=code&
  scope=openid email profile&
  state=${generateRandomState()}&
  access_type=offline`;

// 2. ผู้ใช้ Login + Consent บน Google
// 3. Google Redirect กลับมาพร้อม Authorization Code
// GET /callback?code=AUTH_CODE&state=RANDOM_STATE

// 4. เซิร์ฟเวอร์แลก Code เป็น Token (Server-to-Server)
app.get('/callback', async (req, res) => {
  const { code, state } = req.query;

  // ตรวจสอบ state ป้องกัน CSRF
  if (state !== req.session.oauthState) {
    return res.status(403).send('Invalid state');
  }

  // แลก code เป็น tokens
  const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      code,
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,  // เก็บเป็นความลับ!
      redirect_uri: REDIRECT_URI,
      grant_type: 'authorization_code'
    })
  });

  const tokens = await tokenResponse.json();
  // tokens = { access_token, refresh_token, id_token, expires_in }

  // 5. ใช้ Access Token เข้าถึงข้อมูลผู้ใช้
  const userInfo = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', {
    headers: { Authorization: `Bearer ${tokens.access_token}` }
  }).then(r => r.json());

  // 6. สร้างหรืออัปเดตผู้ใช้ในระบบ
  const user = await upsertUser(userInfo);
  req.session.userId = user.id;
  res.redirect('/dashboard');
});

Authorization Code Flow + PKCE (สำหรับ SPA และ Mobile Apps)

PKCE (Proof Key for Code Exchange อ่านว่า "pixy") เป็น Extension ของ Authorization Code Flow ที่ออกแบบมาเพื่อป้องกัน Authorization Code Interception Attack สำหรับ Client ที่ไม่สามารถเก็บ Client Secret ได้อย่างปลอดภัย เช่น SPA และ Mobile Apps

// PKCE Flow สำหรับ SPA
// 1. สร้าง Code Verifier (Random String)
function generateCodeVerifier() {
  const array = new Uint8Array(32);
  crypto.getRandomValues(array);
  return base64UrlEncode(array);
}

// 2. สร้าง Code Challenge จาก Code Verifier
async function generateCodeChallenge(verifier) {
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const hash = await crypto.subtle.digest('SHA-256', data);
  return base64UrlEncode(new Uint8Array(hash));
}

// 3. เริ่ม Auth Flow
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
sessionStorage.setItem('codeVerifier', codeVerifier);

const authUrl = `https://auth.example.com/authorize?
  client_id=${CLIENT_ID}&
  redirect_uri=${REDIRECT_URI}&
  response_type=code&
  scope=openid profile&
  code_challenge=${codeChallenge}&
  code_challenge_method=S256&
  state=${generateState()}`;

window.location.href = authUrl;

// 4. แลก Code (พร้อม Code Verifier)
const codeVerifier = sessionStorage.getItem('codeVerifier');
const tokenResponse = await fetch('https://auth.example.com/token', {
  method: 'POST',
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code: authCode,
    redirect_uri: REDIRECT_URI,
    client_id: CLIENT_ID,
    code_verifier: codeVerifier  // พิสูจน์ว่าเป็นคนเดียวกัน
  })
});

Client Credentials Flow (Machine-to-Machine)

ใช้สำหรับการสื่อสารระหว่าง Service กับ Service โดยไม่มีผู้ใช้เกี่ยวข้อง เหมาะกับ Microservices, Background Jobs และ API Integrations

// Client Credentials Flow — ไม่มี User, Service กับ Service
const tokenResponse = await fetch('https://auth.example.com/token', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
    'Authorization': `Basic ${btoa(`${CLIENT_ID}:${CLIENT_SECRET}`)}`
  },
  body: new URLSearchParams({
    grant_type: 'client_credentials',
    scope: 'read:reports write:analytics'
  })
});
const { access_token } = await tokenResponse.json();

Implicit Flow (ไม่แนะนำแล้ว)

Implicit Flow เคยเป็น Flow ที่แนะนำสำหรับ SPA ในอดีต แต่ปัจจุบัน OAuth 2.1 ประกาศ Deprecate แล้ว เนื่องจากส่ง Access Token ผ่าน URL Fragment ซึ่งเสี่ยงต่อการรั่วไหลผ่าน Browser History, Referrer Header และ Log Files ให้ใช้ Authorization Code + PKCE แทนทุกกรณี

OpenID Connect (OIDC)

OpenID Connect เป็น Layer ที่สร้างอยู่บน OAuth 2.0 เพิ่มความสามารถด้าน Authentication เข้ามา ในขณะที่ OAuth 2.0 เน้นเรื่อง Authorization (การเข้าถึงทรัพยากร) OIDC เพิ่ม Identity Layer ทำให้รู้ตัวตนของผู้ใช้ได้ด้วย

OIDC เพิ่มสิ่งสำคัญเข้ามาคือ ID Token ซึ่งเป็น JWT ที่มีข้อมูลตัวตนของผู้ใช้ เช่น ชื่อ อีเมล รูปภาพ โดยที่ไม่ต้องเรียก API เพิ่มเติม นอกจากนี้ยังมี UserInfo Endpoint, Discovery Document และ Standard Scopes (openid, profile, email) ที่ทำให้การ Implement ง่ายและเป็นมาตรฐานเดียวกัน

// ID Token ที่ได้จาก OIDC มีข้อมูลเช่น:
{
  "iss": "https://accounts.google.com",
  "sub": "110169484474386276334",
  "aud": "YOUR_CLIENT_ID",
  "exp": 1672534800,
  "iat": 1672531200,
  "email": "user@gmail.com",
  "email_verified": true,
  "name": "สมชาย ใจดี",
  "picture": "https://lh3.googleusercontent.com/photo.jpg",
  "nonce": "random_nonce_value"
}

Social Login — Login ด้วย Google, Facebook, GitHub

Social Login เป็นการนำ OAuth 2.0 + OpenID Connect มาใช้กับ Identity Provider ที่นิยม ทำให้ผู้ใช้สามารถ Login ได้โดยไม่ต้องสร้าง Account ใหม่ ลด Friction ในการลงทะเบียนและเพิ่ม Conversion Rate ได้อย่างมาก

# Python — Social Login ด้วย Authlib
from authlib.integrations.starlette_client import OAuth

oauth = OAuth()
oauth.register(
    name='google',
    client_id=GOOGLE_CLIENT_ID,
    client_secret=GOOGLE_CLIENT_SECRET,
    server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',
    client_kwargs={'scope': 'openid email profile'}
)

@app.get('/login/google')
async def google_login(request):
    redirect_uri = request.url_for('google_callback')
    return await oauth.google.authorize_redirect(request, redirect_uri)

@app.get('/auth/google/callback')
async def google_callback(request):
    token = await oauth.google.authorize_access_token(request)
    userinfo = token.get('userinfo')

    # สร้างหรืออัปเดตผู้ใช้
    user = await User.get_or_create(
        provider='google',
        provider_id=userinfo['sub'],
        defaults={
            'email': userinfo['email'],
            'name': userinfo['name'],
            'avatar': userinfo.get('picture')
        }
    )

    # สร้าง Session หรือ JWT ของระบบเรา
    session_token = create_session(user)
    return RedirectResponse('/dashboard', cookies={'session': session_token})

RBAC และ ABAC — ระบบควบคุมสิทธิ์

RBAC (Role-Based Access Control)

RBAC เป็นระบบควบคุมสิทธิ์ที่กำหนดสิทธิ์ตาม Role (บทบาท) ของผู้ใช้ เช่น Admin, Editor, Viewer แต่ละ Role มี Permissions ที่กำหนดไว้ตายตัว ง่ายต่อการจัดการและเข้าใจ เหมาะกับระบบที่มีโครงสร้างสิทธิ์ไม่ซับซ้อนมากนัก

// RBAC Implementation
const ROLES = {
  admin:  ['read', 'write', 'delete', 'manage_users', 'view_analytics'],
  editor: ['read', 'write', 'delete'],
  author: ['read', 'write:own', 'delete:own'],
  viewer: ['read']
};

// Middleware ตรวจสอบ Permission
function requirePermission(permission) {
  return (req, res, next) => {
    const userRole = req.user.role;
    const permissions = ROLES[userRole] || [];

    if (permissions.includes(permission)) {
      return next();
    }

    // ตรวจสอบ :own permissions
    if (permissions.includes(`${permission}:own`)) {
      req.ownershipCheck = true;
      return next();
    }

    return res.status(403).json({ error: 'Insufficient permissions' });
  };
}

// ใช้งาน
app.delete('/api/articles/:id',
  authMiddleware,
  requirePermission('delete'),
  async (req, res) => {
    if (req.ownershipCheck) {
      const article = await Article.findById(req.params.id);
      if (article.authorId !== req.user.userId) {
        return res.status(403).json({ error: 'Not your article' });
      }
    }
    await Article.findByIdAndDelete(req.params.id);
    res.json({ message: 'Deleted' });
  }
);

ABAC (Attribute-Based Access Control)

ABAC มีความยืดหยุ่นมากกว่า RBAC โดยตัดสินใจจาก Attributes หลายตัวประกอบกัน เช่น ผู้ใช้ (department, level), ทรัพยากร (type, classification), สภาพแวดล้อม (เวลา, IP, location) และ Action ที่ต้องการทำ เหมาะกับระบบที่มีกฎควบคุมสิทธิ์ซับซ้อน

// ABAC Example — Policy-based Access Control
const policies = [
  {
    effect: 'allow',
    description: 'HR can view all employee records during business hours',
    condition: (subject, resource, action, environment) => {
      return subject.department === 'HR' &&
             resource.type === 'employee_record' &&
             action === 'read' &&
             environment.hour >= 9 && environment.hour <= 18;
    }
  },
  {
    effect: 'allow',
    description: 'Managers can approve reports of their own department',
    condition: (subject, resource, action, environment) => {
      return subject.role === 'manager' &&
             resource.type === 'report' &&
             action === 'approve' &&
             resource.department === subject.department;
    }
  }
];

function evaluateAccess(subject, resource, action) {
  const environment = {
    hour: new Date().getHours(),
    ip: subject.ip,
    dayOfWeek: new Date().getDay()
  };

  return policies.some(policy =>
    policy.effect === 'allow' &&
    policy.condition(subject, resource, action, environment)
  );
}

Multi-Factor Authentication (MFA/2FA)

MFA เป็นการเพิ่มชั้นความปลอดภัยโดยกำหนดให้ผู้ใช้ต้องยืนยันตัวตนมากกว่าหนึ่งวิธี โดยแบ่งเป็น 3 ปัจจัย ได้แก่ สิ่งที่คุณรู้ (Something You Know) เช่น รหัสผ่าน PIN สิ่งที่คุณมี (Something You Have) เช่น โทรศัพท์มือถือ Hardware Key และสิ่งที่คุณเป็น (Something You Are) เช่น ลายนิ้วมือ ใบหน้า

TOTP (Time-based One-Time Password)

# Python — TOTP Implementation ด้วย pyotp
import pyotp
import qrcode

# สร้าง Secret สำหรับผู้ใช้ (เก็บใน Database แบบเข้ารหัส)
def setup_totp(user):
    secret = pyotp.random_base32()
    totp = pyotp.TOTP(secret)

    # สร้าง QR Code สำหรับ Google Authenticator
    provisioning_uri = totp.provisioning_uri(
        name=user.email,
        issuer_name="MyApp"
    )

    qr = qrcode.make(provisioning_uri)
    qr.save(f"/tmp/qr_{user.id}.png")

    # เก็บ Secret ใน DB (ต้องเข้ารหัส!)
    user.totp_secret = encrypt(secret)
    user.save()

    return provisioning_uri

# ตรวจสอบ TOTP Code
def verify_totp(user, code):
    secret = decrypt(user.totp_secret)
    totp = pyotp.TOTP(secret)
    return totp.verify(code, valid_window=1)  # +/- 30 วินาที

# Login Flow พร้อม MFA
@app.post("/login")
async def login(email: str, password: str, totp_code: str = None):
    user = await get_user(email)
    if not verify_password(password, user.password_hash):
        raise HTTPException(status_code=401)

    if user.mfa_enabled:
        if not totp_code:
            return {"requires_mfa": True, "mfa_token": create_mfa_token(user)}
        if not verify_totp(user, totp_code):
            raise HTTPException(status_code=401, detail="Invalid MFA code")

    return {"access_token": create_access_token(user)}

Passkeys และ WebAuthn — อนาคตของ Authentication

Passkeys เป็นเทคโนโลยีใหม่ที่ถูกผลักดันโดย FIDO Alliance ร่วมกับ Apple, Google และ Microsoft เพื่อแทนที่รหัสผ่านแบบเดิมทั้งหมด Passkeys ใช้ Public Key Cryptography ที่ทำงานร่วมกับ Biometric Authentication ของอุปกรณ์ (ลายนิ้วมือ, Face ID, Windows Hello) ทำให้ปลอดภัยกว่ารหัสผ่านมากและไม่มีอะไรให้ Phishing ได้

ข้อดีของ Passkeys เหนือรหัสผ่าน

// WebAuthn Registration (Server-side — Node.js)
const {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  verifyAuthenticationResponse
} = require('@simplewebauthn/server');

const rpName = 'My App';
const rpID = 'myapp.com';
const origin = 'https://myapp.com';

// Registration — สร้าง Passkey ใหม่
app.post('/api/webauthn/register/options', authMiddleware, async (req, res) => {
  const user = await User.findById(req.user.userId);
  const existingKeys = await Credential.find({ userId: user.id });

  const options = await generateRegistrationOptions({
    rpName,
    rpID,
    userID: user.id,
    userName: user.email,
    attestationType: 'none',
    excludeCredentials: existingKeys.map(key => ({
      id: key.credentialID,
      type: 'public-key',
      transports: key.transports
    })),
    authenticatorSelection: {
      residentKey: 'preferred',
      userVerification: 'preferred'
    }
  });

  user.currentChallenge = options.challenge;
  await user.save();
  res.json(options);
});

// Verify Registration
app.post('/api/webauthn/register/verify', authMiddleware, async (req, res) => {
  const user = await User.findById(req.user.userId);

  const verification = await verifyRegistrationResponse({
    response: req.body,
    expectedChallenge: user.currentChallenge,
    expectedOrigin: origin,
    expectedRPID: rpID
  });

  if (verification.verified) {
    await Credential.create({
      userId: user.id,
      credentialID: verification.registrationInfo.credentialID,
      credentialPublicKey: verification.registrationInfo.credentialPublicKey,
      counter: verification.registrationInfo.counter,
      transports: req.body.response.transports
    });
    res.json({ verified: true });
  }
});

ช่องโหว่ที่พบบ่อยและวิธีป้องกัน

1. Token Theft (การขโมย Token)

ผู้โจมตีอาจขโมย JWT จาก localStorage ผ่าน XSS Attack หรือดักจับ Token จาก Network ที่ไม่เข้ารหัส

// ป้องกัน: ใช้ httpOnly Cookie + Short-lived Token
// ป้องกัน: ใช้ HTTPS เสมอ
// ป้องกัน: ตรวจสอบ Token Fingerprint
function createTokenWithFingerprint(user, req) {
  const fingerprint = crypto.randomBytes(32).toString('hex');
  const fingerprintHash = crypto.createHash('sha256')
    .update(fingerprint).digest('hex');

  const token = jwt.sign({
    userId: user.id,
    fingerprint: fingerprintHash
  }, SECRET);

  // ส่ง fingerprint เป็น httpOnly Cookie แยก
  return { token, fingerprint };
}

2. CSRF (Cross-Site Request Forgery)

เมื่อใช้ Cookie-based Auth ผู้โจมตีสามารถสร้างหน้าเว็บปลอมที่ส่ง Request ไปยังเว็บจริงโดยใช้ Cookie ของเหยื่อ

// ป้องกัน CSRF ด้วย Token
const csrf = require('csurf');
app.use(csrf({ cookie: { httpOnly: true, sameSite: 'Strict' } }));

// หรือใช้ SameSite Cookie (วิธีง่ายสุด)
res.cookie('session', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'Strict'  // ป้องกัน CSRF
});

// หรือใช้ Custom Header ตรวจสอบ Origin
function csrfProtection(req, res, next) {
  const origin = req.headers.origin || req.headers.referer;
  if (!origin || !origin.startsWith('https://myapp.com')) {
    return res.status(403).json({ error: 'CSRF detected' });
  }
  next();
}

3. Session Fixation

ผู้โจมตีตั้ง Session ID ไว้ล่วงหน้าในเบราว์เซอร์ของเหยื่อ เมื่อเหยื่อ Login สำเร็จ ผู้โจมตีก็ใช้ Session ID เดิมเข้าถึงระบบได้

// ป้องกัน: Regenerate Session ID หลัง Login
app.post('/login', (req, res) => {
  // ... ตรวจสอบ credentials ...

  // สร้าง Session ID ใหม่หลัง Login สำเร็จ!
  req.session.regenerate((err) => {
    req.session.userId = user.id;
    req.session.save(() => {
      res.json({ success: true });
    });
  });
});

4. Brute Force Attack

// ป้องกัน: Rate Limiting + Account Lockout
const rateLimit = require('express-rate-limit');

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,  // 15 นาที
  max: 5,                     // 5 ครั้ง
  message: { error: 'Too many attempts, try again after 15 minutes' },
  standardHeaders: true,
  legacyHeaders: false,
  keyGenerator: (req) => req.body.email || req.ip
});

app.post('/login', loginLimiter, loginHandler);

// + Account Lockout
async function checkAccountLock(email) {
  const attempts = await LoginAttempt.count({
    email,
    success: false,
    createdAt: { $gt: new Date(Date.now() - 30 * 60 * 1000) }
  });

  if (attempts >= 10) {
    throw new Error('Account locked for 30 minutes');
  }
}

Best Practices สำหรับ Authentication 2026

รหัสผ่าน

JWT

OAuth 2.0

General Security

เปรียบเทียบวิธี Authentication ทั้งหมด

วิธีความปลอดภัยความเหมาะสมความซับซ้อน
Session + Cookieสูง (ถ้าตั้งค่าถูก)Server-rendered Appsต่ำ
JWT Access TokenปานกลางSPA, Mobile, APIsปานกลาง
JWT + Refresh TokenสูงSPA, Mobile (แนะนำ)ปานกลาง-สูง
OAuth 2.0 + PKCEสูงมากThird-party Loginสูง
Passkeys/WebAuthnสูงที่สุดทุกประเภท (อนาคต)สูง
MFA (TOTP)เสริมความปลอดภัยใช้ร่วมกับวิธีอื่นปานกลาง

สรุป

Authentication และ Authorization เป็นหัวใจสำคัญของทุกแอปพลิเคชันที่มีผู้ใช้งาน การเข้าใจความแตกต่างระหว่าง Authentication กับ Authorization การเลือกวิธีที่เหมาะสม (Session vs JWT vs OAuth 2.0) และการ Implement อย่างปลอดภัยเป็นทักษะที่ Web Developer ทุกคนต้องมีในปี 2026

สิ่งที่สำคัญที่สุดคือ อย่าสร้างระบบ Authentication เอง ถ้าไม่จำเป็น ใช้ Library และ Service ที่ผ่านการทดสอบแล้ว เช่น NextAuth.js, Passport.js, Auth0, Firebase Auth, Supabase Auth หรือ Keycloak เพราะการเขียน Auth System จากศูนย์มีโอกาสพลาดสูงมาก และข้อผิดพลาดเพียงจุดเดียวอาจทำให้ข้อมูลผู้ใช้ทั้งระบบรั่วไหลได้

ในอนาคตอันใกล้ Passkeys จะเข้ามาแทนที่รหัสผ่านแบบเดิมมากขึ้นเรื่อยๆ เริ่มศึกษาและทดลอง Implement WebAuthn ตั้งแต่วันนี้ เพื่อให้แอปของคุณพร้อมสำหรับยุค Passwordless Authentication อย่างเต็มที่


Back to Blog | iCafe Forex | SiamLanCard | Siam2R