Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

csp-api-logout-404-follow-up.md 9.0 KiB

há 3 semanas
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. # Follow-up: `connect-src` reports for `/api/logout` (404)
  2. PROD CSP reports from 2026-10-13 and 2026-10-14. The browser is on
  3. `https://pnsps.gld.gov.hk/login` and reports
  4. `https://pnsps.gld.gov.hk/api/logout` as a `connect-src` violation with
  5. **HTTP 404**.
  6. This is not a React route, and it is not a Content-Security-Policy block.
  7. The logout API call is reaching something that answers "not found". Chrome
  8. then files a report-only CSP violation for that failed same-origin request.
  9. Related docs:
  10. - [csp-apache.conf.md](./csp-apache.conf.md) — current CSP header
  11. - [csp-report-review-2026-09-22.md](./csp-report-review-2026-09-22.md) — earlier PROD review (`status-code: 404` on the SPA shell)
  12. ---
  13. ## What the report actually says
  14. Example (same shape on every hit in this batch):
  15. | Field | Value | Meaning |
  16. |---|---|---|
  17. | `document-uri` | `https://pnsps.gld.gov.hk/login` | The page that was loaded when the report was sent |
  18. | `referrer` | usually `https://pnsps.gld.gov.hk/dashboard`; one hit from `https://epaygateway2.gcis.gov.hk/` | Where the user was before `/login` |
  19. | `violated-directive` | `connect-src` | Chrome classified the failed request under `connect-src` |
  20. | `blocked-uri` | `https://pnsps.gld.gov.hk/api/logout` | The request that failed |
  21. | `status-code` | `404` | The server answered. A real CSP block is status `0` |
  22. | `disposition` | `report` | Report-Only. The policy did not enforce a block |
  23. | Policy | `connect-src 'self'` | Same host is already allowed |
  24. Do not add anything to `connect-src` for this. `'self'` already covers
  25. `https://pnsps.gld.gov.hk`.
  26. ---
  27. ## Why this is not a React route
  28. React Router only runs after an HTML document has loaded, and only for the
  29. address bar (`/login`, `/dashboard`, and the other paths in
  30. `src/routes/`). It never sees `axios` / `fetch` calls.
  31. `/api/logout` is an API call, not a page:
  32. 1. The user is on `/dashboard` (or returns from the payment gateway).
  33. 2. Logout runs, or a 401 refresh fails.
  34. 3. The app posts to `/api/logout`, then moves the browser to `/login`.
  35. Frontend call (`PNSPS-frontend-MaterialUI`):
  36. - `src/utils/ApiPathConst.js` — `LOGOUT = "/logout"`
  37. - `src/auth/utils.js` — production `apiPath` is `/api`
  38. - `src/auth/index.js` — `handleLogoutFunction` does
  39. `axios.post(\`${apiPath}${LOGOUT}\`, { refreshToken })`, which is
  40. `POST /api/logout`
  41. - Callers then `navigate('/login')`. The axios 401 path also does
  42. `window.location.reload()`
  43. `/login` is already a React route (`src/routes/LoginRoutes.js`, path
  44. `login`). The document in the report loaded. The 404 is the POST, not the
  45. login page.
  46. Adding a React route for `/api/logout` or `/logout` does not receive this
  47. POST. The browser sends it to Apache before React runs.
  48. ---
  49. ## What the backend already does
  50. `PNSPS-backend`:
  51. - `src/main/resources/application.yml` — `server.servlet.contextPath: /api`
  52. - `JwtAuthenticationController` — `@PostMapping("/logout")`, response
  53. **204 No Content**
  54. - `SecurityConfig` — `LOGOUT_URL = "/logout"` is on `URL_WHITELIST`
  55. (`permitAll`), so an expired session can still call it
  56. - `RefreshTokenService.revokePresented` — unknown or bad tokens return
  57. without an error. A request that hits this method is still **204**
  58. So a correct `POST /api/logout` is 204, including a dummy refresh token.
  59. **404 means the request never entered that method.**
  60. ---
  61. ## What to change on Apache
  62. Two rules, in this order. `/api` must be proxied to Spring. Page paths that
  63. are not real files must be served as `index.html` with **status 200**.
  64. ### Step 1. Confirm where `/api` goes today
  65. On the PROD Apache host, find the vhost for `pnsps.gld.gov.hk` and note:
  66. - `ProxyPass` / `ProxyPassReverse` for `/api`
  67. - Any `RewriteRule` that sends unknown paths to `index.html`
  68. - Any `ErrorDocument 404` or `FallbackResource`
  69. `ErrorDocument 404 /index.html` is the wrong tool for a React app. The body
  70. can be the SPA while the status stays **404**. That is the "SPA shell 404"
  71. called out in the 2026-08-03 and 2026-09-22 reviews. It also makes a missed
  72. API path look like a client route.
  73. ### Step 2. Keep `/api` on the backend
  74. `/api` must be handled before any SPA rewrite, and must not be rewritten to
  75. `index.html`.
  76. ```apache
  77. ProxyPreserveHost On
  78. ProxyPass /api http://127.0.0.1:8090/api
  79. ProxyPassReverse /api http://127.0.0.1:8090/api
  80. ```
  81. Use the real upstream (host and port) already used for
  82. `/api/csp-report` and `/api/login`. Do not invent a new port if PROD is
  83. not on `8090`.
  84. If a rewrite engine is also on, stop rewriting `/api`:
  85. ```apache
  86. RewriteEngine On
  87. RewriteRule ^/api/ - [L]
  88. ```
  89. If `/api/logout` is rewritten to `index.html`:
  90. - the refresh token is never revoked
  91. - the browser still sees a failed logout call
  92. - these CSP reports continue
  93. ### Step 3. Serve React routes as 200 `index.html`
  94. Apply this only after the `/api` rule. It covers `/login`, `/dashboard`,
  95. and the other client paths. Real files (`/static/js/...`, css, images) stay
  96. as files.
  97. ```apache
  98. RewriteEngine On
  99. # API is the backend, not a React route. Must stay above the fallback.
  100. RewriteRule ^/api/ - [L]
  101. RewriteCond %{REQUEST_FILENAME} !-f
  102. RewriteCond %{REQUEST_FILENAME} !-d
  103. RewriteRule ^ /index.html [L]
  104. ```
  105. Remove `ErrorDocument 404 /index.html` for this vhost if it is what currently
  106. loads the SPA. `FallbackResource /index.html` is acceptable only when
  107. `/api` is excluded and the fallback response is **200**.
  108. ### Step 4. Reload Apache and deploy the backend that contains logout
  109. The 204 handler has to be in the build that PROD is running. A proxy to an
  110. old jar that has no `POST /logout` still returns 404 from Spring.
  111. ```bash
  112. sudo apachectl configtest
  113. sudo systemctl reload apache2
  114. ```
  115. Use `httpd` instead of `apache2` if that is the service name on this host.
  116. ### Step 5. Prove the POST hits Spring
  117. From a machine that can reach PROD:
  118. ```bash
  119. curl -sS -D - -o /dev/null \
  120. -X POST "https://pnsps.gld.gov.hk/api/logout" \
  121. -H "Content-Type: application/json" \
  122. -d '{"refreshToken":"not-a-real-token"}'
  123. ```
  124. | Status | Meaning | Next action |
  125. |---|---|---|
  126. | **204** | Handler ran. Bad token is ignored on purpose | Apache and the deployed jar are fine. Go to step 6 |
  127. | **404** | Still not the logout method | Response body: Spring JSON means the proxy works but this build has no mapping. HTML (`index.html` or an Apache error page) means the SPA/error fallback caught `/api/logout`. Fix step 2, and deploy the backend if the body is Spring's 404 |
  128. | **401** | Request reached Spring Security and was not on the whitelist | Deploy the build where `LOGOUT_URL` is in `URL_WHITELIST` |
  129. | **405** | Path exists, method does not match | Something sent GET. The app sends POST. Check for a redirect that turned POST into GET |
  130. | **502 / 503** | Proxy target down or wrong | Fix the `ProxyPass` upstream |
  131. Also check a real page, not only the API:
  132. ```bash
  133. curl -sS -D - -o /dev/null "https://pnsps.gld.gov.hk/login"
  134. curl -sS -D - -o /dev/null "https://pnsps.gld.gov.hk/dashboard"
  135. ```
  136. Both should be **200** and `Content-Type: text/html` (the SPA shell). A
  137. **404** here means step 3 is not in effect.
  138. ### Step 6. Prove logout in the browser
  139. 1. Sign in and open `/dashboard`.
  140. 2. Open devtools → Network. Keep "Preserve log" on.
  141. 3. Use the header logout control.
  142. 4. Expect `POST /api/logout` → **204**, then the document request for
  143. `/login` → **200**.
  144. 5. Repeat once by waiting for session expiry (the axios path in
  145. `src/auth/index.js` posts logout, then `navigate('/login')` and
  146. `window.location.reload()`). Same 204, then `/login` 200.
  147. The payment-gateway referrer in one report is the same logout POST. The
  148. user landed on `/login` after `epaygateway2.gcis.gov.hk`; it is not a
  149. separate route to allowlist.
  150. ### Step 7. Watch CSP reports
  151. After the 204s are confirmed, new `connect-src` hits for
  152. `blocked-uri` `https://pnsps.gld.gov.hk/api/logout` should stop. Reports
  153. already stored are historical.
  154. Leave the CSP header as in [csp-apache.conf.md](./csp-apache.conf.md).
  155. Do not add origins, and do not drop `connect-src 'self'`.
  156. ---
  157. ## What not to change
  158. | Idea | Why it does not fix this |
  159. |---|---|
  160. | New React route for `/logout` or `/api/logout` | The POST never goes through the router. `LogoutPage.js` exists and is not mounted; mounting it would not see `axios.post` |
  161. | Rewrite `/api/*` to `index.html` | Logout never revokes the refresh token |
  162. | Allow `/api/logout` in CSP | `'self'` already allows it. Status 404 shows the request was sent |
  163. | Turn off Report-Only to "hide" it | Hides the symptom. The POST would still 404 |
  164. ---
  165. ## Code map
  166. | Piece | Location |
  167. |---|---|
  168. | Logout POST | `PNSPS-frontend-MaterialUI/src/auth/index.js` (`handleLogoutFunction`) |
  169. | 401 → logout → `/login` → reload | same file, axios response interceptor |
  170. | Header logout | `src/layout/MainLayout/Header/index.js` (`handleLogout`) |
  171. | API path | `src/utils/ApiPathConst.js`, `src/auth/utils.js` |
  172. | Login route | `src/routes/LoginRoutes.js` (`path: 'login'`) |
  173. | `POST /logout` → 204 | `PNSPS-backend` `JwtAuthenticationController` |
  174. | Whitelist | `SecurityConfig.LOGOUT_URL` |
  175. | Context path `/api` | `src/main/resources/application.yml` |