Repository navigation
COG-1160: W1 AdminPortal XSRF-TOKEN plumbing (trailing-slash audit: no changes needed) - #344
Conversation
…minPortal login Co-Authored-By: patrick.bradley <patrick.bradley@cognition.ai>
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
| if (token) { | ||
| headers.set(XSRF_HEADER_NAME, token); |
There was a problem hiding this comment.
🟡 Existing XSRF cookie blocks admin login
When localhost has an XSRF-TOKEN cookie, xsrfOptions adds a header that triggers a preflight. RequestFilter omits that header from its allowlist, so the browser blocks login.
Learn more
The AdminPortal runs at localhost:4200 and posts credentials to localhost:8080, so its requests are cross-origin. The existing RequestFilter answers OPTIONS requests without permitting x-xsrf-token. A browser sends that preflight as soon as the cookie is present, even when server-side CSRF is disabled. The preflight fails, and the browser never sends the login POST.
Example: A different localhost application previously set XSRF-TOKEN=abc with path /. An admin loads the portal and submits valid credentials. The new header triggers OPTIONS /index; the browser rejects the response and the admin cannot log in.
Recommended fix: Coordinate the header rollout with an update to RequestFilter that permits x-xsrf-token on preflight responses. Verify login with a cookie already present, not just a fresh browser profile.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
This does happen, and it's a known edge case of the design the audit prescribes (COG-1147 item M6). Adding the header only when the cookie exists is what keeps it from firing on a clean browser against the 1.5.4 server. The real fix is the one you point to: allow x-xsrf-token in the preflight. That belongs to COG-1165 (CorsConfig replacing RequestFilter), which is out of scope for this TypeScript-only W1 ticket, and it has to land before or with COG-1166 anyway. Until then, the trigger is a stray XSRF-TOKEN cookie set on localhost by some other app, which doesn't happen in the normal dev or prod setup. Leaving the code as is and flagging it for COG-1165.
There was a problem hiding this comment.
Follow-up from browser testing: this already happens on master. Angular 4 HttpModule registers CookieXSRFStrategy('XSRF-TOKEN', 'X-XSRF-TOKEN') by default (@angular/http XHRConnection → configureRequest). So any AdminPortal request already carries X-XSRF-TOKEN whenever that cookie exists, and hits the same preflight rejection. Testing with a synthetic cookie confirmed it: the bootstrap GET, which doesn't use xsrfOptions, was preflighted and blocked the same way. The fix is still the COG-1165 allow-list.
| // GET /index is permitAll, so unlike a protected URL it is not stored as the post-login redirect target. | ||
| private fetchCsrfToken(): Observable<any> { | ||
| const url = 'http://localhost:8080/index'; | ||
| return this.http.get(url, { withCredentials: true }).catch(() => Observable.of(null)); |
There was a problem hiding this comment.
There was a problem hiding this comment.
This is intentional. The bootstrap GET is best-effort, and a failure there shouldn't stop the login attempt: while server CSRF is off, the POST works without a token. Once COG-1166 turns CSRF on, a failed bootstrap means the POST goes out without X-XSRF-TOKEN, the server rejects it with 403, and that error reaches LoginComponent's err callback. So the failure does surface, just as a failed login. I'm not adding a separate error state for the bootstrap here, because it would change the login component's UX, which is outside this ticket.
Summary
Linear: COG-1160. This is a W1 forward-compatible change: TypeScript only, no Java changes, no version bumps. It builds on the COG-1147 audit (#340,
docs/adminportal-http-inventory.md)./, so there is nothing to remove.AdminPortal/src/app/xsrf.tshelper.RequestFilterpreflight does not listx-xsrf-tokeninAccess-Control-Allow-Headers. While server CSRF is disabled there is no cookie, so no header and no preflight: nothing changes on the wire today.login.service.tsGET /index, notGET /api/csrf. I tested/api/csrfagainst master with curl. On master it is an authenticated URL, so Spring Security saves it as the post-login target, and the loginPOST /indexthen redirects to it:GET /indexispermitAll, so it is never saved. It also renders theth:actionlogin form, which reads the CSRF token, so once W2 turns CSRF on withCookieCsrfTokenRepositorythis same request writes theXSRF-TOKENcookie. After COG-1159 (COG-1159: W1 · Add GET /api/csrf token-bootstrap endpoint (204) #334) lands, switching the bootstrap to/api/csrfbecomes safe, but it isn't needed.withCredentialsis already set on all 9 calls (per the audit) and on the new bootstrap GET.Out of scope, file-disjoint from sibling PRs:
appointment.service.tsbelongs to COG-1153 anduser.service.tsto COG-1154. Each currently inlines its own cookie read. Both should switch toxsrfOptions()from./xsrfwhen rebased on this PR.logout()stays a GET (audit item K1, which only applies at W5).RequestFilteris left alone (that is COG-1165).For the server side (W2), still not done here: configure
csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()). Otherwise Security 6+ rejects the plain cookie value this client echoes back.Verification (Boot 1.5.4 unmodified, Temurin 8, mysql:5.7, Node 12)
Browser run-through (login -> user list -> statements -> appointments) is posted in a PR comment below.
Link to Devin session: https://app.devin.ai/sessions/c0c85e18808b4bc799be66afa6c80916
Open in Devin Desktop: https://app.devin.ai/desktop/session/c0c85e18808b4bc799be66afa6c80916?variant=devin
Requested by: @patrickbradley-cog
Devin Review