# LLD — B1 Authentication Module **Scope:** JWT auth, RBAC dependencies, cookie strategy, refresh token rotation, permissions, data masking **Companion doc:** `docs/backend/01_authentication_module.md` --- ## Diagram 1 — Auth Module File Dependency Graph How the four auth files relate to each other and to the rest of the backend. ```mermaid graph TD subgraph AuthFiles["B1 — Files Created"] SEC["security.py\nhash_password()\nverify_password()\ncreate_access_token()\ncreate_refresh_token()\ndecode_token()\nhash_token()\nverify_token_hash()"] AUTH_SVC["auth_service.py\nauthenticate_user()\ncreate_token_pair()"] DEPS["dependencies.py\nget_db()\nget_current_user()\nrequire_role(*roles)"] AUTH_RT["routers/auth.py\nPOST /auth/login\nPOST /auth/refresh\nPOST /auth/logout\nGET /auth/me"] end subgraph Utils["Utilities"] PERM["permissions.py\nROLE_PERMISSIONS\nhas_permission()"] MASK["mask.py\nmask_ssn()\nmask_bank_account()\nmask_ein()"] end subgraph External["External Dependencies"] PASSLIB["passlib[bcrypt]\npwd_context"] JOSE["python-jose\njwt.encode/decode"] CFG["config.py\nsettings.secret_key\nsettings.algorithm\nsettings.access_token_expire_minutes\nsettings.refresh_token_expire_days"] USER_MODEL["models/user.py\nUser ORM model"] end subgraph AllRouters["All Other Routers (B3–B8)"] R3["properties.py"] R4["users.py"] R5["meetings.py"] R6["vendors.py"] R7["invoices.py"] R8["chatbot.py"] end SEC --> PASSLIB SEC --> JOSE SEC --> CFG AUTH_SVC --> SEC AUTH_SVC --> USER_MODEL DEPS --> SEC DEPS --> USER_MODEL AUTH_RT --> AUTH_SVC AUTH_RT --> DEPS AUTH_RT --> PERM DEPS -->|"require_role() imported by"| R3 & R4 & R5 & R6 & R7 & R8 MASK -->|"called by"| R4 style SEC fill:#1e3a5f,color:#fff style AUTH_SVC fill:#1e3a5f,color:#fff style DEPS fill:#1e5f3a,color:#fff style AUTH_RT fill:#3a1e5f,color:#fff style PERM fill:#5f3a1e,color:#fff style MASK fill:#5f3a1e,color:#fff ``` --- ## Diagram 2 — Password Hashing Lifecycle Two separate contexts where password functions are called: **user creation / seed** and **login verification**. ```mermaid flowchart LR subgraph Creation["User Creation / Seed Script"] PLAIN1["plain: 'password'"] HASH_FN["hash_password(plain)\npwd_context.hash()"] DB_STORE["users.password_hash\n'$2b$12$...(60 chars)'"] PLAIN1 --> HASH_FN --> DB_STORE end subgraph Login["Login Verification"] PLAIN2["plain from request\nrequest.password"] DB_READ["users.password_hash\nfrom DB row"] VERIFY["verify_password(plain, hashed)\npwd_context.verify()"] TRUE["True → proceed"] FALSE["False → return None\n→ 401 Invalid credentials"] PLAIN2 --> VERIFY DB_READ --> VERIFY VERIFY -->|match| TRUE VERIFY -->|no match| FALSE end subgraph NeverDo["❌ Never Do This"] BAD1["store plain text\nin DB"] BAD2["compare strings directly\nplain == stored"] BAD3["use MD5 / SHA1\nfor passwords"] end ``` --- ## Diagram 3 — JWT Token Anatomy Side-by-side view of what's inside each token type and where it lives. ```mermaid flowchart LR subgraph AccessToken["Access Token — 15 min"] direction TB AT_HDR["Header\n{ alg: HS256, typ: JWT }"] AT_PAY["Payload\n{\n sub: '3f8a1c2d-...(UUID)',\n role: 'FIELD_AGENT',\n legacy_id: 'e1',\n type: 'access',\n iat: 1740400000,\n exp: 1740400900\n}"] AT_SIG["Signature\nHMAC-SHA256(\n base64(header) + '.' + base64(payload),\n SECRET_KEY\n)"] AT_COOKIE["Cookie: access_token\nhttpOnly=true\nsecure=true\nsamesite=strict\npath=/\nmax_age=900s"] AT_HDR --> AT_PAY --> AT_SIG --> AT_COOKIE end subgraph RefreshToken["Refresh Token — 7 days"] direction TB RT_HDR["Header\n{ alg: HS256, typ: JWT }"] RT_PAY["Payload\n{\n sub: '3f8a1c2d-...(UUID)',\n type: 'refresh',\n iat: 1740400000,\n exp: 1741004800\n}"] RT_SIG["Signature\nHMAC-SHA256(...)"] RT_COOKIE["Cookie: refresh_token\nhttpOnly=true\nsecure=true\nsamesite=strict\npath=/api/v1/auth ← scoped\nmax_age=604800s"] RT_DB["DB: users.refresh_token_hash\nbcrypt hash of token string\n(not the token itself)"] RT_HDR --> RT_PAY --> RT_SIG --> RT_COOKIE RT_SIG -.->|"hash stored in"| RT_DB end note1["Access token has role + legacy_id\nso routers don't need extra DB lookup\nfor these common fields"] note2["Refresh token path scoped to /api/v1/auth\nso it is NOT sent with every API request —\nreduces exposure surface"] ``` --- ## Diagram 4 — Login Type Decision Tree How `authenticate_user()` maps the frontend's `loginType` to a DB query strategy. ```mermaid flowchart TD START["authenticate_user(\n db, identifier, password, login_type\n)"] START --> LT{login_type?} LT -->|"'customer'"| CUS["SELECT FROM users\nWHERE username = identifier\nAND role = 'CUSTOMER'\nAND deleted_at IS NULL\nAND is_active = true"] LT -->|"'employee'"| EMP["SELECT FROM users\nWHERE emp_id = identifier\nAND role IN ('FIELD_AGENT','ADMIN')\nAND deleted_at IS NULL\nAND is_active = true"] LT -->|"'owner'"| OWN["SELECT FROM users\nWHERE (username = identifier\n OR email = identifier)\nAND role = 'OWNER'\nAND deleted_at IS NULL\nAND is_active = true"] LT -->|"'contractor'"| CON["SELECT FROM users\nWHERE (username = identifier\n OR email = identifier)\nAND role = 'CONTRACTOR'\nAND deleted_at IS NULL\nAND is_active = true"] LT -->|"'subcontractor'"| SUB["SELECT FROM users\nWHERE (username = identifier\n OR email = identifier)\nAND role = 'SUBCONTRACTOR'\nAND deleted_at IS NULL\nAND is_active = true"] LT -->|"'vendor'"| VEN["SELECT FROM users\nWHERE (username = identifier\n OR email = identifier)\nAND role = 'VENDOR'\nAND deleted_at IS NULL\nAND is_active = true"] LT -->|"anything else"| NONE["return None\n→ 401"] CUS & EMP & OWN & CON & SUB & VEN --> FOUND{user row\nreturned?} FOUND -->|No| NONE2["return None → 401\n'Invalid credentials'"] FOUND -->|Yes| PWD["verify_password(\n request.password,\n user.password_hash\n)"] PWD -->|False| NONE3["return None → 401\n'Invalid credentials'\n(same message — never\nreveal which field failed)"] PWD -->|True| RETURN["return User object\n→ create_token_pair()"] subgraph SeedRef["Seed credentials reference"] S1["CUSTOMER: username='alice'"] S2["FIELD_AGENT: emp_id='FA001'–'FA005'"] S3["ADMIN: emp_id='ADM01'–'ADM03'"] S4["OWNER: username='justin'/'diana'"] S5["CONTRACTOR: username='mike'"] S6["SUBCONTRACTOR: username='carlos'"] S7["VENDOR: username='abc_supply'"] end ``` --- ## Diagram 5 — Refresh Token Rotation + Reuse Attack Detection The full refresh flow, including the security branch when a reused refresh token is detected. ```mermaid sequenceDiagram participant FE as React (interceptors.js) participant API as POST /auth/refresh participant SVC as auth_service.py participant SEC as security.py participant DB as users table Note over FE: Received 401 on any request Note over FE: isRefreshing flag set to true Note over FE: Other 401s queued in failedQueue FE->>API: POST /auth/refresh\nCookie: refresh_token= API->>SEC: decode_token(refresh_token) alt Token missing or expired SEC-->>API: JWTError API-->>FE: 401 "Invalid or expired refresh token" FE->>FE: dispatch('auth:logout-required') FE->>FE: redirect to /login else Token valid SEC-->>API: { sub: user_id, type: "refresh" } API->>DB: SELECT * FROM users WHERE id = user_id alt User not found / deactivated / soft-deleted DB-->>API: None API-->>FE: 401 else User found DB-->>API: User row (includes refresh_token_hash) API->>SEC: verify_token_hash(\n incoming_token,\n user.refresh_token_hash\n) alt Hash mismatch ← REUSE ATTACK Note over API,DB: Token was already rotated.\nSomeone is replaying an old token. API->>DB: UPDATE users\nSET refresh_token_hash = NULL\n(revoke ALL sessions) API-->>FE: 401 "Refresh token reuse detected" FE->>FE: dispatch('auth:logout-required') FE->>FE: force logout + redirect /login else Hash matches — normal rotation API->>SEC: create_access_token(user_id, role, legacy_id) API->>SEC: create_refresh_token(user_id) SEC-->>API: new_access, new_refresh API->>SEC: hash_token(new_refresh) SEC-->>API: new_hash API->>DB: UPDATE users\nSET refresh_token_hash = new_hash\n last_login_at = NOW() API-->>FE: 200 Set-Cookie: access_token=new\nSet-Cookie: refresh_token=new Note over FE: processQueue(null) — release all queued requests FE->>FE: Retry original request(s) end end end ``` --- ## Diagram 6 — Cookie Security Configuration How the two cookies differ in path, scope, and lifetime, and why. ```mermaid flowchart TD subgraph AccessCookie["access_token Cookie"] AC1["key: 'access_token'"] AC2["httponly: True\n← JS cannot read it"] AC3["secure: True\n← HTTPS only"] AC4["samesite: 'strict'\n← blocks CSRF"] AC5["max_age: 900s (15 min)"] AC6["path: '/'\n← sent with ALL requests to backend"] AC1 --> AC2 --> AC3 --> AC4 --> AC5 --> AC6 end subgraph RefreshCookie["refresh_token Cookie"] RC1["key: 'refresh_token'"] RC2["httponly: True"] RC3["secure: True"] RC4["samesite: 'strict'"] RC5["max_age: 604800s (7 days)"] RC6["path: '/api/v1/auth'\n← only sent to /auth/* endpoints\nnot leaked with every API call"] RC1 --> RC2 --> RC3 --> RC4 --> RC5 --> RC6 end subgraph XSSProtection["Why httpOnly?"] XSS1["XSS attack injects script:\ndocument.cookie → blocked\nlocalStorage.getItem() → blocked\nfetch with credentials → blocked by samesite"] end subgraph CSRFProtection["Why samesite=strict?"] CSRF1["Attacker tricks victim\ninto clicking evil.com link\nwhich sends request to api.lynkeduppro.com\nsamesite=strict: cookie NOT attached\nto cross-site navigation"] end ``` --- ## Diagram 7 — Permission Matrix Decision Flow How `has_permission()` is used inside service functions (not routers) for fine-grained access control. ```mermaid flowchart TD subgraph RolePermTable["ROLE_PERMISSIONS — Full Matrix"] direction LR HDR["Permission → Role grants"] VP["VIEW_ALL_PROJECTS\n→ OWNER ✅ ADMIN ✅"] VPE["VIEW_ALL_PERSONNEL\n→ OWNER ✅ ADMIN ✅"] VF["VIEW_FINANCIALS\n→ OWNER ✅ ADMIN ✅"] VS["VIEW_SENSITIVE_DATA\n→ OWNER ✅ only"] MU["MANAGE_USERS\n→ OWNER ✅ ADMIN ✅"] AD["APPROVE_DOCUMENTS\n→ OWNER ✅ ADMIN ✅"] VAP["VIEW_ASSIGNED_PROJECTS\n→ OWNER ✅ ADMIN ✅ CONTRACTOR ✅ SUBCONTRACTOR ✅"] UD["UPLOAD_DOCUMENTS\n→ OWNER ✅ ADMIN ✅ CONTRACTOR ✅ VENDOR ✅"] SI["SUBMIT_INVOICES\n→ OWNER ✅ ADMIN ✅ CONTRACTOR ✅ VENDOR ✅"] MC["MANAGE_OWN_CREW\n→ CONTRACTOR ✅ only"] VAT["VIEW_ASSIGNED_TASKS\n→ OWNER ✅ ADMIN ✅ CONTRACTOR ✅ SUBCONTRACTOR ✅"] UTS["UPDATE_TASK_STATUS\n→ OWNER ✅ ADMIN ✅ CONTRACTOR ✅ SUBCONTRACTOR ✅"] end subgraph Usage["Service Layer Usage Pattern"] SVC["service function receives\ncurrent_user: User"] CHECK["has_permission(\n current_user.role,\n 'VIEW_SENSITIVE_DATA'\n)"] GRANT["Return full data\nincluding SSN, EIN,\nbank accounts"] DENY["Return masked data\nmask_ssn(user.ssn)\nmask_ein(user.ein)\nmask_bank_account(user.bank)"] SVC --> CHECK CHECK -->|True — OWNER only| GRANT CHECK -->|False — all other roles| DENY end subgraph RouterVsService["Router vs Service Responsibility"] ROUTER_CHECK["Router:\nrequire_role('ADMIN','OWNER')\n← coarse: is this role allowed at all?"] SERVICE_CHECK["Service:\nhas_permission(role, 'VIEW_FINANCIALS')\n← fine: which fields/operations?"] ROUTER_CHECK -->|"if pass"| SERVICE_CHECK end ``` --- ## Diagram 8 — Sensitive Data Masking Flow When `mask.py` is invoked per endpoint and per role. ```mermaid flowchart TD REQ["GET /users/{id}\nor GET /users (list)"] AUTH["get_current_user() → User\nrequire_role('ADMIN','OWNER','CONTRACTOR',..."] AUTH --> ROLE{current_user.role?} ROLE -->|OWNER| FULL["Return FULL data\nssn: '123-45-6789'\nein: '12-3456789'\nbank_account: '9876543210'"] ROLE -->|ADMIN\nCONTRACTOR\nFIELD_AGENT\nVENDOR\nSUBCONTRACTOR| MASKED["Apply mask.py\nssn → mask_ssn() → '***-**-6789'\nein → mask_ein() → '**-***6789'\nbank → mask_bank_account() → '******3210'"] ROLE -->|CUSTOMER| OWN_ONLY["CUSTOMER sees only\ntheir own record\nwith masked sensitive fields"] subgraph MaskFunctions["mask.py — Functions"] M1["mask_ssn('123-45-6789')\n→ '***-**-6789'\nlast 4 digits visible"] M2["mask_bank_account('9876543210')\n→ '******3210'\nlast 4 digits visible"] M3["mask_ein('12-3456789')\n→ '**-***6789'\nlast 4 digits visible"] end MASKED --> MaskFunctions OWN_ONLY --> MaskFunctions ``` --- ## Diagram 9 — `get_current_user()` Dependency Resolution The exact path FastAPI takes to resolve the `get_current_user` dependency on every protected request. ```mermaid flowchart TD RT["Protected route handler called\nDepends(get_current_user)"] RT --> COOKIE["Extract cookie:\naccess_token: str | None = Cookie(default=None)"] COOKIE --> HAS{access_token\npresent?} HAS -->|No| E401A["raise HTTPException 401\n'Not authenticated'"] HAS -->|Yes| DECODE["decode_token(access_token)\njwt.decode(token, SECRET_KEY, algorithms)"] DECODE --> VALID{Valid\nsignature?} VALID -->|No / tampered| E401B["raise HTTPException 401\n'Not authenticated'"] VALID -->|Yes| EXPIRED{Token\nexpired?} EXPIRED -->|Yes| E401C["raise HTTPException 401\n'Not authenticated'\n→ FE interceptor triggers /auth/refresh"] EXPIRED -->|No| TYPE{"payload['type']\n== 'access'?"} TYPE -->|No (refresh token sent by mistake)| E401D["raise HTTPException 401"] TYPE -->|Yes| SUB["user_id = payload['sub']\nrole = payload['role']"] SUB --> DB["db.get(User, user_id)"] DB --> EXISTS{User found?\ndeleted_at IS NULL?\nis_active = True?} EXISTS -->|No| E401E["raise HTTPException 401\n'Not authenticated'"] EXISTS -->|Yes| INJECT["return User object\n→ injected as current_user\ninto route handler"] INJECT --> ROLE_CHECK["require_role() checker:\nif current_user.role not in allowed_roles\n→ 403 FORBIDDEN\nelse → proceed to handler"] ```