Skip to content

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.

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
LinkedIn linkedInLogin action /linkedInRegister
Twitter 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/auth Facebook strategy is not defined in nuxt.config.js, but plugins/auth.js reads auth._token.facebook cookie. [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:

app.$auth.logout()
app.$cookies.removeAll()
localStorage.clear()
redirect('/#login')

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:

  1. The backend requiring a separate CSRF token in a header.
  2. 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/overview on account_role !== 3 and the payment-required redirect on roleType === 5 && !isInvited.
  • helpers/helper.js → getPlanList filters plan options based on noId and signup-date cohort.
  • Various components use bare roleType === N comparisons.

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 httpOnly cookie option on auth response
  • [VERIFY-AUTH-2] Does the backend re-validate roleType on privileged endpoints?
  • [VERIFY-AUTH-3] Backend acceptance of social-login placeholder password
  • [VERIFY-AUTH-4] Facebook auth wiring — is auth._token.facebook actually used?
  • [VERIFY-AUTH-5] MFA enforcement and methods
  • [VERIFY-AUTH-6] CSRF posture (Origin check / CSRF token / neither)
  • [VERIFY-AUTH-7] Backend validation of accountId membership

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