Autentizace
Tento dokument popisuje technickou implementaci autentizačních mechanismů v DnA Cruises systému.
Přehled
Systém podporuje několik metod autentizace:
- Email + heslo - klasické přihlášení s podporou 2FA
- Google OAuth - přihlášení přes Google účet
- Passkeys (WebAuthn) - bezpečné přihlášení pomocí biometrie nebo PINu
- 2FA (Two-Factor Authentication) - dodatečná vrstva zabezpečení pomocí TOTP nebo SMS
JWT Autentizace
Architektura
Všechny metody autentizace nakonec generují JWT (JSON Web Token), který se používá pro autorizaci dalších požadavků.
Token struktura
JWT token obsahuje následující claims:
userId- ID uživateleemail- Email uživatelerole- Role uživatele (Admin, Producer, Loader)iat- Issued at (čas vydání)exp- Expiration (čas expirace, defaultně 7 dní)
Token lifecycle
- Generování: Token je generován po úspěšném přihlášení pomocí
generateJWT()funkce - Uložení: Frontend ukládá token do
localStoragepod klíčemauth_token - Odesílání: Token se odesílá v
Authorizationheaderu jakoBearer <token> - Validace: Middleware
requireAuth()validuje token při každém požadavku - Obnova: Token se automaticky obnovuje při každém úspěšném požadavku (pokud je blízko expirace)
Implementace
Backend (workers/api/src/auth/jwt.ts):
generateJWT()- generuje token s HS256 algoritmemverifyJWT()- ověřuje token a extrahuje payload
Frontend (apps/web/src/hooks/useAuth.tsx):
AuthProvider- React context pro správu autentizačního stavuuseAuth()- hook pro přístup k autentizačním funkcím- Automatické načítání tokenu z
localStoragepři startu aplikace
Passkeys (WebAuthn)
Architektura
Passkeys používají WebAuthn standard (FIDO2) pro bezpečné přihlášení bez hesla. Systém podporuje:
- Registraci - vytvoření nového passkey pro uživatele
- Autentizaci - přihlášení pomocí existujícího passkey
- Správu - zobrazení, přejmenování a mazání passkeys
Registrace flow
-
Start (
POST /auth/passkey/register/start):- Server vygeneruje náhodný challenge
- Challenge se uloží do databáze s expirací (5 minut)
- Server vrátí
PublicKeyCredentialCreationOptions
-
Browser:
- Uživatel potvrdí registraci (biometrie, PIN, atd.)
- Browser vytvoří
PublicKeyCredentials attestation objectem
-
Finish (
POST /auth/passkey/register/finish):- Frontend odešle credential data na server
- Server extrahuje public key z attestation objectu (CBOR parsing)
- Server převede COSE public key na JWK formát
- Server uloží credential ID a public key do databáze
- Server ověří challenge a smaže ho
Autentizace flow
-
Start (
POST /auth/passkey/login/start):- Pokud je zadán email, server vrátí seznam povolených credentials
- Server vygeneruje challenge a uloží ho do databáze
-
Browser:
- Browser zobrazí seznam dostupných passkeys
- Uživatel vybere passkey a potvrdí (biometrie, PIN)
- Browser vytvoří assertion s podpisem
-
Finish (
POST /auth/passkey/login/finish):- Frontend odešle assertion data na server
- Server načte public key z databáze
- Server ověří podpis pomocí
verifyAssertion():- Ověří clientDataJSON (type, challenge, origin)
- Ověří authenticatorData (RP ID hash, user present flag, sign count)
- Ověří kryptografický podpis pomocí ECDSA P-256
- Pokud je vše v pořádku, server vygeneruje JWT token
Bezpečnostní aspekty
- Challenge: Každý request má unikátní, náhodný challenge (32 bytes)
- Sign count: Server kontroluje, že sign count roste (ochrana proti replay útokům)
- RP ID verification: Server ověřuje, že request přichází z očekávané domény
- Origin verification: Server ověřuje origin v clientDataJSON
- Public key extraction: Server extrahuje public key z attestation objectu (nevěří klientovi)
Implementace
Backend (workers/api/src/utils/webauthn.ts):
generateChallenge()- generuje náhodný challengeextractPublicKeyFromAttestation()- extrahuje public key z attestation objectuverifyAssertion()- ověřuje assertion podpisparseAuthenticatorData()- parsuje authenticator dataverifyES256Signature()- ověřuje ECDSA P-256 podpis
Frontend (apps/web/src/hooks/useAuth.tsx):
registerPasskey()- registruje nový passkeyloginWithPasskey()- přihlásí se pomocí passkeygetPasskeys()- načte seznam passkeysdeletePasskey()- smaže passkeyrenamePasskey()- přejmenuje passkey
2FA (Two-Factor Authentication)
Podporované metody
-
TOTP (Time-based One-Time Password)
- RFC 6238 kompatibilní
- 6místné kódy, 30 sekundový interval
- Podporuje time window (±1 time step)
-
SMS
- 6místné kódy
- Platnost 10 minut
- Integrace s DinoSMS API
Setup flow
-
TOTP Setup:
- Uživatel zavolá
POST /auth/2fa/setupsmethod: "TOTP" - Server vygeneruje Base32 secret (20 bytes)
- Server vytvoří otpauth:// URL pro QR kód
- Uživatel naskenuje QR kód v autentizační aplikaci
- Uživatel zavolá
POST /auth/2fa/enables kódem z aplikace - Server ověří kód pomocí
verifyTOTP() - Pokud je kód platný, server povolí 2FA
- Uživatel zavolá
-
SMS Setup:
- Uživatel zavolá
POST /auth/2fa/setupsmethod: "SMS"a telefonním číslem - Server vygeneruje 6místný kód
- Server odešle SMS přes DinoSMS API
- Uživatel zavolá
POST /auth/2fa/enables kódem ze SMS - Server ověří kód
- Pokud je kód platný, server povolí 2FA
- Uživatel zavolá
Login flow s 2FA
- Uživatel se přihlásí email + heslem
- Pokud má uživatel povolenou 2FA:
- Server vytvoří dočasnou session (
user_sessionstabulka) - Server vrátí
requires2FA: trueasession_token - Frontend zobrazí formulář pro 2FA kód
- Server vytvoří dočasnou session (
- Uživatel zadá 2FA kód
- Frontend zavolá
POST /auth/2fa/verifys kódem a session tokenem - Server ověří kód:
- TOTP: použije
verifyTOTP()s time window - SMS: porovná kód s uloženým kódem a zkontroluje expiraci
- TOTP: použije
- Pokud je kód platný, server vygeneruje JWT token a smaže session
TOTP implementace
Backend (workers/api/src/utils/totp.ts):
generateTOTPSecret()- generuje Base32 secretgenerateTOTPCode()- generuje TOTP kód pro daný time counterverifyTOTP()- ověřuje TOTP kód s time window tolerancegenerateOTPAuthURL()- vytváří otpauth:// URL pro QR kód
Algoritmus:
- Decode Base32 secret na bytes
- Vypočítat time counter:
floor(now / timeStep) - Vytvořit 8-byte big-endian buffer z time counteru
- Vypočítat HMAC-SHA1(secret, time counter)
- Dynamic truncation (RFC 4226)
- Modulo 1000000 pro 6místný kód
SMS implementace
Backend (workers/api/src/utils/dinosms.ts):
generateSMSCode()- generuje 6místný náhodný kódsendSMS()- odesílá SMS přes DinoSMS API
Flow:
- Server vygeneruje kód a uloží ho do
users.sms_code - Server nastaví expiraci (
sms_code_expires_at) - Server odešle SMS přes DinoSMS API
- Po ověření se kód smaže z databáze
Bezpečnostní aspekty
-
TOTP:
- Constant-time comparison pro ochranu proti timing útokům
- Time window tolerance (±1 time step = ±30 sekund)
- Secret je uložen v databázi (hashovaný by byl lepší, ale pro TOTP není nutný)
-
SMS:
- Kódy mají expiraci (10 minut)
- Kódy se smažou po použití
- Rate limiting by měl být implementován (TODO)
Google OAuth
Flow
- Uživatel klikne na "Přihlásit se přes Google"
- Frontend přesměruje na
GET /auth/google - Server vygeneruje state token (CSRF protection)
- Server uloží state do databáze (
oauth_statestabulka) - Server přesměruje na Google OAuth consent screen
- Uživatel autorizuje aplikaci
- Google přesměruje zpět na
GET /auth/google/callback?code=...&state=... - Server ověří state token
- Server vymění authorization code za access token
- Server získá user info z Google API
- Server vytvoří nebo najde uživatele v databázi
- Server vygeneruje JWT token
- Server přesměruje na frontend s tokenem v URL
Implementace
Backend (workers/api/src/routes/oauth.ts):
GET /auth/google- iniciuje OAuth flowGET /auth/google/callback- zpracovává OAuth callback
Frontend (apps/web/src/app/auth/callback/page.tsx):
- Zpracovává redirect s tokenem v URL
- Ukládá token do localStorage
- Přesměruje na hlavní stránku
Middleware
requireAuth()
Middleware pro ochranu endpointů vyžadujících autentizaci.
Funkce:
- Extrahuje JWT token z
Authorizationheaderu - Ověří token pomocí
verifyJWT() - Načte uživatele z databáze
- Přidá
authobjekt do requestu suserId,email,role
Použití:
routes.push({
method: 'GET',
path: '/protected',
handler: async (request, env) => {
const auth = (request as any).auth as AuthContext;
// auth.userId, auth.email, auth.role jsou dostupné
},
middleware: [requireAuth()],
});
Role-based access control
Middleware podporuje kontrolu rolí:
requireAuth({ role: 'Admin' })- pouze AdminrequireAuth({ roles: ['Admin', 'Producer'] })- Admin nebo Producer
Databázové tabulky
users
id- primární klíčemail- unikátní emailpassword_hash- PBKDF2 hash heslarole- Admin, Producer, nebo Loadertwo_factor_enabled- boolean flagtwo_factor_method- TOTP nebo SMStotp_secret- Base32 secret pro TOTPphone_number- telefonní číslo pro SMSsms_code- dočasný SMS kódsms_code_expires_at- expirace SMS kódu
user_passkeys
id- primární klíčuser_id- reference na users.idcredential_id- base64url encoded credential IDpublic_key- JSON string s public key JWK a metadataname- uživatelsky definovaný názevcounter- sign count pro replay protectioncreated_at- čas vytvořenílast_used_at- čas posledního použití
passkey_challenges
id- primární klíčchallenge- base64url encoded challengeuser_id- reference na users.id (volitelné)email- email pro login flow (volitelné)type- 'register' nebo 'login'expires_at- Unix timestamp expiracecreated_at- čas vytvoření
oauth_states
state- primární klíč (CSRF token)user_id- reference na users.id (volitelné)expires_at- Unix timestamp expiracecreated_at- čas vytvoření
user_sessions
id- primární klíčsession_token- unikátní session tokenuser_id- reference na users.idtwo_factor_verified- boolean flagexpires_at- Unix timestamp expiracecreated_at- čas vytvoření
Bezpečnostní best practices
- Hesla: PBKDF2 s 100,000 iteracemi, SHA-256, 32-byte salt
- JWT: HS256 algoritmus, 7 denní expirace
- Passkeys: Server-side signature verification, sign count tracking
- 2FA: Constant-time comparison, time window tolerance
- OAuth: State token pro CSRF protection
- Sessions: Expirace po 10 minutách neaktivity
- Rate limiting: Mělo by být implementováno (TODO)
Troubleshooting
Passkey registrace selhává
- Zkontrolujte, že prohlížeč podporuje WebAuthn
- Zkontrolujte, že doména má HTTPS (nebo localhost)
- Zkontrolujte console logy pro specifické chyby
2FA kód nefunguje
- TOTP: Zkontrolujte čas na serveru a zařízení (musí být synchronizovaný)
- SMS: Zkontrolujte, že SMS dorazila (může trvat několik sekund)
- Zkontrolujte, že kód není expirovaný
JWT token expiroval
- Token má expiraci 7 dní
- Uživatel se musí znovu přihlásit
- Frontend automaticky přesměruje na login page