Authentication (Client-side)¶
Status: Audit v0.1 (2026-05-09).
This document covers the client-side authentication posture of the web client. Because identity in this codebase is split across @nuxtjs/auth, ~20+ cookies, localStorage, and Vuex state, the surface area is large.
The existing project doc ../03-auth-and-sessions.md maps the cookie inventory and the post-login redirect decision tree exhaustively. This audit doc captures the security posture of those mechanisms.
1. Auth library¶
| Property | Value | Source |
|---|---|---|
| Library | @nuxtjs/auth v4.9.1 |
package.json:24 |
| Strategy | local only |
nuxt.config.js:298-310 |
| Login endpoint | POST /webauthenticate |
nuxt.config.js:301-305 |
| User-fetch endpoint | GET /me |
nuxt.config.js:306 |
| Logout (server-side) | Disabled at strategy level | nuxt.config.js:307 (logout: false) |
| Cookie options | sameSite: 'none'; secure: true |
nuxt.config.js:286-291 |
| Redirects | login: '/#login', home: '/redirect' |
nuxt.config.js:292-297 |
Finding [WC-AUTH-1] (Severity: Medium): Logout is disabled at the auth-strategy level (logout: false). This means app.$auth.logout() is a client-only operation — no backend revocation call. Consequence: a leaked token remains valid until natural expiry, even if the user explicitly logs out. Recommendation: add a /auth/logout endpoint backend-side and wire it up.
2. Token storage¶
| Storage | What | Source | Security implication |
|---|---|---|---|
token cookie |
The session token | Set in login flows in store/index.js (getUserData, socialRegister, linkedInLogin, twitterLogin) |
Cookie is not httpOnly — the auth strategy uses cookie-universal-nuxt which sets browser-readable cookies. Readable from JS, hence XSS-readable. |
usedetail cookie |
Full user-detail object incl. accountId |
Same | Same — not httpOnly. |
auth._token.local |
@nuxtjs/auth's own token cookie |
Set by the strategy | Set by cookie-universal-nuxt; not httpOnly. |
auth.strategy |
'local' |
Set by the strategy | Same. |
localStorage.accountId |
Account ID | Set by middleware/head.js (which is unwired — see routing-and-state.md) |
XSS-readable by definition. |
localStorage.loggedInUserId |
User ID | Set by Polotno editor | Same. |
Vuex state.user |
User record | Rarely populated; cleared on refresh | Transient. |
app.$auth.user |
User record (via @nuxtjs/auth) |
Set after /me succeeds |
In-memory only. |
Finding [WC-AUTH-2] (Severity: HIGH): The session token is stored in browser-readable cookies and (transitively) accessible to any JavaScript on the origin. This is the single most consequential auth-client finding.
XSS exposure surface:
- 9 v-html occurrences across 4 component files (Chatbot.vue 4, Layout/EaMainSection.vue 2, Forms/EaInputSwitch.vue 1, Dashboard/EaGauge.vue 2), including Chatbot.vue rendering chat messages without sanitisation (grep -rEn "v-html" components)
- 161 components, none using DOMPurify or any sanitisation library (grep -rEn "DOMPurify\|sanitize" pages components)
- 14 external third-party CDN scripts loaded via <script src=...> in nuxt.config.js → head.script (jQuery, Bootstrap × 3 versions, Popper × 2, Stripe, Paddle, Clarity, slick-carousel, jQuery-validate, Flagmeister, FirstPromoter, FontAwesome kit) — any compromise of these CDNs (no SRI, see security.md) executes JS with token-cookie read access
Combined exposure: XSS through any of these vectors equals account takeover. The standard mitigation (httpOnly cookie) is not in play.
Mitigations:
1. Phase 0: Move the session token to an httpOnly cookie. This requires backend cooperation (the backend must set the cookie, with Secure; HttpOnly; SameSite=None); the frontend stops touching it.
2. Phase 0: Add CSP header (currently absent — see security.md) to limit script-source origins.
3. Phase 0a: Add Subresource Integrity to the third-party CDN scripts.
4. Phase 1: Sanitise all v-html use sites (DOMPurify), or refactor to interpolation where possible.
[VERIFY-AUTH-1]: Does the backend's auth API have the option to set an httpOnly Set-Cookie response? If yes, the move is straightforward.
3. Cookie inventory¶
The full cookie inventory is in ../03-auth-and-sessions.md. Quick audit-relevant summary:
| Cookie | Sensitive? | Notes |
|---|---|---|
token |
Highly — auth bearer | Not httpOnly; XSS-readable |
usedetail |
Yes — full user record incl. accountId, email, memberId |
Not httpOnly |
accountId |
Sensitive (identifier) | Not httpOnly |
useid, username, email, avatar |
PII | Not httpOnly |
impersonationLogin |
Privilege escalation if forged | Cookie is read by middleware to suppress safety guards |
impersonredirect |
One-shot redirect flag | Less sensitive |
socialAccConnect, paddleTxnId, currentUrl |
Flow control | Not security-critical |
subs |
{ sub_id } cache |
Not sensitive |
accountdetails |
{ isInvited, noId, roleType, accountId, onboardingTypeId } |
Roles in a cookie — see Finding [WC-AUTH-4] |
auth._token.facebook, auth._token.instagram |
Read by plugins/auth.js but no Facebook/Instagram strategies are defined |
Likely dead |
fbconn |
Marker for Facebook OAuth roundtrip | Less sensitive |
Finding [WC-AUTH-3] (Severity: High): PII (email, full name, avatar URL, member ID, account ID) is stored in browser-readable cookies. This isn't strictly a token-leak risk, but combined with no httpOnly enforcement, it's poor data-handling hygiene and may have GDPR implications (a third-party script can read all of this).
Finding [WC-AUTH-4] (Severity: High): The accountdetails cookie contains roleType and isInvited. The frontend reads these to gate access (see 12-permissions-and-roles.md in the existing dev docs). A user who edits the cookie locally can flip roleType to 5 (owner) and bypass owner-only routes. The backend must enforce role checks server-side (it likely does — [VERIFY-AUTH-2]: confirm the backend re-validates roleType on every privileged endpoint).
This is not necessarily a critical issue if the backend is the actual security boundary, but the frontend should not gate on cookie-resident roles. Move role-gating decisions to data fetched per-navigation from the backend (with caching) rather than a cookie that the user can edit.
4. Login flows¶
Five flows lead to authenticated state. All ultimately call $auth.loginWith('local', ...) at the end so the auth strategy's interceptors are active.
| Flow | Action | Endpoint |
|---|---|---|
| Email/password | Direct $auth.loginWith('local', ...) |
/webauthenticate |
| Social (generic) | socialRegister action |
/socialRegister |
linkedInLogin action |
/linkedInRegister |
|
twitterLogin action |
/twitterRegister |
|
| Twitter relink | twitterRLAuth action |
/twitterAuth |
The literal placeholder password: '##########' is sent by all social-login loginWith('local') calls — the backend recognises the social: true flag and bypasses password verification.
Finding [WC-AUTH-5] (Severity: Medium): The placeholder password '##########' is sent over the wire on every social login. This is fine if the backend explicitly checks social: true first; [VERIFY-AUTH-3] that this is the actual backend behaviour and not a "works because the password is incidentally accepted" pattern.
5. OAuth flow¶
OAuth is initiated client-side; the backend handles token exchange.
- Facebook —
@nuxtjs/authFacebook strategy is not defined innuxt.config.js, butplugins/auth.jsreadsauth._token.facebookcookie. [VERIFY-AUTH-4]: is Facebook auth wired through a different mechanism? If not, this is dead code. - Twitter — uses Nuxt server middleware (
api/twitter-middleware.js) for the callback handshake (OAuth 1.0a needs server-side signing). - LinkedIn — frontend → backend (
/linkedInRegister). - TikTok — frontend → backend (
/auth/tiktokRegister).
6. Logout¶
The app.$auth.logout() call is invoked from a number of error paths in store/index.js and middleware/redirect.js. Each is paired with:
Finding [WC-AUTH-6] (Severity: Low): localStorage.clear() wipes the Polotno editor's in-progress design autosaves on logout. If a user accidentally logs out mid-edit, their unsaved design is gone. Verify with the team whether this is intentional. Could be mitigated by namespacing Polotno autosaves under a key prefix and selectively preserving them.
7. Token expiry handling¶
The client does not detect token expiry. There is no:
- JWT decode to read exp
- Background polling of /me
- Response interceptor catching 401
When the token expires, the next cached endpoint silently returns stale data; the next navigation triggers redirect.js → getuser → catch-all logout. See ./api-consumption.md Finding WC-API-10.
8. MFA / 2FA¶
Two endpoints exist: /sendMFA, /MFAauthenticate. The frontend usage is inline — no dedicated MFA component visible at a glance. [VERIFY-AUTH-5]: Is MFA optional/required/disabled by default? Is it enforced at the backend or only frontend-routed?
Recovery codes, backup methods, and SMS/TOTP differentiation are all unverified.
9. Multi-tab synchronisation¶
There is no storage event listener for cross-tab logout sync. If a user logs out in tab A, tab B continues to operate with the (now-revoked, if backend revokes; or still-valid, since logout is client-only here) token until its next navigation triggers redirect.js.
grep -rEn "addEventListener\(['\"]storage" pages components store plugins returns no matches. Finding [WC-AUTH-7] (Severity: Low): Add a storage event listener that re-checks auth state on cookie/localStorage changes from other tabs.
10. CSRF¶
The auth strategy uses sameSite: 'none'; secure: true (nuxt.config.js:286-291). With SameSite=None, the browser will send cookies on cross-origin requests. CSRF protection therefore depends on either:
- The backend requiring a separate CSRF token in a header.
- The backend accepting only
Origin: <known frontend origin>requests.
The frontend does not appear to send a separate CSRF token. [VERIFY-AUTH-6]: does the backend validate Origin and/or require a CSRF token? If neither, a malicious cross-origin page could (in browsers without strict isolation) cause authenticated requests on the user's behalf.
Finding [WC-AUTH-8] (Severity: Medium–High): With SameSite=None cookies and unverified backend CSRF posture, the frontend's CSRF defense relies entirely on the backend's Origin/CSRF-token enforcement. Confirm backend behaviour.
11. Account scoping (privilege check)¶
The current accountId is read from cookies at request time (middleware/axios.js:13). When a user has multiple accounts and switches, the cookie is updated, and subsequent requests use the new value.
Risk: if the cookie is malformed or stale (e.g., user was removed from an account), the backend must re-validate per request. [VERIFY-AUTH-7]: does the backend validate accountId membership on every request?
12. Role-based gating (client-side)¶
Client-side role checks are scattered:
head.js(unwired) gates/my_library,/dashboard/roi,/dashboard/overviewonaccount_role !== 3and the payment-required redirect onroleType === 5 && !isInvited.helpers/helper.js → getPlanListfilters plan options based onnoIdand signup-date cohort.- Various components use bare
roleType === Ncomparisons.
The role values: roleType ∈ {1, 5, 11}; account_role === 3; isInvited ∈ {0, 1}; noId ∈ {0, 1}.
These are gates for UX, not security boundaries. The backend is the actual security boundary. (Same caveat as #3 above.)
13. Open questions¶
Tracked in ./verify-markers.md:
- [VERIFY-AUTH-1] Backend support for
httpOnlycookie option on auth response - [VERIFY-AUTH-2] Does the backend re-validate
roleTypeon privileged endpoints? - [VERIFY-AUTH-3] Backend acceptance of social-login placeholder password
- [VERIFY-AUTH-4] Facebook auth wiring — is
auth._token.facebookactually used? - [VERIFY-AUTH-5] MFA enforcement and methods
- [VERIFY-AUTH-6] CSRF posture (Origin check / CSRF token / neither)
- [VERIFY-AUTH-7] Backend validation of
accountIdmembership
14. Recommendations summary¶
| Priority | Action | Effort |
|---|---|---|
| Phase 0a | Audit and rotate any deployed AWS / token / API keys (see security.md) |
<1d |
| Phase 0 | Move session token to httpOnly cookie (requires backend coordination) | 1–2 weeks |
| Phase 0 | Add CSP header restricting script-src and frame-ancestors | 1 week |
| Phase 0 | Add Subresource Integrity to third-party CDN scripts | <1d |
| Phase 0 | Sanitise the 9 v-html use sites in components |
2–3 days |
| Phase 0 | Add storage event listener for multi-tab logout sync |
1d |
| Phase 1 | Consolidate auth header to one canonical format (drop token/raw Authorization in favour of Authorization: Bearer) |
1 week (touches many call sites) |
| Phase 1 | Add token-refresh path (response interceptor on 401) | 1 week |
| Phase 1 | Move client-side role gating to per-navigation backend fetch | 1 week |
| Phase 2 | MFA UX coverage (if not already complete) | TBD |