feat(auth)!: stop persisting ID token; persist decoded profile instead#248
feat(auth)!: stop persisting ID token; persist decoded profile instead#248bmanquen wants to merge 1 commit into
Conversation
- Decode ID token once at sign-in to derive userInfo, then discard it - Persist the decoded profile (Zod-validated on read) so it survives reloads - Clear stored profile on sign-out and on unrecoverable session expiry - core: drop idToken from saveAuthData; add saveUserInfo/storedUserInfo and getStoredUserInfo() - hooks: remove AuthenticationState.idToken; provider rehydrates from stored profile - ui: update authenticated-user test helper BREAKING CHANGE: the ID token is no longer exposed or persisted. - Removed AuthenticationState.idToken (use userInfo for profile data) - Removed the YouVersionPlatformConfiguration.idToken getter - saveAuthData(accessToken, refreshToken, expiryDate) no longer accepts an idToken arg
🦋 Changeset detectedLatest commit: abcf0b6 The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
| export const YouVersionUserInfoJSONSchema = z.object({ | ||
| name: z.string().optional(), | ||
| id: z.string().optional(), | ||
| avatar_url: z.string().optional(), | ||
| email: z.string().optional(), | ||
| }); |
There was a problem hiding this comment.
All schema fields optional — empty object passes validation
Because every field is optional, {} is a fully valid YouVersionUserInfoJSON. If the userInfo key in localStorage is tampered with and set to {}, storedUserInfo will return {}, getStoredUserInfo() will return a non-null YouVersionUserInfo({}), and the provider will call setUserInfo(non-null) — making isAuthenticated true with an empty profile. This only fires alongside a valid refresh token, so the practical risk is low, but adding z.string().min(1) for the id field (the canonical unique identifier) would let the schema reject an empty stub and give you a meaningful invariant to lean on.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/core/src/schemas/user-info.ts
Line: 11-16
Comment:
**All schema fields optional — empty object passes validation**
Because every field is `optional`, `{}` is a fully valid `YouVersionUserInfoJSON`. If the `userInfo` key in localStorage is tampered with and set to `{}`, `storedUserInfo` will return `{}`, `getStoredUserInfo()` will return a non-null `YouVersionUserInfo({})`, and the provider will call `setUserInfo(non-null)` — making `isAuthenticated` true with an empty profile. This only fires alongside a valid refresh token, so the practical risk is low, but adding `z.string().min(1)` for the `id` field (the canonical unique identifier) would let the schema reject an empty stub and give you a meaningful invariant to lean on.
How can I resolve this? If you propose a fix, please make it concise.
Decode ID token once at sign-in to derive userInfo, then discard it
BREAKING CHANGE: the ID token is no longer exposed or persisted.
Greptile Summary
This PR removes the ID token from the persistence layer: it is decoded once at sign-in to produce a
YouVersionUserInfoJSONprofile, which is then saved tolocalStorage(Zod-validated on read-back viastoredUserInfo), while the token itself is discarded. The provider anduseYVAuthare updated to rehydrate user state from the stored profile rather than from an idToken getter.handleAuthCallbacknow callssaveUserInfoaftersaveAuthData;extractSignInResultstill populatesidTokenon the in-memorySignInWithYouVersionResult(available toprocessCallbackcallers), which is an incomplete removal of the exposure path described in the changeset.YouVersionUserInfoJSONSchemahas all four fields asoptional, so an empty{}object passes validation and would produce a non-nullYouVersionUserInfo— consider requiring at leastidto be a non-empty string to prevent a tampered entry from appearing as a valid session.handleAuthCallbackcatch block removes PKCE/OAuth temporary keys but not the auth tokens or user profile saved earlier in the same try block; a failure aftersaveUserInfoleaves the user partially authenticated on the next page load.Confidence Score: 4/5
The change is safe to merge; the core rehydration flow (callback → saveUserInfo → storedUserInfo → setUserInfo) is correct, test coverage is solid, and sign-out/expiry cleanup is handled consistently.
The three findings are all non-blocking: the idToken is still reachable through the in-memory result object, the Zod schema is overly permissive on the profile shape, and the catch block leaves stale auth data in a failure scenario that requires an extremely unlikely runtime error. None of these affect the happy path or common error paths.
packages/core/src/Users.ts (extractSignInResult idToken field, catch block cleanup) and packages/core/src/schemas/user-info.ts (all-optional schema) are the areas worth a second look before merging.
Important Files Changed
Comments Outside Diff (2)
packages/core/src/Users.ts, line 192-201 (link)extractSignInResultstill attachesidTokento the result — partial removal of the exposure pathhandleAuthCallbackno longer persists the ID token, but it still returns aSignInWithYouVersionResultwhoseidTokenfield is set (viaextractSignInResult).processCallback()inuseYVAuthreturns this object directly, so any consumer callingresult.idTokencan still read the raw token from memory. If the intent is to fully remove the exposure vector,idTokenshould be omitted from the result constructed insideextractSignInResult.Prompt To Fix With AI
packages/core/src/Users.ts, line 148-154 (link)saveAuthData/saveUserInfoon errorIf an exception is raised after
saveAuthDataandsaveUserInfohave already written to localStorage (e.g.window.history.replaceStatethrowing in a restrictive browser environment), the catch block removes the PKCE/OAuth temporary keys but leaves the persisted tokens and user profile intact. On the next page load the user would be treated as authenticated, while the provider also exposes the error that was thrown. AddingYouVersionPlatformConfiguration.clearAuthTokens()at the top of the catch block would make the failure path consistent with sign-out semantics.Prompt To Fix With AI
Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "feat(auth)!: stop persisting ID token; p..." | Re-trigger Greptile