Passkeys WebAuthn

Passkeys WebAuthn FIDO2 Passwordless Biometric Public Key Cryptography Hexagonal Architecture Ports Adapters Domain-driven Design Authentication Security

MethodSecurityUXPhishingImplementation
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