Passkeys WebAuthn กับ CDN Configuration —
Passkeys และ WebAuthn

Passkeys เป็นวิธี Login แบบ Passwordless ที่ใช้ Biometrics หรือ Device PIN แทน Password ทำงานบน WebAuthn Standard ใช้ Public Key Cryptography ปลอดภัยจาก Phishing, Credential Stuffing และ Brute Force
เนื้อหาเกี่ยวข้อง — ทำความเข้าใจ windows server 2022 คือ
CDN ช่วยเพิ่ม Performance สำหรับ Authentication Flow Cache Static Assets, Edge Functions สำหรับ Token Validation, WAF ป้องกัน Attacks, Rate Limiting ที่ Edge
เนื้อหาเกี่ยวข้อง — ทำความเข้าใจ seo website คือ — ข้อมูลครบถ้วน 2026
WebAuthn Server Implementation
# webauthn_server.py — WebAuthn Server ด้วย Python
# pip install py_webauthn flask redis
from flask import Flask, request, jsonify, session
from webauthn import (
generate_registration_options,
verify_registration_response,
generate_authentication_options,
verify_authentication_response,
)
from webauthn.helpers.structs import (
AuthenticatorSelectionCriteria,
ResidentKeyRequirement,
UserVerificationRequirement,
PublicKeyCredentialDescriptor,
)
from webauthn.helpers.cose import COSEAlgorithmIdentifier
import json
import redis
import os
app = Flask(__name__)
app.secret_key = os.urandom(32)
# Redis สำหรับเก็บ Challenges และ Credentials
r = redis.Redis(host="localhost", port=6379, db=0)
RP_ID = "example.com"
RP_NAME = "Example App"
ORIGIN = "https://example.com"
# === Registration ===
@app.route("/api/register/options", methods=["POST"])
def register_options():
"""สร้าง Registration Options"""
data = request.json
user_id = data["user_id"]
username = data["username"]
# ดึง Existing Credentials
existing = get_user_credentials(user_id)
exclude = [
PublicKeyCredentialDescriptor(id=cred["id"])
for cred in existing
]
options = generate_registration_options(
rp_id=RP_ID,
rp_name=RP_NAME,
user_id=user_id.encode(),
user_name=username,
user_display_name=username,
authenticator_selection=AuthenticatorSelectionCriteria(
resident_key=ResidentKeyRequirement.REQUIRED,
user_verification=UserVerificationRequirement.REQUIRED,
),
supported_pub_key_algs=[
COSEAlgorithmIdentifier.ECDSA_SHA_256,
COSEAlgorithmIdentifier.RSASSA_PKCS1_v1_5_SHA_256,
],
exclude_credentials=exclude,
timeout=60000,
)
# เก็บ Challenge ใน Redis (expire 5 นาที)
r.setex(f"challenge:{user_id}", 300, options.challenge)
return jsonify(options)
@app.route("/api/register/verify", methods=["POST"])
def register_verify():
"""Verify Registration Response"""
data = request.json
user_id = data["user_id"]
challenge = r.get(f"challenge:{user_id}")
if not challenge:
return jsonify({"error": "Challenge expired"}), 400
try:
verification = verify_registration_response(
credential=data["credential"],
expected_challenge=challenge,
expected_rp_id=RP_ID,
expected_origin=ORIGIN,
)
# เก็บ Credential
save_credential(user_id, {
"id": verification.credential_id,
"public_key": verification.credential_public_key,
"sign_count": verification.sign_count,
})
r.delete(f"challenge:{user_id}")
return jsonify({"success": True})
except Exception as e:
return jsonify({"error": str(e)}), 400
# === Authentication ===
@app.route("/api/login/options", methods=["POST"])
def login_options():
"""สร้าง Authentication Options"""
data = request.json
user_id = data.get("user_id", "")
credentials = get_user_credentials(user_id)
allow = [
PublicKeyCredentialDescriptor(id=cred["id"])
for cred in credentials
]
options = generate_authentication_options(
rp_id=RP_ID,
allow_credentials=allow if allow else None,
user_verification=UserVerificationRequirement.REQUIRED,
timeout=60000,
)
r.setex(f"auth_challenge:{user_id}", 300, options.challenge)
return jsonify(options)
@app.route("/api/login/verify", methods=["POST"])
def login_verify():
"""Verify Authentication Response"""
data = request.json
user_id = data["user_id"]
challenge = r.get(f"auth_challenge:{user_id}")
credential = get_credential_by_id(user_id, data["credential"]["id"])
try:
verification = verify_authentication_response(
credential=data["credential"],
expected_challenge=challenge,
expected_rp_id=RP_ID,
expected_origin=ORIGIN,
credential_public_key=credential["public_key"],
credential_current_sign_count=credential["sign_count"],
)
# อัพเดท Sign Count
update_sign_count(user_id, data["credential"]["id"],
verification.new_sign_count)
session["user_id"] = user_id
r.delete(f"auth_challenge:{user_id}")
return jsonify({"success": True, "user_id": user_id})
except Exception as e:
return jsonify({"error": str(e)}), 401
def get_user_credentials(user_id):
data = r.get(f"creds:{user_id}")
return json.loads(data) if data else []
def save_credential(user_id, cred):
creds = get_user_credentials(user_id)
creds.append(cred)
r.set(f"creds:{user_id}", json.dumps(creds))
def get_credential_by_id(user_id, cred_id):
creds = get_user_credentials(user_id)
return next((c for c in creds if c["id"] == cred_id), None)
def update_sign_count(user_id, cred_id, new_count):
creds = get_user_credentials(user_id)
for c in creds:
if c["id"] == cred_id:
c["sign_count"] = new_count
r.set(f"creds:{user_id}", json.dumps(creds))
# if __name__ == "__main__":
# app.run(host="0.0.0.0", port=5000, ssl_context="adhoc")
Frontend WebAuthn Client

// webauthn_client.js — WebAuthn Frontend Client
class PasskeyAuth {
constructor(apiBase = '/api') {
this.apiBase = apiBase;
}
// === Registration ===
async register(userId, username) {
// 1. ขอ Options จาก Server
const optionsRes = await fetch(`/register/options`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_id: userId, username }),
});
const options = await optionsRes.json();
// 2. แปลง Base64 เป็น ArrayBuffer
options.challenge = this.base64ToBuffer(options.challenge);
options.user.id = this.base64ToBuffer(options.user.id);
// 3. เรียก Browser WebAuthn API
const credential = await navigator.credentials.create({
publicKey: options,
});
// 4. ส่งผลลัพธ์กลับ Server
const verifyRes = await fetch(`/register/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
user_id: userId,
credential: {
id: credential.id,
rawId: this.bufferToBase64(credential.rawId),
response: {
attestationObject: this.bufferToBase64(
credential.response.attestationObject
),
clientDataJSON: this.bufferToBase64(
credential.response.clientDataJSON
),
},
type: credential.type,
},
}),
});
return verifyRes.json();
}
// === Authentication ===
async login(userId = '') {
const optionsRes = await fetch(`/login/options`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_id: userId }),
});
const options = await optionsRes.json();
options.challenge = this.base64ToBuffer(options.challenge);
if (options.allowCredentials) {
options.allowCredentials = options.allowCredentials.map(c => ({
...c, id: this.base64ToBuffer(c.id),
}));
}
const assertion = await navigator.credentials.get({
publicKey: options,
});
const verifyRes = await fetch(`/login/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
user_id: userId,
credential: {
id: assertion.id,
rawId: this.bufferToBase64(assertion.rawId),
response: {
authenticatorData: this.bufferToBase64(
assertion.response.authenticatorData
),
clientDataJSON: this.bufferToBase64(
assertion.response.clientDataJSON
),
signature: this.bufferToBase64(assertion.response.signature),
},
type: assertion.type,
},
}),
});
return verifyRes.json();
}
// === Helpers ===
base64ToBuffer(base64) {
const binary = atob(base64.replace(/-/g, '+').replace(/_/g, '/'));
return Uint8Array.from(binary, c => c.charCodeAt(0)).buffer;
}
bufferToBase64(buffer) {
return btoa(String.fromCharCode(...new Uint8Array(buffer)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
// === Check Support ===
static isSupported() {
return window.PublicKeyCredential !== undefined;
}
static async isPlatformSupported() {
if (!this.isSupported()) return false;
return PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
}
}
// const auth = new PasskeyAuth();
// await auth.register('user123', 'john@example.com');
// await auth.login('user123');
CDN Configuration
# === CDN Configuration สำหรับ Authentication ===
# 1. Cloudflare Configuration
# cloudflare-workers/auth-edge.js
#
# export default {
# async fetch(request, env) {
# const url = new URL(request.url);
#
# // Rate Limiting สำหรับ Auth Endpoints
# if (url.pathname.startsWith('/api/login') ||
# url.pathname.startsWith('/api/register')) {
# const ip = request.headers.get('CF-Connecting-IP');
# const key = `rate::`;
# const count = await env.RATE_LIMIT.get(key);
#
# if (count && parseInt(count) > 10) {
# return new Response('Too Many Requests', { status: 429 });
# }
#
# await env.RATE_LIMIT.put(key, (parseInt(count || 0) + 1).toString(),
# { expirationTtl: 60 });
# }
#
# // JWT Validation ที่ Edge
# if (url.pathname.startsWith('/api/protected')) {
# const token = request.headers.get('Authorization')?.split(' ')[1];
# if (!token) {
# return new Response('Unauthorized', { status: 401 });
# }
# // Validate JWT at edge
# }
#
# return fetch(request);
# }
# };
# 2. Nginx CDN Cache Configuration
# /etc/nginx/conf.d/auth-cdn.conf
# # Cache Static Assets
# location ~* \.(js|css|png|jpg|svg|woff2)$ {
# proxy_cache static_cache;
# proxy_cache_valid 200 1d;
# add_header X-Cache $upstream_cache_status;
# add_header Cache-Control "public, max-age=86400";
# }
#
# # No Cache สำหรับ Auth Endpoints
# location /api/login {
# proxy_pass http://auth-backend;
# proxy_no_cache 1;
# proxy_cache_bypass 1;
# add_header Cache-Control "no-store";
#
# # Rate Limiting
# limit_req zone=auth_limit burst=10 nodelay;
# }
#
# location /api/register {
# proxy_pass http://auth-backend;
# proxy_no_cache 1;
# add_header Cache-Control "no-store";
# limit_req zone=auth_limit burst=5 nodelay;
# }
#
# # Security Headers
# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# add_header X-Content-Type-Options "nosniff" always;
# add_header X-Frame-Options "DENY" always;
# add_header Content-Security-Policy "default-src 'self'" always;
# 3. Rate Limit Zone
# limit_req_zone $binary_remote_addr zone=auth_limit:10m rate=5r/s;
echo "CDN Configuration:"
echo " Static Assets: Cache 1 day"
echo " Auth Endpoints: No cache, Rate limited"
echo " Edge: JWT validation, Rate limiting"
echo " Security: HSTS, CSP, X-Frame-Options"
Best Practices
- Resident Keys: ใช้ Resident Keys (Discoverable Credentials) สำหรับ Usernameless Login
- User Verification: ตั้ง User Verification เป็น Required เสมอ
- Challenge Expiry: ตั้ง Challenge ให้ Expire ใน 5 นาที เก็บใน Redis
- Sign Count: ตรวจสอบ Sign Count ป้องกัน Cloned Authenticators
- CDN No-cache Auth: ไม่ Cache Auth Endpoints ที่ CDN
- Rate Limiting: ตั้ง Rate Limit สำหรับ Login/Register ที่ CDN Edge
Passkeys คืออะไร
วิธี Login Passwordless ใช้ Biometrics ลายนิ้วมือ Face ID Device PIN แทน Password ทำงานบน WebAuthn ปลอดภัยกว่า Password ไม่ Phishing ไม่ Leak Cross-device iCloud Google Password Manager
แนะนำเพิ่มเติม — อ่านเพิ่มเติมที่ SiamCafeBook
เนื้อหาเกี่ยวข้อง — ดูเพิ่มเติมเรื่อง Zipkin Tracing IoT Gateway





