# F5 BIG-IP WAF — Login / Refresh `accessToken` follow-up Date: 2026-10-28 Related symptom: after login (or later on the dashboard), `localStorage.accessToken` becomes the string `"undefined"`, white blank screen, user cannot logout until local storage is cleared. Worse with **WAF blocking mode**; usually fine when blocking is off. ## Goal `/api/login` and `/api/refresh-token` (and iAM Smart login APIs) must return **untouched JSON** from the backend in blocking mode, with a real JWT in `accessToken` / `refreshToken`. The WAF must **not**: - Replace the body with an HTML block / challenge page - Strip or alter the `accessToken` field - Rewrite long base64url JWT strings in JSON ## App-side mitigation (already done) Frontend changes stop a permanent stuck white screen: - JWT payload decode uses base64url (not raw `atob` on `-` / `_`) - Do not store a missing / `"undefined"` access token - Force clear session and redirect to `/login` when token is unusable App mitigation does **not** make a WAF-damaged response succeed. WAF/LB still needs tuning so login/refresh return a real JWT. ## API paths to protect (PNSPS) Base context path: `/api` (see backend `application.yml`). | Purpose | Method | Path (after host) | |---------|--------|-------------------| | Password login | POST | `/api/login` | | Refresh token | POST | `/api/refresh-token` | | Logout | POST | `/api/logout` | | iAM Smart login | POST | `/api/smart/login` | | iAM Smart direct login | POST | `/api/smart/directLogin` | Confirm exact URLs in browser **Network** tab if your reverse-proxy path differs. --- ## F5 BIG-IP WAF — step by step Menu names vary by BIG-IP / ASM / Advanced WAF version. Use the closest equivalent. ### 1. Confirm which policy sits in front of PNSPS 1. Log in to the BIG-IP UI. 2. Go to **Local Traffic → Virtual Servers**. 3. Open the virtual server for `pnsps.gld.gov.hk` (or the VS that terminates TLS for the site). 4. Note the attached **Application Security Policy** (ASM) / security policy name. 5. Also check whether **Bot Defense**, **DoS**, or other profiles are attached — those can inject HTML challenges. ### 2. Find what was blocked when `accessToken` became `"undefined"` 1. Go to **Security → Event Logs → Application → Requests** (or **Security → Application Security → Event Logs**). 2. Filter around the time of a failed login or blank dashboard. 3. Look for: - `/api/login` - `/api/refresh-token` - `/api/smart/login` / `/api/smart/directLogin` if using iAM Smart 4. Open the event and record: - **Support ID** - **Violation** name (signature, illegal meta character, evasion, bot challenge, etc.) - Whether the **response** was a block / challenge page instead of JSON That violation name is what you exception. ### 3. Ensure API URLs exist in the policy 1. Open the security policy → **URLs** (or **Policy → URLs**). 2. Ensure these URLs exist (exact path as in Network tab), for example: - `/api/login` - `/api/refresh-token` - `/api/smart/login` - `/api/smart/directLogin` 3. Method: **POST**. Prefer a **JSON** content profile if the policy uses content profiles. ### 4. Disable response rewriting for these APIs In the policy, review anything that inspects or rewrites **responses**: 1. **Data Guard** / **Response scrubbing** / **Information leakage** - Disable for `/api/*`, or exclude the URLs above. 2. **Response signatures** / checks that modify the body - Prefer alarm-only on responses for API URLs, or exclude them. 3. **Custom / attack signatures** that false-positive on JWT-looking JSON values - JWTs contain `-`, `_`, `.` and long base64url strings. - From the event log (step 2), **Learn / Allow / Exception** that signature for that URL only. ### 5. Add a targeted exception (preferred once violation is known) From the blocked request in the event log: 1. Open the request → **Accept / Learn** or **Create exception**. 2. Scope narrowly: - **URL** = `/api/login` and/or `/api/refresh-token` (and smart login URLs if needed) - Prefer **this signature + this URL**, not disabling the whole policy 3. Apply / publish the policy. ### 6. Bot Defense / challenge pages If the log shows a bot challenge or CAPTCHA HTML page on login/refresh: 1. Open **Security → Bot Defense** (or the policy Bot profile). 2. Whitelist / exception: - `/api/login` - `/api/refresh-token` - smart login paths if needed 3. Those endpoints must return **`application/json`** from the app, not an HTML interstitial. ### 7. Transparent vs Blocking (UAT validation only) 1. On a **UAT** policy clone, briefly use **Transparent** (or transparent blocking). 2. Reproduce login / stay on dashboard through refresh. 3. If the problem disappears only in transparent mode, the blocking policy is still rewriting or blocking — use event logs to exception those violations, then return to **Blocking**. 4. Do **not** leave production in transparent as the long-term fix. ### 8. Verify end-to-end 1. Browser → log in → **Network** → **login** response: - `Content-Type: application/json` - Body contains long `accessToken` and `refreshToken` strings (three JWT segments each) 2. Wait for refresh near expiry (or trigger refresh) → same checks on **refresh-token**. 3. Console: ```javascript localStorage.getItem('accessToken') ``` Must **never** be the literal `"undefined"`. --- ## Message for the F5 admin (copy/paste) > PNSPS SPA needs `/api/login` and `/api/refresh-token` (POST, JSON), and if used `/api/smart/login` and `/api/smart/directLogin`, to return the backend body unchanged in WAF **blocking** mode. The WAF must not replace them with an HTML block/challenge page or strip/alter `accessToken`. Please identify the violating signature/URL from ASM Event Logs and add a narrow exception; also disable response Data Guard / bot challenge on those API paths if they rewrite JSON. --- ## App verification checklist (after frontend deploy) | Test | Expected | |------|----------| | Normal password login + logout | Dashboard OK; clean logout | | Console: plant `accessToken` = `eyJhbGciOiJIUzUxMiJ9.Pj4-.sig` without `userData`, reload, wait 15s | No repeating `InvalidCharacterError`; redirect to `/login` | | Console: plant `accessToken` = `"undefined"` with `userData`, reload | Within ~10s redirect to `/login`; session keys cleared | | Login/refresh body missing `accessToken` (WAF case) | Stay on login / fail page or force logout — **not** permanent white screen | | iAM Smart success | Still works with real JWT | ### Console helpers Plant base64url payload that old `atob` rejected: ```javascript localStorage.removeItem('userData'); localStorage.setItem('accessToken', 'eyJhbGciOiJIUzUxMiJ9.Pj4-.sig'); location.reload(); ``` Plant stuck `"undefined"` session: ```javascript localStorage.setItem('userData', JSON.stringify({id:1,type:'IND',role:'primary'})); localStorage.setItem('accessToken', 'undefined'); location.reload(); ``` Cleanup: ```javascript localStorage.removeItem('accessToken'); localStorage.removeItem('userData'); location.reload(); ``` --- ## Responsibility split | Layer | Role | |-------|------| | Frontend | Do not store bad tokens; force logout instead of stuck white screen | | F5 WAF / LB | Do not damage login/refresh JSON so `accessToken` is present and valid | ## Related frontend files - `src/components/RefreshTokenProvider.js` — base64url decode + force logout on bad token - `src/auth/index.js` — `isAccessTokenUsable`, `forceLocalLogoutRedirect`, login/refresh guards - `src/auth/jwt/jwtService.js` — same guard on JwtService 401 refresh - Login forms / iAM Smart callbacks — skip success navigate when token unusable