03 — User Journeys (Auth Module)
- 1. Login
- 2. First-login email verification
- 3. TOTP 2FA — setup & challenge
- 4. Forgot / reset password
- 5. Logout
- 6. Silent refresh / session expiry (token lifecycle)
- 7. Device / session management
- 8. API key management
- 9. Tenant onboarding → first login (register)
- 10. Cross-cutting: deep links, push, QR, email
End-to-end journeys for the Auth module computed from
auth.controller.ts+auth.service.ts. Each journey: entry, intent, decision points, system responses, loading, failures, recovery, exit, back navigation, abandonment, timeout, session expiry, permission denial, offline.(planned)/(forward-looking)marks per global rules.
1. Login
entry: warm/cold app open, deep link, push→content, session-expiry redirect
intent: obtain an authenticated context
sequenceDiagram
actor U as User
participant F as LoginScreen
participant R as AuthRepository
participant API as POST /auth/login
U->>F: enter email + password
F->>R: submit()
R->>API: {email, password}
alt success (200)
API-->>R: envelope.data = {accessToken, refreshToken}
R-->>F: persist tokens (secure storage) + session meta
F-->>U: navigate /home (role-aware)
else 401 UNAUTHENTICATED
API-->>R: "Invalid email or password."
F-->>U: field-level error, inline (never reveals which field wrong)
else 429 RATE_LIMITED
F-->>U: countdown, no auto-retry
else network
F-->>U: AppErrorState + Retry
end
- Decision points: remember this device? → uses refresh in secure storage for auto-session;
2FA challenge if server returns one
(planned). - Loading: button
loadingspinner replaces label; anti double-submit (08_Interaction_&_Motion.md §6). - Failure covers: wrong password, locked account ("Account locked. Try again later." —
auth.service.ts:135), unknown email (identical text), rate limit, offline. - Exit: successful →
/home(per-role landing,00-shared/05 §2); back = quit. - Abandonment: back stack cleared after success; token loss →
sessionExpired→ re-login. - Password managers: autofill hints
username+current-password,autofillHintson TextFields (09_Accessibility_Baseline.md §10).
2. First-login email verification
entry: in-app banner post-register, email click (deep link /login) — target contract
intent: satisfy "email verification mandatory before first login" (FIXED WIRE CONTRACT; OQ-1)
sequenceDiagram
actor U as New user
participant E as Email client
participant W as EmailWorker (BullMQ emails)
participant API as POST /auth
U->>API: POST /auth/register
API-->>W: UserRegistered -> emails/send-welcome-email
W-->>E: Welcome email, token=verificationToken
U->>E: see verify link (deep link /verify-email?token=...)
E-->>F: open app VerifyEmailScreen
F->>API: POST /auth/verify-email {token}
alt token valid
API-->>F: 200 {message:"Email verified successfully."} (auth.service.ts:235)
else token invalid/expired
API-->>F: 400 "Invalid or expired verification token."
end
F-->>U: success (check) → /login or /home
- Resend:
POST /auth/resend-verification(JWT) — 5/120s rate (auth.controller.ts:83-89). - Already-verified token: 200 "Email already verified." (
auth.service.ts:215) — idempotent client handling. - Recovery: invalid token → inline error + "resend email" (rate-limited) + deep-link fallback.
3. TOTP 2FA — setup & challenge
2FA setup is fully implemented; login challenge is (planned) (server returns no
challenge signal today — OQ-1).
sequenceDiagram
actor U as Admin
participant S as SecuritySettings
participant API1 as POST /auth/2fa/enable
participant API2 as POST /auth/2fa/verify
U->>S: "Set up two-factor authentication"
S->>API1: (JWT)
API1-->>S: {secret, qrCodeUri} (otpauth URI, auth.service.ts:330)
S-->>U: QR + manual `secret` — Save (copy) + pickup
U->>APP: open authenticator, scan
U->>S: enter 6-digit code
S->>API2: POST /auth/2fa/verify {token}
API2-->>S: 200 {message:"2FA enabled successfully."}
S-->>U: green confirmation; TOTP badge ON
Note over U: next login, issuer StudyLyon
- Failure: wrong code → 401 "Invalid TOTP code."; already enabled → 400 "already enabled.";
verify before enable → 400 "Call enable first." (
auth.service.ts:326,350-353). - Disable: needs code prompt →
POST /auth/2fa/disable(verified action). - Loss of device: no recovery codes today
(planned)(IMPLEMENTATION_PLAN.md§Phase 4/mfa/recovery-codes) → route to account recovery / admin un-enroll(planned).
4. Forgot / reset password
entry: "Forgot password?" on login; privacy: indistinguishable for unknown email
sequenceDiagram
actor U as User
participant F as ForgotPasswordScreen
participant R as AuthRepo
participant API1 as POST /auth/forgot-password
participant E as Email (TOKEN reset link)
participant P2 as ResetScreen
participant API2 as POST /auth/reset-password
U->>F: email
F->>API1: {email}
API1-->>F: 200 — identical body for existing/missing user
API1->>E: PasswordResetRequested → emails worker, token 32B hex, 1h TTL
U->>E: click link → app deep-link /reset-password?token=...
P2->>API2: POST /auth/reset-password {token, newPassword(≥8)}
API2-->>P2: 200 {message:"Password reset successfully."} — all sessions revoked
P2-->>U: "Log in with new password" → /login
- Rate: forgot 3/min, reset 5/min (
auth.controller.ts:92-107). - Timeout: token expires 1 h (
auth.service.ts:266,passwordResetExpiresAt) → 400 "Reset token has expired." → back to forgot. - Reuse: after successful reset
passwordResetTokencleared (auth.service.ts:303); re-use → 400 "Invalid or expired reset token." - All devices: reset deletes every session — user must log in on every device (
auth.service.ts:306).
5. Logout
entry: settings → "Log out" (all), per-device “Log out” from Sessions
sequenceDiagram
participant U as User
participant S as SecurityPage
participant API as POST /auth/logout|logout-all
S->>API: logout (refreshToken) // delete 1 session by hash
S->>API: logout-all // (JWT) deletes ALL sessions (user)
API1-->>S: 200 {message:"Logged out successfully."}
API-->>S: 200 {message:"Logged out from all devices."}
S-->>U: clear local tokens → /login (+ "You have been logged out" snackbar)
- Permission denial: logout-all is JWT-only; unauthenticated → 401 → redirect login.
- Risks documented: the controllers' logout requires
refreshToken— an empty token throws arbitraryError→ 500 (OQ-4); client must never submit blank. - Exit: no back into app shell.
6. Silent refresh / session expiry (token lifecycle)
stateDiagram-v2
[*] --> Authenticated: login
Authenticated --> Refreshing: accessToken expires
Refreshing --> Authenticated: POST /auth/refresh ok
Refreshing --> SessionExpired: refresh fails (reuse/expired/revoked)
SessionExpired --> Login: show reason banner
Login --> Authenticated: fresh login
Authenticated --> LoggedOut: logout / revoked on another device
- Single-flight refresh on 401 interceptor (
diorefresh interceptor,00-shared/11 §5). - Refresh rotation is implicit: every successful refresh yields a new pair; old refresh
is revoked server-side (
auth.service.ts:190-196). - Expired refresh (4xx) →
SessionExpired→ re-login + reason snackbar (if known).
7. Device / session management
entry: Settings → Security → Devices
GET /auth/sessions → list (deviceName/browser/operatingSystem/platform/ipAddress/location
from user_sessions, created/expires/lastActivity)
- Item → "Log out this device" → confirm dialog →
DELETE /auth/sessions/:id. - "Log out all other devices" (would be
(planned)— today there's only logout-all-like and revoke-by-id on the current user). - Data note: session
listreturns all of the current user's sessions sorted as-in-insertion (no page param — small list, client renders grouped by device).
8. API key management
entry: Security → API keys
- List:
GET /api/v1/api-keys→ cards (name, prefixabc12345…, scopes, createdAt, last); only active. - Create:
POST /api/v1/api-keys→ one-time modal shows full key (copy); never re-displayed. - Revoke:
DELETE /api/v1/api-keys/:id→ confirm.
9. Tenant onboarding → first login (register)
entry: `/register` (fresh install / SaaS landing)
POST /auth/register→ tokens returned → already authenticated in tenant.- Decision: some tenants may want admin to pick plan/channel before auth — no signal; assume immediate.
10. Cross-cutting: deep links, push, QR, email
| Entry | Behavior | Status |
|---|---|---|
Deep link studylyon://verify-email?token= / /reset-password?token= | parse token, preload form | (forward-looking) client; no server link endpoint |
| Push → "session revoked on another device" | openssessions; re-auth | (planned) push infra 00-shared/12 B3 |
| QR scan for 2FA | only 2FA setup QR display (generated client-side); scanner not needed | display-only |
Abandonment & exit rules (all): back = cancel token capture; timeout = 15 min idle reset token expired → 400; permission denial impossible within module (self-service); offline = cached verify/reset state only, actions blocked (write queue not defined for auth).