Implemented proposals from comprehensive QA review: 1. extend-csrf-protection - Add POST to CSRF protected methods in frontend - Global CSRF middleware for all state-changing operations - Update tests with CSRF token fixtures 2. tighten-cors-websocket-security - Replace wildcard CORS with explicit method/header lists - Disable query parameter auth in production (code 4002) - Add per-user WebSocket connection limit (max 5, code 4005) 3. shorten-jwt-expiry - Reduce JWT expiry from 7 days to 60 minutes - Add refresh token support with 7-day expiry - Implement token rotation on refresh - Frontend auto-refresh when token near expiry (<5 min) 4. fix-frontend-quality - Add React.lazy() code splitting for all pages - Fix useCallback dependency arrays (Dashboard, Comments) - Add localStorage data validation in AuthContext - Complete i18n for AttachmentUpload component 5. enhance-backend-validation - Add SecurityAuditMiddleware for access denied logging - Add ErrorSanitizerMiddleware for production error messages - Protect /health/detailed with admin authentication - Add input length validation (comment 5000, desc 10000) All 521 backend tests passing. Frontend builds successfully. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
328 lines
13 KiB
Markdown
328 lines
13 KiB
Markdown
# User Authentication & Authorization
|
||
|
||
## Purpose
|
||
|
||
使用者認證與授權系統,透過外部認證 API 進行身份驗證,提供細部權限控制。
|
||
## Requirements
|
||
### Requirement: API-Based Authentication
|
||
系統 SHALL 限定使用外部認證 API (https://pj-auth-api.vercel.app) 進行登入認證,不支援其他認證方式。
|
||
|
||
#### Scenario: API 登入成功
|
||
- **GIVEN** 使用者擁有有效的企業帳號
|
||
- **WHEN** 使用者透過前端提交憑證
|
||
- **THEN** 系統呼叫 https://pj-auth-api.vercel.app 驗證憑證
|
||
- **AND** 驗證成功後建立 session 並回傳 JWT token
|
||
|
||
#### Scenario: API 登入失敗
|
||
- **GIVEN** 使用者提供無效的憑證
|
||
- **WHEN** 使用者嘗試登入
|
||
- **THEN** 認證 API 回傳錯誤
|
||
- **AND** 系統拒絕登入並顯示錯誤訊息
|
||
- **AND** 記錄失敗的登入嘗試
|
||
|
||
#### Scenario: 認證 API 無法連線
|
||
- **GIVEN** 認證 API 服務無法連線
|
||
- **WHEN** 使用者嘗試登入
|
||
- **THEN** 系統顯示服務暫時無法使用的訊息
|
||
- **AND** 記錄連線失敗事件
|
||
|
||
### Requirement: System Administrator
|
||
系統 SHALL 預設一個系統管理員帳號,擁有所有權限。系統管理員帳號必須存在於外部認證系統,且登入流程仍需透過外部認證 API;不允許本地繞過認證。
|
||
|
||
#### Scenario: 預設管理員帳號
|
||
- **GIVEN** 系統初始化完成
|
||
- **WHEN** 系統啟動
|
||
- **THEN** 存在預設管理員帳號 `ymirliu@panjit.com.tw`
|
||
- **AND** 該帳號擁有 `super_admin` 角色
|
||
- **AND** 該帳號不可被刪除或降級
|
||
|
||
#### Scenario: 管理員登入流程
|
||
- **GIVEN** 管理員帳號 `ymirliu@panjit.com.tw` 需要登入
|
||
- **WHEN** 管理員提交憑證
|
||
- **THEN** 系統仍需呼叫 https://pj-auth-api.vercel.app 驗證
|
||
- **AND** 不存在任何本地繞過認證的機制
|
||
- **AND** 驗證成功後才授予 `super_admin` 權限
|
||
|
||
#### Scenario: 管理員全域權限
|
||
- **GIVEN** 管理員帳號 `ymirliu@panjit.com.tw` 已通過 API 認證並登入
|
||
- **WHEN** 管理員存取任何資源
|
||
- **THEN** 系統允許存取,無視部門隔離限制
|
||
|
||
### Requirement: Role-Based Access Control
|
||
系統 SHALL 支援基於角色的存取控制 (RBAC)。
|
||
|
||
#### Scenario: 角色權限檢查
|
||
- **GIVEN** 使用者被指派特定角色 (如:工程師、主管、PMO)
|
||
- **WHEN** 使用者嘗試存取受保護的資源
|
||
- **THEN** 系統根據角色權限決定是否允許存取
|
||
|
||
#### Scenario: 角色指派
|
||
- **GIVEN** 管理員擁有使用者管理權限
|
||
- **WHEN** 管理員為使用者指派角色
|
||
- **THEN** 系統更新使用者的角色設定
|
||
- **AND** 新權限立即生效
|
||
|
||
### Requirement: Department Isolation
|
||
系統 SHALL 實施部門級別的資料隔離,確保跨部門資料安全。
|
||
|
||
#### Scenario: 部門資料隔離
|
||
- **GIVEN** 使用者屬於研發部門
|
||
- **WHEN** 使用者嘗試存取廠務部門的專案
|
||
- **THEN** 系統拒絕存取並顯示無權限訊息
|
||
|
||
#### Scenario: 跨部門專案存取
|
||
- **GIVEN** 專案被設定為跨部門可見
|
||
- **WHEN** 不同部門的使用者嘗試存取該專案
|
||
- **THEN** 系統根據專案的 Security_Level 設定決定是否允許存取
|
||
|
||
### Requirement: Session Management
|
||
系統 SHALL 管理使用者 session,包含過期與登出機制。
|
||
|
||
#### Scenario: Session 過期
|
||
- **GIVEN** 使用者已登入系統
|
||
- **WHEN** Session 超過設定的有效期限
|
||
- **THEN** 系統自動使 session 失效
|
||
- **AND** 使用者需重新登入
|
||
|
||
#### Scenario: 主動登出
|
||
- **GIVEN** 使用者已登入系統
|
||
- **WHEN** 使用者執行登出操作
|
||
- **THEN** 系統銷毀 session 並清除 token
|
||
|
||
### Requirement: Access Token Expiry
|
||
Access tokens SHALL expire within 60 minutes to limit exposure window in case of token compromise.
|
||
|
||
#### Scenario: Access token expiry
|
||
- **WHEN** an access token issued 61 minutes ago is used for API authentication
|
||
- **THEN** request is rejected with 401 Unauthorized
|
||
- **AND** error indicates "Token expired"
|
||
|
||
### Requirement: Refresh Token Support
|
||
The system SHALL support refresh tokens for seamless session continuity without requiring re-authentication.
|
||
|
||
#### Scenario: Refresh valid token
|
||
- **WHEN** POST to /api/auth/refresh with valid refresh token
|
||
- **THEN** new access token is issued
|
||
- **AND** new refresh token is issued via rotation
|
||
- **AND** old refresh token is invalidated
|
||
|
||
#### Scenario: Refresh expired token
|
||
- **WHEN** POST to /api/auth/refresh with expired refresh token
|
||
- **THEN** request is rejected with 401 Unauthorized
|
||
- **AND** user must re-authenticate via login
|
||
|
||
#### Scenario: Automatic frontend refresh
|
||
- **WHEN** access token expires in less than 5 minutes
|
||
- **AND** frontend prepares to make API call
|
||
- **THEN** token is automatically refreshed first
|
||
- **AND** original request proceeds with new token
|
||
|
||
### Requirement: API Rate Limiting
|
||
The system SHALL implement rate limiting to protect against brute force attacks and DoS attempts.
|
||
|
||
#### Scenario: Login rate limit enforcement
|
||
- **GIVEN** a client IP has made 5 login attempts within 1 minute
|
||
- **WHEN** the client attempts another login
|
||
- **THEN** the system returns HTTP 429 Too Many Requests
|
||
- **AND** the response includes a Retry-After header
|
||
|
||
#### Scenario: Rate limit window reset
|
||
- **GIVEN** a client has exceeded the rate limit
|
||
- **WHEN** the rate limit window expires (1 minute)
|
||
- **THEN** the client can make new requests
|
||
|
||
#### Scenario: Rate limit per IP
|
||
- **GIVEN** rate limiting is IP-based
|
||
- **WHEN** different IPs make requests
|
||
- **THEN** each IP has its own rate limit counter
|
||
|
||
### Requirement: Comprehensive API Rate Limiting
|
||
The system SHALL enforce rate limits on all sensitive API endpoints to prevent abuse and ensure service availability.
|
||
|
||
#### Scenario: Task creation rate limit exceeded
|
||
- **WHEN** user exceeds 60 task creation requests per minute
|
||
- **THEN** system returns 429 Too Many Requests
|
||
- **THEN** response includes Retry-After header
|
||
|
||
#### Scenario: Report generation rate limit exceeded
|
||
- **WHEN** user exceeds 5 report generation requests per minute
|
||
- **THEN** system returns 429 Too Many Requests
|
||
- **THEN** response includes rate limit headers
|
||
|
||
#### Scenario: Rate limit headers provided
|
||
- **WHEN** user makes any rate-limited API request
|
||
- **THEN** response includes X-RateLimit-Limit header
|
||
- **THEN** response includes X-RateLimit-Remaining header
|
||
- **THEN** response includes X-RateLimit-Reset header
|
||
|
||
#### Scenario: Rate limit window reset
|
||
- **WHEN** rate limit window expires
|
||
- **THEN** user can make requests again up to the limit
|
||
- **THEN** X-RateLimit-Remaining resets to maximum
|
||
|
||
### Requirement: Input Length Validation
|
||
The system SHALL enforce maximum length limits on all user-provided string inputs to prevent DoS attacks and database overflow.
|
||
|
||
#### Scenario: Task title exceeds maximum length
|
||
- **WHEN** user submits a task with title longer than 500 characters
|
||
- **THEN** system returns 422 Validation Error with descriptive message
|
||
|
||
#### Scenario: Description field within limits
|
||
- **WHEN** user submits content with description under 10000 characters
|
||
- **THEN** system accepts the input and processes normally
|
||
|
||
### Requirement: CORS Security
|
||
The system SHALL explicitly define allowed CORS methods and headers instead of using wildcards to reduce attack surface.
|
||
|
||
#### Scenario: Request with standard headers
|
||
- **WHEN** a cross-origin request includes Content-Type, Authorization, or X-CSRF-Token headers
|
||
- **THEN** the request is allowed
|
||
|
||
#### Scenario: Request with non-standard header
|
||
- **WHEN** a cross-origin request includes a non-whitelisted custom header
|
||
- **THEN** CORS preflight fails and request is rejected
|
||
|
||
### Requirement: Secure WebSocket Authentication
|
||
The system SHALL authenticate WebSocket connections without exposing tokens in URL query parameters. In production environments, query parameter authentication SHALL be disabled.
|
||
|
||
#### Scenario: WebSocket connection with token in first message
|
||
- **WHEN** client connects to WebSocket endpoint without a query token
|
||
- **THEN** server waits for authentication message containing JWT token
|
||
- **THEN** server validates token before accepting further messages
|
||
- **THEN** server sends an authentication acknowledgment message
|
||
|
||
#### Scenario: WebSocket connection with invalid token
|
||
- **WHEN** client sends an invalid or expired token
|
||
- **THEN** server sends an error message indicating invalid or expired token
|
||
- **THEN** server closes the connection with an authentication error code
|
||
|
||
#### Scenario: WebSocket connection timeout without authentication
|
||
- **WHEN** client connects but does not send authentication within 10 seconds
|
||
- **THEN** server closes the connection with appropriate error code
|
||
|
||
#### Scenario: Query parameter auth in production
|
||
- **WHEN** production environment and WebSocket connection includes token in query parameter
|
||
- **THEN** connection is rejected with code 4002
|
||
- **AND** error message indicates "Query parameter auth disabled in production"
|
||
|
||
### Requirement: WebSocket Connection Limits
|
||
The system SHALL limit each user to a maximum of 5 concurrent WebSocket connections to prevent resource exhaustion.
|
||
|
||
#### Scenario: User exceeds connection limit
|
||
- **WHEN** user already has 5 active WebSocket connections
|
||
- **AND** user attempts to open a 6th connection
|
||
- **THEN** connection is rejected with code 4005
|
||
- **AND** error message indicates "Too many connections"
|
||
|
||
#### Scenario: User within connection limit
|
||
- **WHEN** user has fewer than 5 active connections
|
||
- **AND** user opens a new WebSocket connection
|
||
- **THEN** connection is accepted
|
||
|
||
### Requirement: Path Traversal Protection
|
||
The system SHALL prevent file path traversal attacks by validating all file paths resolve within the designated storage directory.
|
||
|
||
#### Scenario: Path traversal attempt detected
|
||
- **WHEN** request contains file path with "../" or absolute path outside storage
|
||
- **THEN** system rejects request and logs security warning
|
||
- **THEN** system returns 403 Forbidden error
|
||
|
||
#### Scenario: Valid file path within storage
|
||
- **WHEN** request contains valid relative file path
|
||
- **THEN** system resolves path and verifies it is within storage directory
|
||
- **THEN** system processes file operation normally
|
||
|
||
### Requirement: JWT Secret Validation
|
||
The system SHALL validate JWT secret key strength on startup.
|
||
|
||
#### Scenario: Weak secret rejected
|
||
- **WHEN** the configured JWT secret is less than 32 characters
|
||
- **THEN** the system SHALL log a critical warning
|
||
- **AND** optionally refuse to start in production mode
|
||
|
||
#### Scenario: Low entropy secret warning
|
||
- **WHEN** the JWT secret has low entropy (repeating patterns, common words)
|
||
- **THEN** the system SHALL log a security warning
|
||
|
||
### Requirement: CSRF Protection
|
||
The system SHALL protect all state-changing operations (POST, PUT, PATCH, DELETE) with CSRF tokens.
|
||
|
||
#### Scenario: POST request without CSRF token
|
||
- **WHEN** an authenticated user makes a POST request without X-CSRF-Token header
|
||
- **THEN** the request SHALL be rejected with 403 Forbidden
|
||
- **AND** error message indicates "CSRF token is required"
|
||
|
||
#### Scenario: PUT/PATCH/DELETE request without CSRF token
|
||
- **WHEN** an authenticated user makes a PUT, PATCH, or DELETE request without X-CSRF-Token header
|
||
- **THEN** the request SHALL be rejected with 403 Forbidden
|
||
|
||
#### Scenario: Valid CSRF token accepted
|
||
- **WHEN** a state-changing request includes a valid CSRF token
|
||
- **THEN** the request SHALL proceed normally
|
||
|
||
#### Scenario: Public endpoints exempt from CSRF
|
||
- **WHEN** POST to /api/auth/login or other public endpoints
|
||
- **THEN** CSRF token is not required
|
||
|
||
## Data Model
|
||
|
||
```
|
||
pjctrl_users
|
||
├── id: UUID (PK)
|
||
├── email: VARCHAR(200) UNIQUE
|
||
├── name: VARCHAR(200)
|
||
├── department_id: UUID (FK)
|
||
├── role_id: UUID (FK)
|
||
├── skills: JSON
|
||
├── capacity: DECIMAL (週工時上限)
|
||
├── is_active: BOOLEAN
|
||
├── is_system_admin: BOOLEAN DEFAULT false (不可修改的系統管理員標記)
|
||
├── created_at: TIMESTAMP
|
||
└── updated_at: TIMESTAMP
|
||
|
||
pjctrl_departments
|
||
├── id: UUID (PK)
|
||
├── name: VARCHAR(100)
|
||
├── parent_id: UUID (FK, self-reference)
|
||
└── created_at: TIMESTAMP
|
||
|
||
pjctrl_roles
|
||
├── id: UUID (PK)
|
||
├── name: VARCHAR(50)
|
||
├── permissions: JSON
|
||
├── is_system_role: BOOLEAN DEFAULT false
|
||
└── created_at: TIMESTAMP
|
||
```
|
||
|
||
## Default Data (Seed)
|
||
|
||
```sql
|
||
-- 預設系統管理員角色
|
||
INSERT INTO pjctrl_roles (id, name, permissions, is_system_role) VALUES
|
||
('00000000-0000-0000-0000-000000000001', 'super_admin', '{"all": true}', true);
|
||
|
||
-- 預設系統管理員帳號
|
||
INSERT INTO pjctrl_users (id, email, name, role_id, is_active, is_system_admin) VALUES
|
||
('00000000-0000-0000-0000-000000000001', 'ymirliu@panjit.com.tw', 'System Administrator',
|
||
'00000000-0000-0000-0000-000000000001', true, true);
|
||
```
|
||
|
||
## External Dependencies
|
||
|
||
- **Authentication API**: https://pj-auth-api.vercel.app (唯一認證方式)
|
||
|
||
## Authentication Flow
|
||
|
||
```
|
||
┌─────────┐ ┌─────────────┐ ┌──────────────────────────┐
|
||
│ User │────▶│ Frontend │────▶│ pj-auth-api.vercel.app │
|
||
└─────────┘ └─────────────┘ └──────────────────────────┘
|
||
│ │
|
||
│◀────── JWT Token ───────│
|
||
│
|
||
▼
|
||
┌─────────────┐
|
||
│ Backend │ (驗證 JWT, 建立 Session)
|
||
└─────────────┘
|
||
```
|