Cum funcționează Register și Login în Bono.
Autentificarea este primul lucru pe care îl face un user când ajunge în Bono. Este și ultima barieră înainte ca el să vadă produsul. Un flux greșit înseamnă abandon înainte ca userul să descopere valoarea. Un flux slab securizat înseamnă risc real pentru datele financiare ale clienților noștri. Acest document explică fiecare decizie luată, de ce am luat-o și cum funcționează în practică.
Documentul este scris pentru echipa tehnică — backend, frontend, QA — dar și pentru Product și Design. Scopul este ca toată lumea să înțeleagă nu doar ce am construit, ci și de ce.
Principii de design
Înainte de orice flux, câteva reguli după care am luat toate deciziile:
- Email-first, nu parolă-first. Primul câmp este adresa de email. Parola este metodă secundară, nu principală.
- Parola este opțională. Userul poate activa contul și intra cu OTP sau Google, fără să seteze niciodată o parolă.
- Niciodată nu confirmăm în UI că un email există. Asta protejează confidențialitatea clienților și previne account enumeration.
- Un singur flux, nu două. Register și Login pornesc de pe același ecran. Sistemul detectează pe server dacă emailul există și redirecționează corespunzător.
- Pregătit pentru 2026. Passkeys (WebAuthn) ca metodă principală pentru loginurile viitoare, transparent pentru user.
Fluxul de Register
Userul ajunge pe ecranul de Register. Vede un câmp de email și un buton Google. Butonul de email este inactiv (bej) până când introduce o adresă validă — în acel moment devine primar (roz) și Google devine secundar. Motivul: vrem să prioritizăm canalul care duce la verificarea emailului.
Emailul există deja în baza de date
Dacă un user încearcă să se înregistreze cu o adresă care are deja cont, UI-ul nu confirmă asta. Trimitem tot un email cu cod OTP, dar cu un mesaj diferit în corp. Userul vede același ecran neutru.
„Bine ai venit! Confirmă-ți adresa cu codul de mai jos: [123456]. Valabil 10 minute."
„Am primit o cerere de creare cont pentru această adresă. Dacă ești tu, intră cu codul: [123456]. Dacă nu ești tu, ignoră acest email."
Dacă am afișa „Contul tău este deja activ", un atacator ar putea afla că bogdan@bono.ro este client Bono pur și simplu încercând la register. Faptul că ești client Bono este o informație privată și protejată GDPR. Qonto, Revolut și alte fintechs europene aplică același standard.
Fluxul de Login
Login-ul pornește identic cu Register-ul — același ecran, același câmp de email. Diferența se face pe server, nu în UI.
next: "passkey"→browser popup nativ Face ID→Dublinnext: "password"→S2 · Parolă + opțiune email linknext: "otp"→cod trimis automat→S3 · OTPCheckbox „Rămâi autentificat"
Bifat implicit. Când e bifat: sesiune 30 zile cu refresh token automat. Când e debifat: sesiune de browser (expiră la închidere). Util pentru userii care accesează Bono de pe calculatoare partajate.
Contul fără parolă la login
Dacă userul a sărit peste setarea parolei la register, nu îi arătăm câmpul de parolă la login. Trimitem automat un cod OTP și îl ducem la ecranul de verificare. Nicio alegere suplimentară.
Passkey — Face ID și Touch ID
Un passkey este o cheie criptografică stocată în Secure Enclave-ul dispozitivului — un chip hardware dedicat, separat de sistemul de operare. La înregistrare se generează o pereche: cheia privată rămâne pe dispozitiv, cheia publică ajunge la Bono. Cheia privată nu părăsește niciodată dispozitivul.
La autentificare, Bono trimite o provocare criptografică (string random). Dispozitivul o semnează cu cheia privată, dar numai după ce userul confirmă prin Face ID sau Touch ID. Bono verifică semnătura cu cheia publică. Dacă se potrivesc — autentificat.
De ce e mai sigur decât o parolă
- Nu poate fi phishat. Cheia e legată de domeniul bono.ro. Pe un site fals, nu funcționează.
- Nu poate fi furat dintr-un breach. Serverul stochează doar cheia publică, nu secretul.
- Nu necesită 2FA separat. Biometria este al doilea factor, incorporat.
Cum se activează (o singură dată)
La finalul fluxului de register, după verificare OTP și parolă opțională, prezentăm ecranul de activare Passkey. Apare o singură dată. Dacă userul refuză, poate activa din Setări → Securitate oricând.
Cum funcționează login-ul cu Passkey activ
Browserul (Safari, Chrome) detectează automat că există un passkey pentru bono.ro. Afișează un popup nativ de sistem imediat după „Continuă cu email". Userul confirmă prin Face ID — intrat direct. Ecranul de parolă nu se mai afișează.
Apple sincronizează prin iCloud Keychain. Google prin Google Password Manager. Dacă userul schimbă telefonul, passkey-ul este disponibil automat pe dispozitivul nou.
Pe dispozitiv nou, browser incompatibil sau dacă userul refuză Face ID: fluxul cade automat pe email + OTP sau parolă. Passkey-ul este întotdeauna opțional, niciodată singurul mod de login.
Codul OTP — reguli și timing
| Parametru | Valoare și motivație |
|---|---|
| Format | 6 cifre numerice, generat criptografic (CSPRNG). Nu Math.random(). |
| Valabilitate | 10 minute. Countdown vizibil în UI, devine galben la <60s, roșu la expirare. |
| Încercări maxime | 3 greșeli consecutive → blocare 5 minute cu countdown vizibil. |
| Cooldown resend | 30 secunde între două cereri de cod nou. Previne spam-ul pe email. |
| Invalidare | Codul devine invalid după utilizare, expirare sau când se cere un cod nou. |
| Canal | Email exclusiv. Nu SMS — cost ridicat + vulnerabil SIM swap. |
| Rate limiting | Implementat server-side, nu doar în UI. Redis sliding window per (email + IP). |
Decizii de securitate
Fără CAPTCHA
Rate limiting server-side este mai eficient, nu degradează experiența utilizatorilor legitimi și nu penalizează userii cu dizabilități. CAPTCHA adaugă fricțiune fără beneficii reale când ai rate limiting corect implementat.
Fără SMS OTP
Trei motive: (1) cost recurent per mesaj, (2) vulnerabilitate la SIM swap, (3) emailul este deja verificat la register, deci este canal de încredere.
Parola — reguli minime
Minim 8 caractere. Fără reguli de complexitate artificiale (majuscule obligatorii, caractere speciale). NIST SP 800-63B (2017, actualizat 2024) recomandă explicit să nu impui aceste reguli — duc la parole mai slabe. Blocăm în schimb parolele din lista top 10.000 compromise.
Sesiunile
JWT cu refresh token rotit. Token-urile sunt invalide la logout și rotite la orice acțiune sensibilă (schimbare email, schimbare parolă). Cookie HttpOnly + SameSite=Strict.
Note tehnice
| Componentă | Implementare |
|---|---|
| Passkeys / WebAuthn | @simplewebauthn/server + @simplewebauthn/browser — open source, folosit de GitHub și Cloudflare. |
| OTP generation | crypto.randomInt() în Node.js. Nu Math.random(). |
| Email tranzacțional | Resend sau SendGrid. Template HTML separat per tip de email. |
| Rate limiting | Redis + sliding window counter per (email, IP). |
| Sesiuni | JWT + refresh token cu revocation list în Redis. HttpOnly cookie. |
| Parole | bcrypt, cost factor 12. Verificare HaveIBeenPwned la setare. |
| Google OAuth | OAuth 2.0 cu PKCE. Scope: email + profile. |
Endpoint-uri API
POST /auth/login returnează next: "otp" atât pentru emailuri care nu există, cât și pentru conturi fără parolă. Nu expunem niciodată dacă emailul există sau nu printr-un răspuns diferit. Ambele trimit un email — conținutul emailului diferă, nu răspunsul API.
Ce nu facem (și de ce)
- SMS OTP — cost + SIM swap vulnerability.
- CAPTCHA — rate limiting server-side rezolvă mai bine și fără fricțiune.
- 2FA TOTP (Google Authenticator) — Passkey incorporează deja biometria ca al doilea factor. Over-engineering pentru profilul de risc Bono. Reconsiderăm în v2 pentru enterprise.
- Login cu Apple — audiența Bono e web-first. Se adaugă în v2 pentru aplicația mobilă.
- Reguli de complexitate parolă — NIST 2024 recomandă explicit să nu le impui.
Întrebări frecvente de la echipă
Răspunsuri la întrebările pe care le vor pune Product, Engineering și QA.
POST /auth/login, serverul verifică dacă există o înregistrare WebAuthn activă pentru acel user_id. Dacă da, returnează next: "passkey". Frontul inițiază imediat navigator.credentials.get() cu challenge-ul primit — browserul afișează popup-ul nativ. Dacă userul anulează sau browser-ul nu suportă WebAuthn, frontul afișează ecranul de parolă ca fallback.
next: "otp". Trimitem un email cu cod OTP — emailul este verificat deja (Google ne-l dă confirmat). Userul intră cu codul, și după aia poate seta o parolă sau activa passkey. Nu îl forțăm să folosească Google dacă vrea să intre cu email.
POST /auth/logout/all invalidează toate refresh token-urile active pentru acel user (ștergere din Redis per user_id). Access token-urile existente expiră în maxim 15 minute (TTL-ul lor). Dacă securitatea impune invalidare imediată, putem adăuga un token_version pe user — verificat la fiecare request.
user_id (nu emailul în clar), IP hashed, user agent. Nu logăm niciodată codul OTP, parola sau cheia privată.
Bono · PRD Auth Flow v1.0 · Iunie 2026 · Vezi prototipul →