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
- 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
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
