Passkeys WebAuthn
Passkeys WebAuthn FIDO2 Passwordless Biometric Public Key Cryptography Hexagonal Architecture Ports Adapters Domain-driven Design Authentication Security
| Method | Security | UX | Phishing | Implementation |
|---|---|---|---|---|
| Passkeys/WebAuthn | สูงมาก | ดีมาก | ป้องกัน | ปานกลาง |
| Password + 2FA | ดี | ปานกลาง | เสี่ยง | ง่าย |
| Magic Link | ดี | ดี | เสี่ยง | ง่าย |
| OAuth/SSO | ดี | ดีมาก | เสี่ยง | ปานกลาง |
WebAuthn Implementation
# === WebAuthn Registration & Login ===
# JavaScript — Registration (Client)
# async function register() {
# const options = await fetch('/api/webauthn/register/options', {
# method: 'POST',
# headers: { 'Content-Type': 'application/json' },
# body: JSON.stringify({ username: 'alice' }),
# }).then(r => r.json());
#
# // Browser calls Authenticator (Touch ID, Face ID, etc.)
# const credential = await navigator.credentials.create({
# publicKey: {
# challenge: base64ToBuffer(options.challenge),
# rp: { name: "My App", id: "example.com" },
# user: {
# id: base64ToBuffer(options.userId),
# name: "alice",
# displayName: "Alice",
# },
# pubKeyCredParams: [
# { alg: -7, type: "public-key" }, // ES256
# { alg: -257, type: "public-key" }, // RS256
# ],
# authenticatorSelection: {
# authenticatorAttachment: "platform",
# residentKey: "required",
# userVerification: "required",
# },
# },
# });
#
# // Send credential to server
# await fetch('/api/webauthn/register/verify', {
# method: 'POST',
# body: JSON.stringify(serializeCredential(credential)),
# });
# }
# JavaScript — Login (Client)
# async function login() {
# const options = await fetch('/api/webauthn/login/options')
# .then(r => r.json());
#
# const assertion = await navigator.credentials.get({
# publicKey: {
# challenge: base64ToBuffer(options.challenge),
# rpId: "example.com",
# userVerification: "required",
# },
# });
#
# const result = await fetch('/api/webauthn/login/verify', {
# method: 'POST',
# body: JSON.stringify(serializeAssertion(assertion)),
# }).then(r => r.json());
#
# console.log('Logged in:', result.user);
# }
from dataclasses import dataclass
from typing import List
@dataclass
class PasskeyCredential:
user: str
credential_id: str
device: str
authenticator: str
created: str
last_used: str
credentials = [
PasskeyCredential("alice", "cred_001", "iPhone 15", "Face ID", "2024-01-15", "2024-02-20"),
PasskeyCredential("alice", "cred_002", "MacBook Pro", "Touch ID", "2024-01-15", "2024-02-19"),
PasskeyCredential("bob", "cred_003", "Pixel 8", "Fingerprint", "2024-02-01", "2024-02-20"),
PasskeyCredential("bob", "cred_004", "Windows PC", "Windows Hello", "2024-02-01", "2024-02-18"),
]
print("=== Registered Passkeys ===")
for c in credentials:
print(f" [{c.user}] {c.device} ({c.authenticator})")
print(f" ID: {c.credential_id} | Created: {c.created} | Last: {c.last_used}")
Hexagonal Architecture
# === Hexagonal Architecture for Auth ===
# Architecture:
# ┌─────────────────────────────────────┐
# │ Adapters (Inbound) │
# │ REST API │ GraphQL │ gRPC │
# ├───────────┴─────────┴───────────────┤
# │ Ports (Inbound) │
# │ AuthService │ UserService │
# ├─────────────────────────────────────┤
# │ Domain (Core) │
# │ User │ Credential │ Challenge │
# │ AuthPolicy │ WebAuthnVerifier │
# ├─────────────────────────────────────┤
# │ Ports (Outbound) │
# │ UserRepo │ CredentialRepo │ Cache │
# ├─────────────────────────────────────┤
# │ Adapters (Outbound) │
# │ PostgreSQL │ Redis │ S3 │
# └─────────────────────────────────────┘
# Python — Domain Layer
# @dataclass
# class User:
# id: str
# username: str
# display_name: str
# credentials: List[WebAuthnCredential]
#
# @dataclass
# class WebAuthnCredential:
# credential_id: bytes
# public_key: bytes
# sign_count: int
# device_name: str
#
# class AuthService: # Port (Inbound)
# def __init__(self, user_repo, credential_repo, challenge_cache):
# self.user_repo = user_repo # Port (Outbound)
# self.credential_repo = credential_repo
# self.challenge_cache = challenge_cache
#
# def generate_registration_options(self, username):
# user = self.user_repo.find_by_username(username)
# challenge = os.urandom(32)
# self.challenge_cache.store(user.id, challenge, ttl=300)
# return RegistrationOptions(challenge, user)
#
# def verify_registration(self, user_id, credential_data):
# challenge = self.challenge_cache.get(user_id)
# credential = WebAuthnVerifier.verify(credential_data, challenge)
# self.credential_repo.save(user_id, credential)
# Adapters
# class PostgresUserRepo: # Adapter for UserRepo Port
# def find_by_username(self, username):
# row = db.execute("SELECT * FROM users WHERE username = ?", username)
# return User(**row)
#
# class RedisChallengeCashe: # Adapter for ChallengeCache Port
# def store(self, user_id, challenge, ttl):
# redis.setex(f"challenge:{user_id}", ttl, challenge)
layers = {
"Domain": ["User", "Credential", "Challenge", "AuthPolicy", "WebAuthnVerifier"],
"Inbound Ports": ["AuthService", "UserService", "SessionService"],
"Outbound Ports": ["UserRepository", "CredentialRepository", "ChallengeCache"],
"Inbound Adapters": ["REST Controller", "GraphQL Resolver", "gRPC Handler"],
"Outbound Adapters": ["PostgreSQL Repo", "Redis Cache", "S3 Storage"],
}
print("\n=== Hexagonal Layers ===")
for layer, components in layers.items():
print(f" [{layer}]: {', '.join(components)}")
Production Setup
# === Production Passkeys ===
# Python Server — py_webauthn library
# pip install py-webauthn
#
# from webauthn import (
# generate_registration_options,
# verify_registration_response,
# generate_authentication_options,
# verify_authentication_response,
# )
#
# # Registration
# options = generate_registration_options(
# rp_id="example.com",
# rp_name="My App",
# user_id=user.id.encode(),
# user_name=user.username,
# user_display_name=user.display_name,
# )
#
# # Verification
# verification = verify_registration_response(
# credential=credential_data,
# expected_challenge=stored_challenge,
# expected_rp_id="example.com",
# expected_origin="https://example.com",
# )
auth_metrics = {
"Total Users": "15,000",
"Passkey Enrolled": "8,500 (56.7%)",
"Password Only": "6,500 (43.3%)",
"Login Success Rate (Passkey)": "99.2%",
"Login Success Rate (Password)": "87.5%",
"Avg Login Time (Passkey)": "1.2 seconds",
"Avg Login Time (Password)": "8.5 seconds",
"Phishing Incidents (Passkey)": "0",
"Phishing Incidents (Password)": "12/month",
"Support Tickets (Password Reset)": "Down 75%",
}
print("Auth Dashboard:")
for k, v in auth_metrics.items():
print(f" {k}: {v}")
# Migration Strategy
migration = [
"Phase 1: เพิ่ม Passkey เป็น Optional (Password ยังใช้ได้)",
"Phase 2: แนะนำ Passkey เมื่อ Login ด้วย Password",
"Phase 3: Passkey เป็น Default (Password เป็น Fallback)",
"Phase 4: Password-only accounts ต้องเพิ่ม Passkey",
"Phase 5: ปิด Password Login สำหรับ Account ที่มี Passkey",
]
print(f"\n\nMigration Strategy:")
for i, step in enumerate(migration, 1):
print(f" {step}")
เคล็ดลับ
- Progressive: เพิ่ม Passkey ทีละ Phase ไม่บังคับทันที
- Fallback: เก็บ Password เป็น Fallback ช่วงแรก
- Multi-device: ให้ User ลงทะเบียนหลาย Device
- Hexagonal: แยก Auth Logic ออกจาก Framework เปลี่ยนได้
- Testing: Mock Authenticator สำหรับ Integration Test
Passkeys คืออะไร
Passwordless Public Key Biometric Face ID Touch ID FIDO2 WebAuthn ไม่ Phishing ไม่ Leak Apple Google Microsoft Sync
WebAuthn ทำงานอย่างไร
Browser credentials.create() Authenticator Key Pair Private Key อุปกรณ์ Public Key Server Challenge Sign Verify Standard
Hexagonal Architecture คืออะไร
Ports Adapters แยก Business Logic Infrastructure Domain ตรงกลาง Interface Implementation เปลี่ยน DB ไม่กระทบ Mock Test Clean
Passkeys ปลอดภัยกว่า Password อย่างไร
ไม่ Leak ไม่ Phishing Origin Bound Public Key Private Key อุปกรณ์ Biometric Challenge Replay ไม่ Brute Force ไม่จำ
สรุป
Passkeys WebAuthn FIDO2 Passwordless Biometric Hexagonal Architecture Ports Adapters Domain Public Key Cryptography Challenge Verify Registration Login Security
