fix(provider): read organizationId from access token after client initialization#98
fix(provider): read organizationId from access token after client initialization#98nicknisi wants to merge 1 commit into
Conversation
…tialization After createClient() resolves, the .then() callback previously spread initialState (which has organizationId: null) into the new state, thereby overwriting any organizationId that onRefresh had already set. Now the .then() callback reads JWT claims via getClaims() from the access token and includes organizationId, role, roles, permissions, and featureFlags in the setState call. Adds vitest + @testing-library/react and three tests proving: 1. organizationId is populated from access token claims 2. Auth metadata comes from JWT claims, not initialState defaults 3. onRefresh-set organizationId is not overwritten Closes #87
nicknisi
left a comment
There was a problem hiding this comment.
Code Review Findings
Warnings
- [Convention]
@types/react-dom@^19conflicts with@types/react@18.3.3in devDependencies — version mismatch may cause type-checking issues in development (package.json:33)
Info
- [Principle 10] Single commit bundles bug fix and test infrastructure setup (vitest, testing-library, jsdom). Ideally separate commits for fix vs test infra, but acceptable given focused scope.
Automated review by case/reviewer agent
Greptile SummaryThis PR fixes a state-overwrite bug where the
Confidence Score: 4/5The fix is correct and well-scoped; the main watch-out is a broad catch block that silently absorbs any error from getAccessToken(), not just unauthenticated sessions. The core logic change is sound — spreading ...prev instead of ...initialState and sourcing organizationId from JWT claims eliminates the overwrite. The broad catch {} is the only material concern: a network error or unexpected runtime exception during getAccessToken() would leave the app silently in a not logged in state with no diagnostic path. The try/catch block in src/provider.tsx (lines 100-122) deserves a second look to ensure only LoginRequiredError is swallowed. Important Files Changed
|
| } catch { | ||
| // No active session – just mark loading as done | ||
| setState((prev) => ({ ...prev, isLoading: false, user })); | ||
| } |
There was a problem hiding this comment.
The
catch block captures every exception thrown by await client.getAccessToken(), not only LoginRequiredError. A real error — network failure, unexpected runtime exception, token-decode crash — would be silently absorbed and the app would appear unauthenticated with no way to distinguish it from a normal "not logged in" state. Narrowing the guard makes the intent explicit and lets genuine errors surface.
| } catch { | |
| // No active session – just mark loading as done | |
| setState((prev) => ({ ...prev, isLoading: false, user })); | |
| } | |
| } catch (err) { | |
| if (!(err instanceof LoginRequiredError)) { | |
| throw err; | |
| } | |
| // No active session – just mark loading as done | |
| setState((prev) => ({ ...prev, isLoading: false, user })); | |
| } |
| try { | ||
| const accessToken = await client.getAccessToken(); | ||
| const { | ||
| org_id: organizationId = null, | ||
| role = null, | ||
| roles = null, | ||
| permissions = [], | ||
| feature_flags: featureFlags = [], | ||
| } = getClaims(accessToken); |
There was a problem hiding this comment.
JWT claims read without
iss/aud validation
getClaims(accessToken) base64-decodes the token payload to extract auth metadata but does not verify the signature or validate the iss and aud claims. A tampered or mis-issued token would be accepted and its claims written into React state. The same pattern already exists in handleRefresh; both call sites should either rely on the library performing those checks internally, or explicitly validate before trusting the claims.
Rule Used: JWTs should always be validated before use and the... (source)
Summary
After
createClient()resolves, the.then()callback was setting state with onlyisLoading: falseanduser, spreading frominitialStatewhich hasorganizationId: null. This overwrote anyorganizationId(and other auth metadata) that had been correctly set by theonRefresh/handleRefreshcallback moments earlier due to React state batching.The fix reads auth metadata (
organizationId,role,roles,permissions,featureFlags) directly from the access token's JWT claims in the.then()callback, using the existinggetClaims()helper. This ensures state is fully populated regardless of callback ordering. A try/catch handles unauthenticated sessions gracefully.Manual reproduction steps
AuthKitProviderwith anonRefreshcallback that logsresponse.organizationIduseAuth().organizationIdOn
main:organizationIdisnulleven thoughonRefreshreceived the correct value — the.then()callback overwrites itOn this branch:
organizationIdis correctly populated from the access token JWT claimsWhat was tested
Automated
3 vitest unit tests added and passing:
organizationIdis populated from JWT claims aftercreateClientresolvesinitialStatedefaultsonRefresh-setorganizationIdis not overwritten by the.then()callbackManual
npm linkuseAuth()fields are properly populated on the Account pagegetClaims()correctly extractsorg_idfrom JWT, provider source usesgetClaimsin.then()callback, old bug pattern removed, catch block handles unauthenticated sessionsVerification
Before
After
Issue
Closes #87
Follow-ups
@types/react-dom@^19in devDependencies conflicts with@types/react@18.3.3— version mismatch may cause type-checking issues in development (pre-existing, not introduced by this fix)