Înapoi la prototip PRD · Auth

Cum funcționează Register și Login în Bono.

BP
de Echipa Produs · 15 min citire · Versiune 1.0 · Iunie 2026

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:

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.

S1 · Register — email sau Google
Google OAuthDublin (cont creat automat)
Email validsistem trimite cod OTP
S2 · Verificare OTP — 6 cifre, valabil 10 minute
↳ Cod corectS3 · Parolă (opțional)
↳ 3 greșeli consecutiveblocat 5 minute
↳ Cod expiratpoate cere cod nou după 30s
S3 · Parolă — opțional, poate fi sărit
↳ Salvează sau sariS4 · Activare Passkey
S4 · Activare Passkey — ofertă Face ID / Touch ID, o singură dată
↳ Activează sau „Mai târziu"Dublin

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.

Email · Cont nou

„Bine ai venit! Confirmă-ți adresa cu codul de mai jos: [123456]. Valabil 10 minute."

Email · Cont existent detectat

„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."

De ce nu confirmăm în UI?

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.

S1 · Login — email + checkbox „Rămâi autentificat"
Google OAuthDublin
Emailserver verifică tipul contului
Server returnează tipul contului
next: "passkey"browser popup nativ Face IDDublin
next: "password"S2 · Parolă + opțiune email link
next: "otp"cod trimis automatS3 · OTP

Checkbox „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ă

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ă.

Sincronizare cross-device

Apple sincronizează prin iCloud Keychain. Google prin Google Password Manager. Dacă userul schimbă telefonul, passkey-ul este disponibil automat pe dispozitivul nou.

Fallback

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

ParametruValoare și motivație
Format6 cifre numerice, generat criptografic (CSPRNG). Nu Math.random().
Valabilitate10 minute. Countdown vizibil în UI, devine galben la <60s, roșu la expirare.
Încercări maxime3 greșeli consecutive → blocare 5 minute cu countdown vizibil.
Cooldown resend30 secunde între două cereri de cod nou. Previne spam-ul pe email.
InvalidareCodul devine invalid după utilizare, expirare sau când se cere un cod nou.
CanalEmail exclusiv. Nu SMS — cost ridicat + vulnerabil SIM swap.
Rate limitingImplementat 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 generationcrypto.randomInt() în Node.js. Nu Math.random().
Email tranzacționalResend sau SendGrid. Template HTML separat per tip de email.
Rate limitingRedis + sliding window counter per (email, IP).
SesiuniJWT + refresh token cu revocation list în Redis. HttpOnly cookie.
Parolebcrypt, cost factor 12. Verificare HaveIBeenPwned la setare.
Google OAuthOAuth 2.0 cu PKCE. Scope: email + profile.

Endpoint-uri API

POST /auth/login → returnează { next: "otp" | "password" | "passkey" }
POST /auth/register → trimite OTP pe email (neutru)
POST /auth/verify-otp → verifică codul, creează sesiune
POST /auth/set-password → opțional, după OTP
POST /auth/login/password → autentificare cu parolă
POST /auth/passkey/register → returnează challenge WebAuthn
POST /auth/passkey/register/verify → salvează cheia publică
POST /auth/passkey/authenticate → returnează challenge pentru login
POST /auth/passkey/authenticate/verify → verifică semnătura, creează sesiune
POST /auth/logout → invalidează token curent
Important pentru backend

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)


Întrebări frecvente de la echipă

Răspunsuri la întrebările pe care le vor pune Product, Engineering și QA.

Cum detectăm pe server dacă un cont are passkey activ?
La 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.
Ce se întâmplă dacă OTP-ul expiră exact când userul apasă butonul?
Serverul validează timestamp-ul de creare al codului la momentul verificării. Dacă a expirat (chiar cu o secundă), returnează eroare. Frontul detectează asta și afișează banner-ul de expirare cu butonul „Trimite din nou" activ. Nu îl ducem la un ecran nou — rămâne pe S2, câmpurile se resetează.
Rate limiting-ul e per email, per IP sau combinat?
Combinat: per email (primele 3 greșeli blochează sesiunea OTP pentru acel email, indiferent de IP) și per IP (mai mult de 10 cereri de register/login în 10 minute → blocare temporară a IP-ului). Ambele sunt în Redis cu sliding window. Contoarele per email se resetează după lockout-ul de 5 minute.
Dacă userul are cont Google și încearcă să se logheze cu email, ce se întâmplă?
Dacă contul a fost creat exclusiv prin Google OAuth și nu are parolă setată, serverul returnează 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.
Cum poate userul să activeze passkey-ul mai târziu, dacă a sărit la register?
Ecranul de activare Passkey apare automat la primele 3 login-uri după register, dacă userul l-a sărit inițial. După 3 dismissal-uri consecutive, nu mai afișăm ecranul — considerăm că userul a ales conștient. Nu adăugăm o bifă „nu mai afișa": 3 respingeri consecutive sunt semnalul suficient, fără a complica UI-ul ecranului. Oricând poate reveni din Setări → Securitate, secțiunea „Metode de autentificare" — butonul „Activează Face ID / Touch ID" reia același flux WebAuthn. Userul poate înregistra passkey-uri de pe mai multe dispozitive — fiecare generează propria pereche de chei.
Ce se întâmplă cu passkey-ul dacă userul schimbă telefonul?
Pe ecosistemul Apple: passkey-ul se sincronizează automat prin iCloud Keychain pe toate dispozitivele cu același cont Apple. Pe Android: prin Google Password Manager. Dacă userul trece de pe Android pe iPhone sau invers, va trebui să activeze un passkey nou de pe noul dispozitiv — dar poate folosi oricând email + OTP ca fallback în acest interval.
Cum testăm WebAuthn în mediul de development / staging?
Chrome DevTools are un Virtual Authenticator integrat (F12 → tab-ul „WebAuthn"). Permite simularea Touch ID/Face ID fără hardware real. Safari pe Mac necesită un dispozitiv real sau simulatorul iOS. Pentru CI/CD, @simplewebauthn are un modul de test cu authenticator virtual. Pe staging, domeniul trebuie să fie HTTPS — WebAuthn nu funcționează pe HTTP (excepție: localhost).
Cum gestionăm cazul în care userul uită parola?
Pe ecranul S2 (parolă), link-ul „Am uitat parola" trimite un cod OTP pe email — același ecran S3 de verificare. După verificarea OTP, userul poate seta o parolă nouă sau intra direct fără parolă. Nu există un „reset password link" separat — OTP-ul servește ambelor scopuri.
Putem adăuga 2FA cu TOTP (Google Authenticator) mai târziu?
Da, dar nu e o prioritate acum. Passkey-ul incorporează deja biometria ca al doilea factor. TOTP ar fi un al treilea factor — overkill pentru profilul de risc al Bono (contabilitate PFA/SRL, nu banking). Dacă apar cerințe enterprise (clienți cu politici interne de securitate stricte), putem adăuga TOTP ca opțiune opt-in în Setări → Securitate.
Cum funcționează „Rămâi autentificat" tehnic?
Bifat: emitem un refresh token cu TTL de 30 de zile, stocat în cookie HttpOnly + SameSite=Strict. Access token-ul (JWT) are TTL de 15 minute și se reînnoiește automat la fiecare request, cât timp refresh token-ul e valid. Debifat: nu emitem refresh token, sesiunea expiră cu access token-ul (15 minute inactivitate sau la închiderea browserului cu session cookie).
Ce se întâmplă dacă același email se înregistrează simultan din două tab-uri?
Serverul folosește un lock optimistic pe emailul de register (upsert cu constrângere UNIQUE pe coloana email). Primul request creează înregistrarea (sau o găsește). Al doilea request, indiferent de timing, primește același răspuns neutru și un email cu cod OTP. Codul generat de al doilea request îl invalidează pe primul — userul folosește ultimul cod primit.
Cum gestionăm logout-ul din toate dispozitivele?
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.
Cât timp păstrăm logurile de autentificare și ce conțin?
Logurile de autentificare (login success, login fail, OTP fail, rate limit hit) se păstrează 90 de zile — suficient pentru investigații de securitate, în limitele GDPR. Conțin: timestamp, tip eveniment, user_id (nu emailul în clar), IP hashed, user agent. Nu logăm niciodată codul OTP, parola sau cheia privată.
De ce nu avem „Login cu Apple"?
Audiența Bono este în principal antreprenori care accesează de pe desktop — unde Google OAuth are penetrare mult mai mare decât Apple ID. În plus, Apple Sign In este obligatoriu doar pentru aplicații iOS care oferă alt provider social (regulă App Store) — pentru web, este opțional. Adăugăm în v2 când lansăm aplicația mobilă nativă.

Bono · PRD Auth Flow v1.0 · Iunie 2026 · Vezi prototipul →