Files
solution-erp/docs/flows/auth-flow.md
pqhuy1987 49a5f57a50 [CLAUDE] Docs: database-guide + 6 flow diagrams
docs/database/database-guide.md:
- Conventions (naming, data types, audit fields, soft delete)
- Schema hien tai (Identity tables sau migration Init) + seed 12 role + admin
- Schema planned: Phase 1 dot 2 (Supplier/Project/Department + Permission Matrix)
- Schema planned: Phase 3 (Contract + Approval + Comment + Attachment + Template + Clause + CodeSequence)
- Mermaid ERD cho tung phase
- Migration workflow (create/apply/revert)
- Index strategy + unique indexes
- Backup/restore SQL
- Common pitfalls + SQL cheatsheet

docs/flows/ — 6 flow documentation:
- README.md: index
- auth-flow.md: login/refresh/me/logout (IMPLEMENTED, sequence + edge cases + security checklist)
- permission-flow.md: Phase 1 dot 2 - Role x MenuKey x CRUD resolution + FE guard + BE policy
- contract-creation-flow.md: Phase 2 - Drafter flow chon template -> fill -> preview -> save draft
- contract-approval-flow.md: Phase 3 - state machine 9 phase chi tiet + reject flow + timeline UI
- form-render-flow.md: Phase 2 - OpenXml + ClosedXML + LibreOffice PDF convert
- sla-expiry-flow.md: Phase 3 - BackgroundService auto-approve qua SLA + warning notify

Update references:
- CLAUDE.md (root): them 2 row Tai lieu quan trong
- docs/CLAUDE.md: update project layout voi flows/ + database/
- docs/STATUS.md: log docs addition
- docs/changelog/migration-todos.md: tick Phase 0 docs items

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 11:15:28 +07:00

7.9 KiB

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 <token> (axios interceptor auto)

2. Login

sequenceDiagram
    actor U as User
    participant FE as Frontend<br/>(LoginPage)
    participant API as SolutionErp.Api<br/>AuthController
    participant M as MediatR<br/>LoginCommandHandler
    participant UM as UserManager<User>
    participant J as IJwtTokenService
    participant DB as SQL Server

    U->>FE: Nhập email + password → Submit
    FE->>API: POST /api/auth/login<br/>{email, password}
    API->>M: Send(LoginCommand)

    M->>M: Validate (FluentValidation)<br/>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)<br/>+ lưu user JSON
    FE->>FE: setUser(user) via AuthContext
    FE->>U: Redirect /dashboard hoặc /inbox + toast success

Code pointers:

3. Authenticated request (mọi API khác)

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<br/>Authorization: Bearer <token>

    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 interceptor catches 401 → clear localStorage → window.location.href = '/login'.

4. Refresh token

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<br/>(server trả 401 từ /api/whatever)
    FE->>API: POST /api/auth/refresh<br/>{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)

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<br/>Bearer <token>
    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

sequenceDiagram
    actor U
    participant FE
    participant API as AuthController.Logout

    U->>FE: Click "Đăng xuất"
    FE->>FE: logout() in AuthContext:<br/>localStorage.removeItem(token + user)<br/>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)