Authentication models: password + TOTP, Passkey enrollment + login, SSO via OIDC (Authorization Code + PKCE), m2m API key, mTLS service mesh, plus phishing/SIM-swap anti-patterns. Concept lesson for security curriculum.
Authentication отвечает на вопрос «кто ты». Это первая и самая атакуемая граница системы: 90% breaches по Verizon DBIR начинаются с компрометации credential (phishing, credential stuffing, SIM-swap, MFA fatigue). Если AuthN продавлен — дальше не помогут ни ACL, ни шифрование, ни аудит.
Путать AuthN с AuthZ — классический баг: проверили JWT signature (AuthN ok), но забыли что user должен быть owner ресурса (AuthZ fail) → IDOR. AuthN говорит «это Alice», AuthZ говорит «Alice может это сделать». Разные системы, разные failure modes.
AuthN строится из трёх осей, которые композируются независимо:
Главный принцип современного AuthN: shared secret = phishable. Пароль и TOTP — это shared secrets, которые user может ввести на фишинг-сайте. WebAuthn/Passkey ломает эту модель: private key никогда не покидает Secure Enclave, browser binds challenge к origin, фишинг становится невозможен на уровне протокола, а не на уровне user education.
NIST SP 800-63B вводит градацию AAL1/AAL2/AAL3 (Authenticator Assurance Level). Password = AAL1. Password+TOTP = AAL2. Hardware key или passkey c verified user presence = AAL3. PCI-DSS, HIPAA, SOC2, PSD2 — все мапятся на эту шкалу.
Топология собрана из четырёх крупных зон, чтобы показать что AuthN — это не только «форма логина», а целый граф протоколов:
Главное на канвасе: ни в одном сценарии user-credentials не идут напрямую к backend в обход auth service, и ни один m2m-вызов не идёт без mTLS или API key. Edges = физические соединения, ответы анимируются по тем же линиям в reverse.
Классический MFA: password + TOTP. Backend верифицирует argon2id-hash (memory-hard, ~150ms — это фича, не баг), проверяет HIBP API через k-anonymity (отправляем только первые 5 символов SHA1, получаем bucket — пароль никогда не покидает наш сервер), затем требует TOTP. TOTP-сервис считает HMAC-SHA1(secret, floor(T/30)) и сверяет с ±1 step window (90 секунд). Поднимаем last_used_step для anti-replay. Это AAL2 — годно для большинства, но не phishing-resistant.
WebAuthn registration ceremony. Backend генерирует random 32-byte challenge, отправляет {rp, user, pubKeyCredParams}. Браузер вызывает navigator.credentials.create, Secure Enclave генерирует ECDSA P-256 keypair, private key никогда не покидает device. Возвращается attestationObject с public key и подписью challenge. Backend проверяет origin (anti-phishing!), challenge match, сохраняет {credential_id, public_key, sign_counter=0, transports, aaguid}. Passkey синкается через iCloud Keychain / Google Password Manager на все devices пользователя.
Phishing-resistant assertion ceremony. Backend выдаёт challenge, браузер вызывает navigator.credentials.get, Secure Enclave подписывает (challenge + clientData), возвращает assertion. Backend верифицирует подпись с stored public key, проверяет sign_counter (monotonic — anti-clone), выдаёт session. Никакой shared secret не передаётся по wire. Origin binding в браузере делает фишинг невозможным: на fake-attacker.com браузер не найдёт credential для cloud-arch.ru.
OAuth 2.0 Authorization Code Flow + PKCE. Backend генерирует code_verifier (random) и code_challenge = SHA256(verifier), сохраняет в temp session. Browser редиректится на Google /authorize с code_challenge. Google после consent редиректит обратно с code. Backend обменивает code + verifier на токены через POST /token. PKCE защищает от code interception: даже если attacker перехватил code (через malicious mobile app, например), без verifier токен не получит. ID token (JWT с identity claims) верифицируется через RS256, проверяется iss, aud, exp, nonce. Upsert account row, link к user, выдача нашей session cookie (не Google JWT).
CI Job → POST /v1/scripts с Authorization: Bearer ca_secret_xxx. Backend хеширует input и lookup-ит в apikey table (не plaintext compare!). Загружает row с scopes, owner_user_id, rate_limit. Проверяет sliding window rate limit (87/1000 ok), scope subset check (scripts:write ⊆ key scopes). Принципал = apikey:abc123 действует от имени user_42. Ротация ключа — каждые 90 дней через CI secrets manager. Revoke — DELETE FROM apikey WHERE id=..., мгновенный, без JWT-style wait-for-exp.
Internal Service A → Service B через Envoy sidecars. SPIRE выдаёт SVID (SPIFFE Verifiable Identity Document) каждому sidecar — spiffe://prod/svc-A, TTL=24h, auto-rotate за 12h до истечения. mTLS handshake: оба cert верифицируются против SPIRE root CA. Sidecar B проверяет SPIFFE authz policy («может svc-A звать /internal/data?»), форвардит запрос с x-forwarded-client-cert header. Identity = mTLS cert, not network position — это и есть zero-trust. Compromise window короткий (24h), ротация без downtime.
Сравнение TOTP vs Passkey под фишингом. Attacker делает прокси-страницу cloud-arch.ru.fake-attacker.com, перехватывает email+password+TOTP code, replay-ит в real backend за 30s window TOTP → session захвачен. С passkey: браузер вызывает navigator.credentials.get(rpId: fake-attacker.com) — нет credential, passkey отказывается подписать. Атака мертва на уровне браузерного API, а не на уровне внимательности пользователя.
SMS-2FA SIM-swap. Attacker через OSINT собирает phone + DOB + maiden name, звонит carrier («потерял телефон»), плохой KYC активирует attacker SIM. Victim теряет signal, attacker делает forgot-password → SMS reset code попадает к attacker → account hijacked. Реальные кейсы: Reddit CEO 2018, Twitter CEO 2019, массовые Coinbase 2021. NIST SP 800-63B с 2017 явно рекомендует против SMS как primary 2FA.
На диаграмме два ADR:
ADR-001: Passkey-first vs Password+MFA — adoption story и fallback ladder. Запускаем passkey-first для новых регистраций (primary CTA), promote баннером для existing. Password+TOTP остаётся permanent fallback пока adoption < 80%. Recovery ladder: другой passkey → recovery codes (10x one-time) → email magic link + TOTP → human support с KYC. SMS-2FA полностью disabled. Для admin/billing actions step-up: только passkey ИЛИ hardware key, TOTP не принимается. M2M только через mTLS (internal) или scope-limited API key (external).
ADR-002: Session cookie vs JWT. Web frontend = HttpOnly Secure SameSite=Lax cookie с opaque session ID, lookup в Redis. Revocation моментальная. JWT используется только как short-lived (5 мин) access token при OIDC SSO с external IdP, refresh token = opaque в Redis. Pass-the-cookie митигируется session binding к device fingerprint + IP-class + absolute TTL 12h.
Compliance angle: passkey удовлетворяет AAL2/AAL3 → SOC2 CC6.1, PCI-DSS 8.3, PSD2 SCA (EU banking), HIPAA — out of the box.
!@#) — NIST с 2017 явно убрал из guideline. Длина важнее class diversity. correct horse battery staple >> P@ssw0rd1.Password1! → Password2!. NIST: ротация только по compromise signal..env утекает в репо, в Docker image layers, в CI logs. Ротация + secrets manager (Vault, AWS Secrets Manager).Passkey-only без fallback — рано. Adoption 1.5% на GitHub после года. User теряет все devices = lost account. Нужен recovery ladder (recovery codes → email+KYC → human support). Passkey-only имеет смысл только для нового workforce-tenant с managed devices (Cloudflare model).
WebAuthn для backend-to-backend — переусложнение. WebAuthn заточен под user-presence (биометрия, touch). Для m2m используй mTLS или API keys.
SSO для consumer mass-market без password fallback — если user пришёл через Google и Google account удалён/locked, user теряет наш аккаунт. Минимум — link multiple providers и иметь email recovery.
Свой AuthN сервер с нуля — почти всегда плохая идея. Better Auth, Keycloak, Auth0, Clerk, Supertokens, Cognito — выбор по бюджету и compliance. Свой имеет смысл только для уникальных требований (медицина, госорганы, специфический compliance) с командой security-инженеров.
JWT когда не нужен stateless — если у тебя один backend и Redis, session cookie проще, безопаснее, и revocation мгновенный. JWT оправдан для federated services (микросервисы которые не должны звать центральный auth на каждый request) и short-lived OIDC access tokens.
Biometric как single factor для high-value — биометрия не secret (отпечатки на стакане, лицо в Instagram). Биометрия = device unlock + possession proof, не сам factor. Passkey правильно сделан: биометрия unlock-ит Secure Enclave, но AuthN — ECDSA signature от private key.