Skip to content

perf(auth): switch to claim-based user lookups#1498

Merged
riderx merged 3 commits into
mainfrom
riderx/getuser-to-getclaims
Jan 25, 2026
Merged

perf(auth): switch to claim-based user lookups#1498
riderx merged 3 commits into
mainfrom
riderx/getuser-to-getclaims

Conversation

@riderx
Copy link
Copy Markdown
Member

@riderx riderx commented Jan 25, 2026

Summary (AI generated)

  • Replace getUser with getClaims in frontend auth checks and API key flows.
  • Use auth context claims in backend endpoints/middleware for JWT user ids.

Test plan (AI generated)

  • Not run (not requested).

Screenshots (AI generated)

  • Not applicable.

Checklist (AI generated)

  • My code follows the code style of this project and passes

/Users/martindonadieu/conductor/workspaces/capgo/sacramento-v1/supabase/functions/_backend/utils/hono_middleware.ts
9:49 error 'supabaseClient' is defined but never used unused-imports/no-unused-imports

✖ 1 problem (1 error, 0 warnings)
1 error and 0 warnings potentially fixable with the --fix option..

  • My change requires a change to the documentation.
  • I have updated the documentation
    accordingly.
  • My change has adequate E2E test coverage.
  • I have tested my code manually, and I have provided steps how to reproduce
    my tests

Generated with AI

Summary by CodeRabbit

  • Documentation
    • Updated guidance on authentication best practices and optimization recommendations for system performance
  • Chores
    • Improved authentication verification performance across all frontend and backend application components
    • Streamlined user validation mechanisms throughout the system for faster and more efficient processing
    • Enhanced overall application performance through authentication improvements

✏️ Tip: You can customize this high-level summary in your review settings.

Copilot AI review requested due to automatic review settings January 25, 2026 04:02
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 25, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Migrates authentication from supabase.auth.getUser() to claims-based authentication using supabase.auth.getClaims() and session contexts. User IDs are derived from JWT claims (claims.sub) instead of user objects. Backend middleware now decodes JWT tokens locally without network calls. Affects frontend Vue components, backend Edge Functions, and utility modules while maintaining existing error handling.

Changes

Cohort / File(s) Summary
Documentation
AGENTS.md
Added guidance box recommending claim-based auth lookups for performance, advising getClaims() usage over getUser() unless full user record is needed.
Frontend Dashboard Components
src/components/dashboard/StepsApp.vue, StepsBuild.vue, StepsBundle.vue
Replaced getUser() with getClaims() to derive userId from claims.sub. Updated validation and API key upsert payloads to use userId instead of user.id.
Frontend Auth Module
src/modules/auth.ts
Refactored to use getClaims() and getSession() instead of direct auth.user access. Introduced sessionUser and hasAuth derived from claims/session data. Updated auth gating logic, TOTP/admin checks, and control flow branches.
Frontend Pages
src/pages/ApiKeys.vue, delete_account.vue, invitation.vue, login.vue, settings/account/index.vue
Replaced getUser() with getClaims() across all pages. Derived userId from claims.sub and updated conditional checks, database queries, and authorization logic.
Backend Edge Functions
supabase/functions/_backend/private/download_link.ts, invite_new_user_to_org.ts, stripe_checkout.ts, stripe_portal.ts
Switched from auth.getUser() to reading userId from auth context (c.get('auth').userId). Updated validation, authorization checks, and logging to use context-based userId.
Backend JWT & Middleware Utilities
supabase/functions/_backend/utils/hono.ts, hono_middleware.ts
Added JWTClaims interface and getClaimsFromJWT() utility for local JWT decoding without network calls. Updated middleware to validate claims.sub and set auth.userId from claims instead of calling getUser().

Sequence Diagram(s)

sequenceDiagram
    participant Client as Frontend Component
    participant Claims as Supabase Auth
    participant Service as Component Logic
    
    Client->>Claims: getClaims()
    Claims-->>Client: claimsData (claims.sub)
    Client->>Service: Extract userId from claims.sub
    Service->>Service: Validate userId presence
    Service-->>Client: Authorization state (hasAuth)
    Client->>Service: Use userId for API operations
Loading
sequenceDiagram
    participant Request as HTTP Request
    participant Middleware as middlewareAuth
    participant JWT as getClaimsFromJWT()
    participant Handler as Edge Function Handler
    participant DB as Database
    
    Request->>Middleware: Bearer token
    Middleware->>JWT: Decode JWT (local)
    JWT-->>Middleware: JWTClaims {sub, exp, ...}
    Middleware->>Middleware: Validate claims.sub
    Middleware->>Middleware: Set auth.userId from claims.sub
    Middleware->>Handler: auth context {userId}
    Handler->>DB: Query with userId
    DB-->>Handler: Results
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

enhancement

Poem

🐰 Claims now flow, no network call,
JWT decoded, swift and small,
From sub to userId, bright and new,
Auth flows faster—claim-based too!

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The description is largely incomplete. It lacks meaningful content for Summary, Test plan, and Screenshots sections, and the checklist contains generated AI content with an unresolved linting error rather than author-verified completions. Provide a proper manual summary explaining the performance rationale, detail the test plan with specific steps to verify claims-based auth works correctly, and resolve the ESLint error before marking checklist items complete.
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'perf(auth): switch to claim-based user lookups' accurately and specifically describes the main change—migrating from getUser/network lookups to claim-based authentication across frontend and backend.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3779a8a66d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +163 to 167
// Decode JWT claims without network call (much faster than getUser())
const claims = getClaimsFromJWT(authorization)
if (!claims || !claims.sub) {
cloudlog({ requestId: c.get('requestId'), message: 'Invalid JWT claims' })
throw simpleError('invalid_jwt', 'Invalid JWT')
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Validate JWT signature before trusting claims

The new middlewareAuth now derives userId solely from getClaimsFromJWT, which only base64-decodes the payload and checks exp without verifying the JWT signature. Because checkPermission later trusts auth.userId directly in rbac_check_permission_direct (no Supabase auth validation), an attacker can forge a Bearer token with any sub and bypass RBAC-protected endpoints. Previously supabase.auth.getUser() verified the token. This creates an authorization bypass whenever a request includes an untrusted JWT string. Please restore signature verification (e.g., getUser() or explicit JWT verification) before setting auth.userId.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a performance optimization by replacing getUser() API calls with local JWT claim decoding across the backend and frontend.

Changes:

  • Added getClaimsFromJWT() function to decode JWT claims locally without network calls
  • Updated backend middleware (middlewareAuth, foundJWT) to use claim-based authentication
  • Updated backend endpoints (stripe_portal, stripe_checkout, invite_new_user_to_org, download_link) to use auth context instead of getUser()
  • Updated frontend pages and components to use getClaims() instead of getUser()

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated no comments.

Show a summary per file
File Description
supabase/functions/_backend/utils/hono.ts Added getClaimsFromJWT() function and JWTClaims interface; updated middlewareAuth to decode claims locally
supabase/functions/_backend/utils/hono_middleware.ts Updated foundJWT() to use getClaimsFromJWT() instead of getUser()
supabase/functions/_backend/private/stripe_portal.ts Changed to use auth context (c.get('auth')) instead of getUser()
supabase/functions/_backend/private/stripe_checkout.ts Changed to use auth context instead of getUser()
supabase/functions/_backend/private/invite_new_user_to_org.ts Changed to use auth context instead of getUser()
supabase/functions/_backend/private/download_link.ts Changed to use auth context instead of getUser()
src/pages/settings/account/index.vue Changed to use getClaims() instead of getUser()
src/pages/login.vue Changed to use getClaims() and getSession() instead of getUser()
src/pages/invitation.vue Changed to use getClaims() to check authentication status
src/pages/delete_account.vue Changed to use getClaims() instead of getUser()
src/pages/ApiKeys.vue Changed to use getClaims() instead of getUser() for API key operations
src/modules/auth.ts Updated guard function to use getClaims() and getSession() instead of getUser()
src/components/dashboard/StepsBundle.vue Changed to use getClaims() for API key creation
src/components/dashboard/StepsBuild.vue Changed to use getClaims() for API key creation
src/components/dashboard/StepsApp.vue Changed to use getClaims() for API key creation

@riderx riderx force-pushed the riderx/getuser-to-getclaims branch from 3115087 to b62a9c3 Compare January 25, 2026 04:28
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
supabase/functions/_backend/utils/hono.ts (1)

163-176: Unverified JWT claims trusted for authentication context.

The middlewareAuth middleware now derives userId from getClaimsFromJWT, which doesn't verify the JWT signature. Previously, supabase.auth.getUser() verified the token server-side. This change means any endpoint using middlewareAuth will accept forged tokens.

Endpoints that only use c.get('auth').userId for authorization decisions (without subsequent RLS-protected Supabase operations) are now vulnerable to identity spoofing.

supabase/functions/_backend/utils/hono_middleware.ts (1)

460-481: Same JWT verification gap applies to middlewareV2.

The foundJWT function now uses the unverified getClaimsFromJWT to derive userId. This affects all endpoints protected by middlewareV2(rights) that rely on JWT authentication. The same authorization bypass risk identified in hono.ts applies here.

Since middlewareV2 is likely used across many endpoints (based on learnings indicating it's the standard middleware for external API keys and JWT auth), this significantly expands the attack surface.

🤖 Fix all issues with AI agents
In `@src/modules/auth.ts`:
- Around line 157-159: In the route-guard inside src/modules/auth.ts (the auth
middleware/guard that uses from.path and to.path), fix the path comparison that
currently reads from.path !== 'login' to include the leading slash: use
from.path !== '/login' so it matches Vue Router's path format and avoids
redirect loops when already on the login page; update the condition in the same
guard where hasAuth is checked to use '/login' for consistency with other
comparisons like to.path !== '/accountDisabled' and
to.path.startsWith('/admin').

In `@supabase/functions/_backend/private/invite_new_user_to_org.ts`:
- Around line 152-155: The guard that checks authContext in
invite_new_user_to_org.ts returns a 500 on missing auth (const authContext =
c.get('auth'); if (!authContext?.userId) ...); update the response to return
status: 401 instead of 500 to reflect an authentication failure—modify the block
that returns { message: 'Failed to get current user', error: 'Not authorized',
status: 500 } so it returns status: 401.
♻️ Duplicate comments (1)
supabase/functions/_backend/utils/hono.ts (1)

30-62: JWT signature not verified - authorization bypass risk.

The getClaimsFromJWT function only base64-decodes the JWT payload and checks expiration. It does not verify the cryptographic signature. The comment on line 33 stating "The JWT signature is still verified by Supabase when used with RLS policies" is incorrect for this context—RLS policies verify the JWT when Supabase client operations occur, but middlewareAuth sets auth.userId directly from the unverified claims.sub before any RLS-protected operation.

An attacker can craft a Bearer token with an arbitrary sub claim and bypass RBAC checks in endpoints that rely on c.get('auth').userId without making subsequent RLS-protected Supabase calls.

Consider one of:

  1. Use supabase.auth.getUser() for JWT validation (original behavior)
  2. Implement proper JWT signature verification using the Supabase JWT secret
  3. Use Supabase's getClaims() server-side if available
🧹 Nitpick comments (1)
src/modules/auth.ts (1)

114-116: Minor redundancy: sessionUser check is implied by hasAuth.

Since hasAuth is defined as !!claimsData?.claims?.sub && !!sessionUser (line 90), the explicit sessionUser check on line 114 is redundant. This doesn't affect correctness but could be simplified.

♻️ Optional simplification
-  if (hasAuth && sessionUser && !main.auth) {
+  if (hasAuth && !main.auth) {

Comment thread src/modules/auth.ts
Comment on lines +157 to 159
else if (from.path !== 'login' && !hasAuth) {
main.auth = undefined
next(`/login?to=${to.path}`)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the auth.ts file to understand the context
rg -n "from\.path" src/modules/auth.ts -B 3 -A 3

Repository: Cap-go/capgo

Length of output: 226


🏁 Script executed:

# Also check other usages of from.path in the codebase
rg -n "from\.path" --type=ts --type=vue -C 2

Repository: Cap-go/capgo

Length of output: 83


🏁 Script executed:

# Check the entire auth.ts file to understand its purpose and structure
wc -l src/modules/auth.ts

Repository: Cap-go/capgo

Length of output: 79


🏁 Script executed:

cat -n src/modules/auth.ts

Repository: Cap-go/capgo

Length of output: 7577


🏁 Script executed:

# Search for path comparisons more broadly
rg "\.path\s*[!=]" src/modules/auth.ts -B 2 -A 2

Repository: Cap-go/capgo

Length of output: 440


🏁 Script executed:

# Search for path string patterns to understand what format is used
rg "'/(login|auth)" src/modules/ -n

Repository: Cap-go/capgo

Length of output: 38


🏁 Script executed:

# Verify vue-router documentation about path format by checking types or usage patterns
rg "path.*==|path.*!=|path.*startsWith" src/modules/ -n | head -20

Repository: Cap-go/capgo

Length of output: 257


🏁 Script executed:

# Check if there are any route definitions that show how paths are used
fd -e ts -e js "router|route" src/ --exec grep -l "path:" {} \;

Repository: Cap-go/capgo

Length of output: 38


Fix path comparison on line 157: add leading slash to match vue-router path format

The condition from.path !== 'login' is missing the leading slash. Vue Router paths always include a leading slash (e.g., /login), so this comparison will always evaluate to true even when the user is already on the login page. This could cause redirect loops.

Change line 157 to: else if (from.path !== '/login' && !hasAuth) {

This aligns with path comparisons on lines 164 (to.path !== '/accountDisabled') and 183 (to.path.startsWith('/admin')).

🤖 Prompt for AI Agents
In `@src/modules/auth.ts` around lines 157 - 159, In the route-guard inside
src/modules/auth.ts (the auth middleware/guard that uses from.path and to.path),
fix the path comparison that currently reads from.path !== 'login' to include
the leading slash: use from.path !== '/login' so it matches Vue Router's path
format and avoids redirect loops when already on the login page; update the
condition in the same guard where hasAuth is checked to use '/login' for
consistency with other comparisons like to.path !== '/accountDisabled' and
to.path.startsWith('/admin').

Comment on lines +152 to 155
const authContext = c.get('auth')
if (!authContext?.userId) {
return { message: 'Failed to get current user', error: 'Not authorized', status: 500 }
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Error status code should be 401, not 500.

When authContext?.userId is missing, returning status 500 ("Internal Server Error") is misleading. This is an authentication failure, not a server error. Use 401 for consistency with other auth failures in the codebase.

Suggested fix
   const authContext = c.get('auth')
   if (!authContext?.userId) {
-    return { message: 'Failed to get current user', error: 'Not authorized', status: 500 }
+    return { message: 'Failed to get current user', error: 'Not authorized', status: 401 }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const authContext = c.get('auth')
if (!authContext?.userId) {
return { message: 'Failed to get current user', error: 'Not authorized', status: 500 }
}
const authContext = c.get('auth')
if (!authContext?.userId) {
return { message: 'Failed to get current user', error: 'Not authorized', status: 401 }
}
🤖 Prompt for AI Agents
In `@supabase/functions/_backend/private/invite_new_user_to_org.ts` around lines
152 - 155, The guard that checks authContext in invite_new_user_to_org.ts
returns a 500 on missing auth (const authContext = c.get('auth'); if
(!authContext?.userId) ...); update the response to return status: 401 instead
of 500 to reflect an authentication failure—modify the block that returns {
message: 'Failed to get current user', error: 'Not authorized', status: 500 } so
it returns status: 401.

@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
11.9% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@riderx riderx merged commit ebfe53a into main Jan 25, 2026
10 of 11 checks passed
@riderx riderx deleted the riderx/getuser-to-getclaims branch January 25, 2026 05:32
jokabuyasina pushed a commit to jokabuyasina/capgo that referenced this pull request Feb 7, 2026
* perf(auth): use claims for user context

* docs: note claim-based auth guidance

* fix(backend): drop unused supabase import
jokabuyasina pushed a commit to jokabuyasina/capgo that referenced this pull request Feb 7, 2026
* perf(auth): use claims for user context

* docs: note claim-based auth guidance

* fix(backend): drop unused supabase import
jokabuyasina pushed a commit to jokabuyasina/capgo that referenced this pull request Feb 7, 2026
* perf(auth): use claims for user context

* docs: note claim-based auth guidance

* fix(backend): drop unused supabase import
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.

2 participants