# Authentication Capability ## ADDED Requirements ### Requirement: User Login with Dual-Token Session Management The system SHALL authenticate users by forwarding credentials to the Panjit AD authentication API (https://pj-auth-api.vercel.app/api/auth/login). Upon successful AD authentication, the system SHALL generate its own internal session token (separate from the AD token), encrypt the user's password using AES-256 encryption (for auto-refresh capability), and store all session data in the database. The internal token is returned to the client for subsequent requests. #### Scenario: Successful login with valid credentials - **WHEN** a user submits username "ymirliu@panjit.com.tw" and password "4RFV5tgb6yhn" to `POST /api/auth/login` - **THEN** the system SHALL forward the credentials to the AD API - **AND** receive a 200 response with `token` and `username` fields - **AND** encrypt the password using Fernet symmetric encryption (AES-256) - **AND** generate a unique internal session token (UUID4) - **AND** estimate or record the AD token expiry time (e.g., current_time + 1 hour) - **AND** create a session record in the `user_sessions` table with: username, display_name (from AD), internal_token, ad_token, encrypted_password, ad_token_expires_at, refresh_attempt_count (default 0), last_activity (current time), created_at - **AND** return status 200 with JSON body `{"token": "", "display_name": ""}` #### Scenario: Password stored securely with encryption - **WHEN** a password is stored in the user_sessions table - **THEN** the system SHALL encrypt it using the Fernet encryption key from environment variable FERNET_KEY - **AND** the encrypted_password field SHALL contain ciphertext that differs from the plaintext - **AND** decrypting the ciphertext SHALL reproduce the original password exactly #### Scenario: Failed login with invalid credentials - **WHEN** a user submits incorrect username or password to `POST /api/auth/login` - **THEN** the system SHALL forward the credentials to the AD API - **AND** receive a non-200 response (e.g., 401 Unauthorized) - **AND** return status 401 to the client with error message `{"error": "Invalid credentials"}` - **AND** NOT create any session record in the database #### Scenario: AD API service unavailable - **WHEN** a user attempts to login but the AD API (https://pj-auth-api.vercel.app) is unreachable - **THEN** the system SHALL return status 503 with error message `{"error": "Authentication service unavailable"}` ### Requirement: User Logout The system SHALL provide a logout endpoint that deletes the user's session record from the database. #### Scenario: Successful logout with valid internal token - **WHEN** an authenticated user sends `POST /api/auth/logout` with header `Authorization: Bearer ` - **THEN** the system SHALL delete the session record from the `user_sessions` table - **AND** return status 200 with `{"message": "Logout successful"}` #### Scenario: Logout without authentication token - **WHEN** a user sends `POST /api/auth/logout` without the Authorization header - **THEN** the system SHALL return status 401 with `{"error": "No authentication token provided"}` ### Requirement: Automatic AD Token Refresh with Retry Limit The system SHALL automatically refresh the AD token before it expires (within 5 minutes of expiry) when the user makes any API request, using the encrypted password stored in the database. The system SHALL limit auto-refresh attempts to a maximum of 3 consecutive failures, after which the session is forcibly terminated (to handle cases where the AD password has been changed). #### Scenario: Auto-refresh AD token on protected route access - **WHEN** an authenticated user accesses a protected endpoint with a valid internal_token - **AND** the stored ad_token will expire in less than 5 minutes (ad_token_expires_at - now < 5 minutes) - **AND** refresh_attempt_count is less than 3 - **THEN** the system SHALL decrypt the encrypted_password from the database - **AND** re-authenticate with the AD API using the decrypted password - **AND** if authentication succeeds: update ad_token, ad_token_expires_at, reset refresh_attempt_count to 0 - **AND** update the last_activity timestamp - **AND** allow the request to proceed normally #### Scenario: No refresh needed for fresh AD token - **WHEN** an authenticated user accesses a protected endpoint with a valid internal_token - **AND** the stored ad_token will not expire within 5 minutes - **THEN** the system SHALL NOT call the AD API - **AND** update only the last_activity timestamp - **AND** allow the request to proceed #### Scenario: Auto-refresh fails but retry limit not reached - **WHEN** an authenticated user accesses a protected endpoint triggering auto-refresh - **AND** the AD API returns 401 (password invalid or changed) - **AND** refresh_attempt_count is 0, 1, or 2 - **THEN** the system SHALL increment refresh_attempt_count by 1 - **AND** log the failed refresh attempt with timestamp for audit - **AND** return status 401 with `{"error": "Token refresh failed. Please try again or re-login if issue persists."}` - **AND** keep the session record in the database #### Scenario: Auto-refresh fails 3 consecutive times - force logout - **WHEN** an authenticated user accesses a protected endpoint - **AND** refresh_attempt_count is already 2 - **AND** the AD API returns 401 on the 3rd refresh attempt - **THEN** the system SHALL increment refresh_attempt_count to 3 - **AND** delete the session record from the database - **AND** log the forced logout event with reason "Password may have been changed in AD" - **AND** return status 401 with `{"error": "Session terminated. Your password may have been changed. Please login again."}` #### Scenario: Session blocked due to previous 3 failed refresh attempts - **WHEN** an authenticated user accesses a protected endpoint - **AND** refresh_attempt_count is already 3 (from previous failed refreshes) - **THEN** the system SHALL delete the session record immediately - **AND** return status 401 with `{"error": "Session expired due to authentication failures. Please login again."}` ### Requirement: 3-Day Inactivity Timeout The system SHALL automatically invalidate user sessions that have been inactive for more than 3 days (72 hours). Inactivity is measured by the last_activity timestamp, which is updated on every API request. #### Scenario: Reject request from inactive session - **WHEN** a user accesses a protected endpoint with an internal_token - **AND** the last_activity timestamp is more than 3 days (72 hours) in the past - **THEN** the system SHALL delete the session record from the database - **AND** return status 401 with `{"error": "Session expired due to inactivity. Please login again."}` #### Scenario: Active user maintains session across multiple days - **WHEN** a user logs in on Day 1 - **AND** makes at least one API request every 2 days (Day 2, Day 4, Day 6, etc.) - **THEN** the system SHALL keep the session active indefinitely - **AND** update last_activity on each request - **AND** auto-refresh the AD token as needed ### Requirement: Token-Based Authentication for Protected Routes The system SHALL validate internal session tokens for protected API endpoints by checking the `Authorization: Bearer ` header against the user_sessions table, enforcing inactivity timeout and auto-refreshing AD tokens when necessary. #### Scenario: Access protected endpoint with valid active session - **WHEN** a request includes header `Authorization: Bearer ` - **AND** the session exists in user_sessions table - **AND** last_activity is within 3 days - **THEN** the system SHALL update last_activity to current time - **AND** check and refresh AD token if needed (per auto-refresh requirement) - **AND** allow the request to proceed with user identity available #### Scenario: Access protected endpoint with invalid internal token - **WHEN** a request includes an internal_token that does not exist in the user_sessions table - **THEN** the system SHALL return status 401 with `{"error": "Invalid or expired token"}` #### Scenario: Access protected endpoint without token - **WHEN** a request to a protected endpoint omits the Authorization header - **THEN** the system SHALL return status 401 with `{"error": "Authentication required"}`