SiamCafe · Blog
Passkeys WebAuthn Hexagonal Architecture — Passwordless Authentication
บทความ

Passkeys WebAuthn Hexagonal Architecture — Passwordless Authentication

เผยแพร่ 28 พฤษภาคม 2569

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

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