fix: stop embedding OAuth token in Inngest audit event payload#212
Closed
anshul23102 wants to merge 2 commits into
Closed
fix: stop embedding OAuth token in Inngest audit event payload#212anshul23102 wants to merge 2 commits into
anshul23102 wants to merge 2 commits into
Conversation
Fixes Coder-s-OG-s#205 The previous implementation enforced the 3-claim cap with two independent database round-trips: a COUNT query, then a separate UPDATE. Between those two steps, a second concurrent request for the same user could also read count < 3, pass the guard, and commit its own UPDATE. Sending multiple claim requests in parallel (trivially via Promise.all) reproduced the bug reliably -- the user ended up with more than 3 active claims. Fix: replaced the two-step sequence with a call to a new database function, claim_recommendation_atomic, added in migration 0012. The function merges the count check and the write into a single UPDATE statement. The subquery that evaluates the claim count and the row that gets written are handled atomically by the database engine, so concurrent calls are serialised at the row level and only one can succeed when the count is at the limit. Migration 0012 (supabase/migrations/0012_claim_recommendation_atomic.sql): - Defines claim_recommendation_atomic(p_rec_id bigint, p_user_id uuid) returning TABLE(id bigint). - The UPDATE only proceeds if the caller's current claimed count is strictly less than 3 AND the target rec is still open. Zero rows returned covers both rejection cases (limit reached or rec not open). - SECURITY DEFINER with SET search_path = public prevents search-path injection. - GRANT EXECUTE given to both authenticated and service_role so server actions and background functions can call it without privilege changes. recommendations.ts: - Removed the COUNT query and the subsequent maybeSingle UPDATE. - Added service.rpc('claim_recommendation_atomic', ...) in their place. - Interprets zero returned rows as a unified claim_limit_or_not_open error; the UI already re-fetches state on any error so the distinction between the two rejection reasons is not needed client-side. - data.id reference updated to claimedId extracted from the RPC result.
bootstrapProfile was passing the user's live GitHub OAuth provider token directly inside inngest.send() for the audit/run event. Inngest persists event payloads to third-party cloud storage, so this exposed long-lived credentials outside the application's trust boundary. The audit-run function already supports a three-tier auth fallback: 1. installationId from the event (minted into a short-lived install token at execution time) 2. Live DB lookup of github_installations at execution time 3. accessToken from the event (legacy path, retained for backward compat) bootstrapProfile now looks up the user's active installation at event-send time and passes installationId instead of accessToken. If no installation exists yet (common at first login), installationId is omitted and the function performs the DB lookup at execution time, by which point the install webhook will typically have landed. Also removes the getSession() call that was only needed to retrieve the provider token, and fixes a TypeScript strict-null error in claimRecommendation (rows[0] after an explicit length guard). Closes Coder-s-OG-s#204
Contributor
|
@anshul23102 is attempting to deploy a commit to the codersogs-3057's projects Team on Vercel. A member of the Team first needs to authorize it. |
Author
|
Ready for review. The fix removes the GitHub OAuth token from the Inngest event payload entirely and replaces it with an installation ID lookup. TypeScript passes clean with |
Author
|
Closing in favour of #222, which supersedes this with a complete unit test suite (3 tests covering the installationId path, the no-install skip path, and the already-audited path) and a cleaner implementation that avoids the unconditional send. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #204
Problem
bootstrapProfilewas passing the user's live GitHub OAuth provider token asaccessTokeninside theinngest.send()payload for theaudit/runevent. Inngest persists event payloads to its own cloud storage, so a long-lived OAuth credential was leaving the application's trust boundary every time a new user completed sign-in.Root cause
The OAuth token has a lifespan measured in hours or days. Once it lands in Inngest event storage it persists indefinitely and cannot be rotated from the application side.
Fix
The
audit-runfunction already supports two safer auth paths:installationIdis provided, the function queriesgithub_installationsat execution time and mints an install token from whatever it finds.bootstrapProfilenow:github_installationsfor the user's active install at event-send time and passesinstallationIdif found.installationIdis omitted. The function performs its own DB lookup at execution time, by which point the webhook will typically have landed.sb.auth.getSession()call entirely since the token is no longer needed.{ skipped: true, reason: 'no_auth_source' }in the rare case where neither path finds auth.The
accessTokenfield is retained inAuditEventand in the function's fallback path to handle any events already in the queue, but is marked@deprecatedso no new code reaches for it.Files changed
src/app/actions/profile.tssrc/inngest/functions/audit-run.tsaccessTokenas deprecatedsrc/app/actions/recommendations.tsrows[0]after explicit length guard (caught bytsc --noEmit)Testing
tsc --noEmitpasses with zero errors.