# 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

```mermaid
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:**
- 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<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`](../../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<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)

```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<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

```mermaid
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)
