ในโลกของ 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 สามารถจัดการผู้ใช้คนอื่นได้ เป็นต้น
| หัวข้อ | Authentication | Authorization |
|---|---|---|
| คำถาม | คุณคือใคร? | คุณทำอะไรได้บ้าง? |
| เกิดเมื่อไหร่ | ก่อน Authorization | หลัง Authentication |
| ตัวอย่าง | Login ด้วย Email/Password | ตรวจสอบ Role/Permission |
| โปรโตคอล | OpenID Connect, SAML | OAuth 2.0, RBAC, ABAC |
| ข้อมูลที่ใช้ | Credentials (รหัสผ่าน, Biometric) | Policies, Roles, Permissions |
Session-Based Authentication
Session-based Authentication เป็นวิธีดั้งเดิมที่ใช้กันมานานตั้งแต่ยุคแรกของเว็บ หลักการทำงานคือ เมื่อผู้ใช้ Login สำเร็จ เซิร์ฟเวอร์จะสร้าง Session ขึ้นมาและเก็บข้อมูลไว้ฝั่ง Server (ในหน่วยความจำ ฐานข้อมูล หรือ Redis) จากนั้นส่ง Session ID กลับไปให้ Browser เก็บไว้ใน Cookie
ขั้นตอนการทำงาน
- ผู้ใช้ส่ง Username และ Password ไปที่เซิร์ฟเวอร์
- เซิร์ฟเวอร์ตรวจสอบ Credentials ถ้าถูกต้องจะสร้าง Session Object และเก็บไว้ใน Session Store
- เซิร์ฟเวอร์ส่ง Session ID กลับมาใน HTTP Response Header แบบ
Set-Cookie: session_id=abc123; HttpOnly; Secure - Browser จะแนบ Cookie นี้ไปกับทุก Request อัตโนมัติ
- เซิร์ฟเวอร์ดึง 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 Auth Flow
- ผู้ใช้ Login ด้วย Email/Password
- เซิร์ฟเวอร์ตรวจสอบ ถ้าถูกต้องจะสร้าง JWT (Access Token + Refresh Token)
- ส่ง Token กลับให้ Client เก็บ
- Client แนบ Token ในทุก Request:
Authorization: Bearer <token> - เซิร์ฟเวอร์ 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 Cookie | JavaScript อ่านไม่ได้ ป้องกัน XSS | ต้องระวัง CSRF | Web Apps (แนะนำ) |
| localStorage | ง่ายต่อการใช้งาน | เสี่ยง XSS มาก | ไม่แนะนำ |
| sessionStorage | หายเมื่อปิด Tab | เสี่ยง XSS เหมือนกัน | ไม่แนะนำ |
| In-Memory Variable | ปลอดภัยที่สุดจาก XSS | หายเมื่อ Refresh หน้า | SPA (ร่วมกับ Refresh Cookie) |
OAuth 2.0 — มาตรฐานการ Authorization
OAuth 2.0 เป็นมาตรฐานเปิด (RFC 6749) ที่ออกแบบมาเพื่อให้แอปพลิเคชันสามารถเข้าถึงทรัพยากรของผู้ใช้โดยไม่ต้องรู้รหัสผ่าน ตัวอย่างที่เห็นบ่อยคือ "Login ด้วย Google" หรือ "Login ด้วย Facebook" ซึ่งแอปของเราสามารถเข้าถึงข้อมูลบางส่วนของผู้ใช้จาก Google/Facebook ได้โดยผู้ใช้ให้ความยินยอม
บทบาทใน OAuth 2.0
- Resource Owner: ผู้ใช้ที่เป็นเจ้าของข้อมูล (เช่น คุณเป็นเจ้าของ Google Account)
- Client: แอปพลิเคชันที่ต้องการเข้าถึงข้อมูล (เช่น เว็บไซต์ที่คุณกำลังใช้งาน)
- Authorization Server: เซิร์ฟเวอร์ที่ออก Token (เช่น accounts.google.com)
- Resource Server: เซิร์ฟเวอร์ที่เก็บข้อมูลที่ต้องการเข้าถึง (เช่น Google APIs)
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 เหนือรหัสผ่าน
- ไม่มี Phishing: Passkey ผูกกับ Domain เฉพาะ ใช้กับเว็บปลอมไม่ได้
- ไม่มี Password Leak: ไม่มีรหัสผ่านเก็บบน Server
- ง่ายกว่า: แค่สแกนนิ้วหรือหน้า ไม่ต้องจำรหัสผ่าน
- ป้องกัน Replay Attack: ทุก Challenge ไม่ซ้ำกัน
- Sync ข้ามอุปกรณ์: iCloud Keychain, Google Password Manager
// 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
รหัสผ่าน
- ใช้ bcrypt, scrypt หรือ Argon2id ในการ Hash รหัสผ่าน อย่าใช้ MD5 หรือ SHA-256 โดยตรงเด็ดขาด
- กำหนดความยาวขั้นต่ำ 12 ตัวอักษร ไม่จำกัดตัวอักษรพิเศษ อย่าบังคับเปลี่ยนรหัสผ่านเป็นระยะ (NIST 800-63b)
- ตรวจสอบกับรายการรหัสผ่านที่เคยรั่วไหล (Have I Been Pwned API)
- ไม่ให้ข้อมูลว่า Email หรือ Password ผิด ตอบแค่ "Invalid credentials" เพื่อป้องกัน User Enumeration
JWT
- ใช้ Algorithm ที่เหมาะสม RS256 สำหรับระบบ Distributed, HS256 สำหรับระบบเดียว อย่าใช้ "none" Algorithm เด็ดขาด
- ตั้ง Expiration สั้น (15 นาทีสำหรับ Access Token) และใช้ Refresh Token สำหรับการต่ออายุ
- Validate ทุก Claim ที่สำคัญ ได้แก่ iss, aud, exp, nbf
- อย่าเก็บข้อมูลที่เป็นความลับใน JWT Payload เพราะใครก็ Decode อ่านได้
OAuth 2.0
- ใช้ Authorization Code + PKCE เสมอ ทั้ง Web และ Mobile ลืม Implicit Flow ไปเลย
- ตรวจสอบ State Parameter ทุกครั้งเพื่อป้องกัน CSRF
- เก็บ Client Secret ฝั่ง Server เท่านั้น อย่าฝังไว้ใน Frontend Code
- กำหนด Scope ให้น้อยที่สุดเท่าที่จำเป็น (Principle of Least Privilege)
General Security
- ใช้ HTTPS ทุกที่ ทุก Environment รวมถึง Development
- Implement Rate Limiting สำหรับ Login Endpoints
- Log ทุก Authentication Event สำหรับ Audit Trail
- แจ้งเตือนผู้ใช้เมื่อมี Login จากอุปกรณ์หรือ Location ใหม่
- มี Account Recovery Flow ที่ปลอดภัย ใช้ Time-limited Token ส่งทาง Email ที่ลงทะเบียนไว้
- พิจารณาใช้ Passkeys เป็น Primary Authentication Method ในโปรเจกต์ใหม่ปี 2026
เปรียบเทียบวิธี 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 อย่างเต็มที่
