SiamCafe.net Blog
Cybersecurity

Passkeys WebAuthn CDN Configuration

passkeys webauthn cdn configuration
Passkeys WebAuthn CDN Configuration | SiamCafe Blog
2026-03-26· อ. บอม — SiamCafe.net· 10,233 คำ

Passkeys และ WebAuthn

Passkeys เป็นวิธี Login แบบ Passwordless ที่ใช้ Biometrics หรือ Device PIN แทน Password ทำงานบน WebAuthn Standard ใช้ Public Key Cryptography ปลอดภัยจาก Phishing, Credential Stuffing และ Brute Force

CDN ช่วยเพิ่ม Performance สำหรับ Authentication Flow Cache Static Assets, Edge Functions สำหรับ Token Validation, WAF ป้องกัน Attacks, Rate Limiting ที่ Edge

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

Passkeys คืออะไร

วิธี Login Passwordless ใช้ Biometrics ลายนิ้วมือ Face ID Device PIN แทน Password ทำงานบน WebAuthn ปลอดภัยกว่า Password ไม่ Phishing ไม่ Leak Cross-device iCloud Google Password Manager

WebAuthn คืออะไร

W3C Standard Passwordless Authentication ใช้ Public Key Cryptography Browser สร้าง Key Pair Private Key เก็บใน Device Public Key ส่ง Server ไม่มี Shared Secret ปลอดภัยจาก Phishing

CDN ช่วย Authentication ได้อย่างไร

Cache Static Assets ลด Latency Login Page Edge Functions validate tokens WAF ป้องกัน Brute Force Rate Limiting ที่ Edge TLS Termination ลด Load Origin

Passkeys ปลอดภัยกว่า Password อย่างไร

Public Key Cryptography ไม่มี Shared Secret ไม่โดน Phishing Browser ตรวจ Origin อัตโนมัติ ไม่โดน Credential Stuffing ไม่มี Password Reuse ไม่โดน Brute Force Cryptographic Challenge

สรุป

Passkeys และ WebAuthn เป็นอนาคตของ Authentication ปลอดภัยกว่า Password ไม่โดน Phishing CDN ช่วย Performance Cache Static Assets Rate Limiting ที่ Edge JWT Validation Security Headers ใช้ Resident Keys สำหรับ Usernameless Login Challenge Expire ใน Redis

📖 บทความที่เกี่ยวข้อง

Passkeys WebAuthn Open Source Contributionอ่านบทความ → Passkeys WebAuthn Machine Learning Pipelineอ่านบทความ → Passkeys WebAuthn CI CD Automation Pipelineอ่านบทความ → Passkeys WebAuthn Career Development ITอ่านบทความ → Passkeys WebAuthn Domain Driven Design DDDอ่านบทความ →

📚 ดูบทความทั้งหมด →

💡 แนะนำ: หากต้องการศึกษาเพิ่มเติมลองดูที่