Skip to content

fix(portal): skip admin Bearer JWT on customer-portal routes - #3

Merged
rrader26 merged 1 commit into
mainfrom
fix/portal-skip-admin-bearer
May 23, 2026
Merged

fix(portal): skip admin Bearer JWT on customer-portal routes#3
rrader26 merged 1 commit into
mainfrom
fix/portal-skip-admin-bearer

Conversation

@rrader26

Copy link
Copy Markdown

Root cause

authLink in src/core/apolloClient/init.ts attaches Authorization: Bearer to every GraphQL request if there's an authToken in localStorage. This includes /customer-portal/* requests, which shouldn't be authenticating via the admin JWT at all (they use the customer-portal-token header).

If a user has ever logged into Lago admin in the same browser, the admin JWT (12h TTL) sits in localStorage forever. Once it expires, every customer-portal GraphQL request fails with expired_jwt_token — because GraphqlController validates the Bearer JWT before resolving the operation — even though the customer-portal session itself is fresh.

This was hidden until we wired the stale-tab recovery flow (PR #2). Now it manifests as an infinite loop: portal hits expired_jwt → bounces back to host app → host mints fresh portal URL → new portal page still sends the expired admin JWT from localStorage → expired_jwt → bounces again.

Fix

Only send Authorization: Bearer header when NOT on a /customer-portal/* route. The customer-portal-token header is still sent on portal routes; the admin Bearer is sent everywhere else as before.

Test plan

  • Log into Lago admin → wait 12h+ for JWT to expire → open customer portal URL → should load without expired_jwt error
  • Confirmed via the bug repro: in incognito (no localStorage admin JWT), portal works fine. In a browser with an expired admin JWT, portal fails with expired_jwt.
  • Admin app behavior unchanged (auth still attached on non-portal routes)

The customer portal authenticates via the customer-portal-token header,
not the admin Bearer JWT. But authLink was attaching the Bearer JWT to
every request if one existed in localStorage — including portal requests.

For users who have ever logged into Lago admin in the same browser, the
admin JWT (12h TTL) sits in localStorage. Once it expires, every
customer-portal GraphQL request returns expired_jwt_token (because
GraphqlController validates the Bearer JWT before resolving), even though
the customer-portal session itself is fresh and valid.

This broke the recently-shipped stale-tab-recovery flow into an infinite
loop: portal hits expired_jwt → bounces to host app → host mints fresh
portal URL → portal still has expired admin JWT in localStorage →
expired_jwt → bounces again.

Fix: only attach the Bearer header when NOT on a /customer-portal/* route.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@rrader26
rrader26 merged commit c5a8abd into main May 23, 2026
4 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant