# Auth Flow — Login / Refresh / Logout / Me > **Status:** ✅ Implemented (Phase 1 foundation, commit `702411f`) > **Actors:** Anyone (login) | Authenticated user (refresh, me, logout) ## 1. Tổng quan - **Schema:** JWT Bearer (HS256) + refresh token (random 64-byte base64) - **Access token:** 1 giờ (config `Jwt:AccessTokenExpiryMinutes`) - **Refresh token:** 7 ngày (config `Jwt:RefreshTokenExpiryDays`) - **Storage FE:** `localStorage` — key `solution-erp-admin-token` (fe-admin) / `solution-erp-user-token` (fe-user) - **Truyền:** header `Authorization: Bearer ` (axios interceptor auto) ## 2. Login ```mermaid sequenceDiagram actor U as User participant FE as Frontend
(LoginPage) participant API as SolutionErp.Api
AuthController participant M as MediatR
LoginCommandHandler participant UM as UserManager participant J as IJwtTokenService participant DB as SQL Server U->>FE: Nhập email + password → Submit FE->>API: POST /api/auth/login
{email, password} API->>M: Send(LoginCommand) M->>M: Validate (FluentValidation)
email format + password min 6 alt Validation fail M-->>API: throw ValidationException API-->>FE: 400 ProblemDetails + errors{} FE->>U: Toast error end M->>UM: FindByEmailAsync(email) UM->>DB: SELECT FROM Users WHERE Email = ? alt User not found OR IsActive = false M-->>API: throw UnauthorizedException API-->>FE: 401 "Email hoặc mật khẩu không đúng" end M->>UM: CheckPasswordAsync(user, password) alt Password sai M-->>API: throw UnauthorizedException API-->>FE: 401 end M->>UM: GetRolesAsync(user) UM->>DB: SELECT Roles JOIN UserRoles UM-->>M: roles[] M->>J: GenerateTokensAsync(user, roles) J->>J: Build claims (sub, email, jti, fullName, roles) J->>J: Sign HS256 + random refresh token J-->>M: (accessToken, refreshToken, refreshTokenExpiresAt) M->>UM: UpdateAsync(user) — save refresh token + expiry UM->>DB: UPDATE Users SET RefreshToken=?, RefreshTokenExpiresAt=? M-->>API: AuthResponseDto API-->>FE: 200 {accessToken, refreshToken, user: {id, email, fullName, roles}} FE->>FE: localStorage.setItem('solution-erp-*-token', accessToken)
+ lưu user JSON FE->>FE: setUser(user) via AuthContext FE->>U: Redirect /dashboard hoặc /inbox + toast success ``` **Code pointers:** - FE: [`fe-admin/src/pages/LoginPage.tsx`](../../fe-admin/src/pages/LoginPage.tsx), [`fe-admin/src/contexts/AuthContext.tsx`](../../fe-admin/src/contexts/AuthContext.tsx) - BE controller: [`AuthController.Login`](../../src/Backend/SolutionErp.Api/Controllers/AuthController.cs) - Handler: [`LoginCommandHandler`](../../src/Backend/SolutionErp.Application/Auth/Commands/Login/LoginCommand.cs) - Token gen: [`JwtTokenService`](../../src/Backend/SolutionErp.Infrastructure/Identity/JwtTokenService.cs) ## 3. Authenticated request (mọi API khác) ```mermaid sequenceDiagram participant FE as Frontend participant AX as axios interceptor participant API as API participant JWT as JwtBearerMiddleware participant CTRL as Controller FE->>AX: api.get('/suppliers') AX->>AX: Đọc localStorage token AX->>API: GET /api/suppliers
Authorization: Bearer API->>JWT: ValidateToken alt Token invalid / expired JWT-->>API: 401 API-->>AX: 401 AX->>AX: Clear localStorage + redirect /login end JWT->>API: HttpContext.User = ClaimsPrincipal API->>CTRL: Invoke action (có [Authorize]) CTRL->>CTRL: ICurrentUser.UserId (từ claim "sub") CTRL-->>FE: 200 data ``` **401 handling:** [`fe-admin/src/lib/api.ts`](../../fe-admin/src/lib/api.ts) interceptor catches 401 → clear localStorage → `window.location.href = '/login'`. ## 4. Refresh token ```mermaid sequenceDiagram participant FE as Frontend participant API as AuthController.Refresh participant M as RefreshTokenCommandHandler participant UM as UserManager participant J as IJwtTokenService participant DB Note over FE: Access token expired
(server trả 401 từ /api/whatever) FE->>API: POST /api/auth/refresh
{refreshToken} API->>M: Send(RefreshTokenCommand) M->>M: Validate not empty M->>UM: Users.First(RefreshToken == ?) UM->>DB: SELECT alt Not found OR RefreshTokenExpiresAt < UtcNow M-->>API: throw UnauthorizedException API-->>FE: 401 "Refresh token không hợp lệ hoặc đã hết hạn" FE->>FE: Clear localStorage + redirect /login end M->>UM: GetRolesAsync(user) M->>J: GenerateTokensAsync(user, roles) J-->>M: new (access, refresh, expiry) M->>UM: UpdateAsync — rotate refresh token DB-->>UM: OK M-->>API: AuthResponseDto API-->>FE: 200 new tokens FE->>FE: Update localStorage + retry original request ``` **⚠️ Chưa implement ở FE:** hiện tại 401 → logout luôn. Phase 1 đợt 2 sẽ thêm logic auto-refresh trong axios interceptor (retry failed request sau khi refresh thành công, queue các request song song). ## 5. /me (lấy user hiện tại) ```mermaid sequenceDiagram actor U participant FE participant API as AuthController.Me participant M as GetMeQueryHandler participant CU as ICurrentUser participant UM as UserManager U->>FE: App bootstrap (sau login) FE->>API: GET /api/auth/me
Bearer API->>M: Send(GetMeQuery) M->>CU: UserId (from claim "sub") alt Not authenticated M-->>API: throw UnauthorizedException end M->>UM: FindByIdAsync(userId.ToString()) alt Not found M-->>API: throw UnauthorizedException end M->>UM: GetRolesAsync(user) M-->>API: UserInfoDto{id, email, fullName, roles} API-->>FE: 200 ``` **Use case:** FE dùng để refresh user info sau page reload (nếu localStorage cũ nhưng role có thể thay đổi). Hiện Phase 1 FE lấy user từ localStorage trực tiếp — Phase 1 đợt 2 sẽ gọi `/me` ở mount AuthContext để verify token + sync role. ## 6. Logout ```mermaid sequenceDiagram actor U participant FE participant API as AuthController.Logout U->>FE: Click "Đăng xuất" FE->>FE: logout() in AuthContext:
localStorage.removeItem(token + user)
setUser(null) FE->>API: POST /api/auth/logout (optional) API-->>FE: 204 No Content FE->>FE: Redirect /login (via ProtectedRoute) ``` **Lưu ý:** Hiện endpoint logout chỉ trả 204. Phase 3 sẽ upgrade: invalidate refresh token trong DB + log audit event. ## 7. Edge cases | Case | Hiện tại handle | Phase nào fix | |---|---|---| | Access token expired mid-request | FE 401 → logout | Phase 1 đợt 2: auto refresh | | Refresh token expired | FE 401 → logout | OK — đúng hành vi | | User bị disable (`IsActive=false`) sau khi đã login | Token vẫn valid tới khi expire | Phase 4: check `IsActive` trong middleware | | Admin reset password | Refresh token cũ vẫn valid | Phase 4: invalidate refresh token khi reset password | | Multi-device login | Ghi đè refresh token → device cũ bị logout khi cần refresh | OK cho Phase 1; Phase 4 optional: bảng `UserRefreshTokens` riêng | | JWT key leak | Toàn bộ user có thể forge token | Phase 5: dùng user secrets prod + rotate annually | | Clock skew giữa API + client | `ClockSkew = 1 minute` đã config | OK | ## 8. Security checklist (Phase 5 review) - [ ] `Jwt:Secret` trong user-secrets / env var prod (không trong appsettings) - [ ] HTTPS enforce (`RequireHttpsMetadata = true` production) - [ ] Rate limit `/api/auth/login` (5 attempts/min/IP) — prevent brute force - [ ] Account lockout sau N lần sai password (config `UserLockout` trong Identity) - [ ] Password policy production (min 12 chars, unique, etc.) - [ ] Audit log cho login success/fail - [ ] Refresh token rotation detection (nếu dùng refresh cũ sau khi đã rotate → compromise alert)