Skip to content

Conversation

@RichardBray
Copy link
Member

@RichardBray RichardBray commented Jan 18, 2026

Summary

Fixes 401 Unauthorized error when accessing /api/me after login in the React Native app.

Problem

React Native's fetch doesn't automatically handle cookies like browsers do. When users signed in:

  • Server returned Set-Cookie: __Secure-better-auth.session_token=...
  • React Native didn't store or send this cookie automatically
  • Subsequent requests to protected endpoints (like /api/me) failed with 401

Solution

Implemented manual session token management:

  • Installed expo-secure-store for secure token storage
  • Created session.ts utility for token lifecycle (save/get/clear/extract)
  • Updated api.ts to:
    • Extract and save session token from Set-Cookie header on sign-in/sign-up
    • Include session token as Cookie header in all subsequent requests
    • Clear session token on sign-out

Technical Details

  • Session tokens are extracted from __Secure-better-auth.session_token cookie
  • Tokens are stored securely using Expo Secure Store
  • All authenticated requests include: Cookie: __Secure-better-auth.session_token=<token>
  • Tokens are automatically cleared on sign-out

Testing

Test the login flow:

  1. Sign in with user@example.com / secret123
  2. Verify /api/me returns 200 with user data (previously 401)
  3. Verify sign-out clears token and subsequent requests fail

Summary by CodeRabbit

  • New Features
    • Encrypted session token storage for mobile, persisting tokens securely on device.
    • Automatic session handling: tokens are saved on sign-in/sign-up and cleared on sign-out.
    • Web platform support enabled (Expo web) for running the app in browsers.
    • Standardized app origin for auth and trusted origins added for local/web auth flows.

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

…ct Native

- Install expo-secure-store for secure token storage
- Create session.ts utility for token management (save/get/clear/extract)
- Update api.ts to handle session tokens via Cookie header
- Extract and save session token on sign-in/sign-up
- Include session token in all subsequent requests
- Clear session token on sign-out

Fixes 401 Unauthorized error when accessing /api/me after login
@coderabbitai
Copy link

coderabbitai bot commented Jan 18, 2026

Warning

Rate limit exceeded

@RichardBray has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 37 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 968024f and 82b2e15.

📒 Files selected for processing (4)
  • .gitignore
  • api/src/lib/auth.ts
  • mobile/src/api.ts
  • mobile/src/session.ts
📝 Walkthrough

Walkthrough

Adds persistent session token handling and secure storage for the mobile app, attaches tokens to API requests (via Cookie and Origin headers), persists/clears tokens on auth responses, adds web-related dependencies, and whitelists local origins in the API auth config.

Changes

Cohort / File(s) Summary
Session token module
mobile/src/session.ts
New file: implements SESSION_TOKEN_KEY, saveSessionToken(), getSessionToken(), clearSessionToken(), and extractSessionToken(response) to persist and parse session tokens from Set-Cookie.
API integration
mobile/src/api.ts
Reads token via getSessionToken() and, if present, adds Cookie: __Secure-better-auth.session_token=...; sets Origin to APP_ORIGIN for /api/auth/ calls; saves token on sign-in/sign-up using saveSessionToken() and clears on sign-out using clearSessionToken(). Exports APP_ORIGIN.
Dependencies / platform support
mobile/package.json
Adds expo-secure-store, react-dom, and react-native-web to dependencies to support secure storage and Expo web targets.
Auth config (API)
api/src/lib/auth.ts
Adds trustedOrigins entries to the Better Auth configuration to allow local development origins.

Sequence Diagram

sequenceDiagram
    participant App as Mobile App
    participant API as API Server
    participant Session as Session Module
    participant Store as SecureStore

    App->>Session: getSessionToken()
    Session->>Store: read(SESSION_TOKEN_KEY)
    Store-->>Session: token|null
    Session-->>App: token|null

    alt token present
        App->>API: fetch(request + Cookie: __Secure-better-auth.session_token=token, credentials: include)
    else no token
        App->>API: fetch(request, credentials: include)
    end

    API-->>App: response (may include Set-Cookie)

    alt response is sign-in or sign-up with Set-Cookie
        App->>Session: extractSessionToken(response)
        Session->>Store: save(SESSION_TOKEN_KEY)
    else response is sign-out success
        App->>Session: clearSessionToken()
        Session->>Store: delete(SESSION_TOKEN_KEY)
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I hide the little cookie safe in leafy store,
I fetch, I tuck, I whisper keys behind the door.
Sign-in scatters ribbons, sign-out lifts the seam,
I hop, I save, I clear — a tiny session dream.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: implementing manual session token management for React Native by adding secure token storage and cookie handling.

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


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

Copy link

@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: 1

🤖 Fix all issues with AI agents
In `@mobile/src/session.ts`:
- Around line 24-33: The current parsing splits setCookieHeader on ', ' which
breaks when cookie attributes (like Expires) contain commas; instead, search the
entire setCookieHeader string for the session cookie using a single regex match
rather than splitting. Update the logic in mobile/src/session.ts to call
cookieMatch =
setCookieHeader.match(/__Secure-better-auth\.session_token=([^;]+)/) (or
equivalent) on the full setCookieHeader and then
decodeURIComponent(cookieMatch[1]) if present, replacing the current loop that
splits on ', ' and uses tokenMatch.
🧹 Nitpick comments (1)
mobile/src/api.ts (1)

159-172: Redundant check and potential silent failures.

Two observations:

  1. Redundant response.ok check (Line 159): This condition is always true here since !response.ok throws at line 142-148.

  2. Silent failures on token operations: If saveSessionToken or clearSessionToken throws (e.g., SecureStore failure), the error is swallowed and the user won't know authentication state is inconsistent.

Suggested improvement
-      if (response.ok) {
-        if (path.includes('/sign-in/') || path.includes('/sign-up/')) {
-          const sessionToken = extractSessionToken(response);
-          if (sessionToken) {
-            await saveSessionToken(sessionToken);
-            log('Session token saved');
+      try {
+        if (path.includes('/sign-in/') || path.includes('/sign-up/')) {
+          const sessionToken = extractSessionToken(response);
+          if (sessionToken) {
+            await saveSessionToken(sessionToken);
+            log('Session token saved');
+          }
           }
-        }
 
-        if (path.includes('/sign-out')) {
-          await clearSessionToken();
-          log('Session token cleared');
+        if (path.includes('/sign-out')) {
+          await clearSessionToken();
+          log('Session token cleared');
+        }
+      } catch (tokenError) {
+        error('Failed to manage session token', tokenError);
+        // Consider whether to rethrow based on criticality
       }
-      }

Better Auth's sign-out endpoint requires Content-Type header even for requests without body. This fixes 415 error during sign-out.
Copy link

@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: 0

Caution

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

⚠️ Outside diff range comments (1)
mobile/src/api.ts (1)

151-173: Token save/clear is skipped on empty responses (e.g., sign‑out).
Because the early return happens before token lifecycle handling, a 204/empty sign‑out response won’t clear the token. Move the save/clear logic above the empty‑body return.

🐛 Proposed fix
     if (!response.ok) {
       error(`API request failed: ${method} ${fullUrl}`, text, {
         status: response.status,
         statusText: response.statusText,
       });
       throw new Error(text || `Request failed (${response.status})`);
     }

+    if (path.includes('/sign-in/') || path.includes('/sign-up/')) {
+      const sessionToken = extractSessionToken(response);
+      if (sessionToken) {
+        await saveSessionToken(sessionToken);
+        log('Session token saved');
+      }
+    }
+
+    if (path.includes('/sign-out')) {
+      await clearSessionToken();
+      log('Session token cleared');
+    }
+
     if (!text) {
       log(`API request successful: ${method} ${fullUrl} (empty response)`);
       return {};
     }

     try {
       const json = JSON.parse(text);
       log(`API request successful: ${method} ${fullUrl}`, json);
-
-      if (response.ok) {
-        if (path.includes('/sign-in/') || path.includes('/sign-up/')) {
-          const sessionToken = extractSessionToken(response);
-          if (sessionToken) {
-            await saveSessionToken(sessionToken);
-            log('Session token saved');
-          }
-        }
-
-        if (path.includes('/sign-out')) {
-          await clearSessionToken();
-          log('Session token cleared');
-        }
-      }
-
       return json;
     } catch (parseError) {
       error('Failed to parse JSON response', parseError, { text });
       return text;
     }
🧹 Nitpick comments (1)
mobile/src/api.ts (1)

125-136: Preserve any existing Cookie header when injecting the session token.
Right now you overwrite any caller-provided cookies. Safer to append the session token instead.

♻️ Proposed refactor
-    const headers = new Headers(init?.headers);
-    
-    if (token) {
-      headers.set('Cookie', `__Secure-better-auth.session_token=${token}`);
-    }
+    const headers = new Headers(init?.headers);
+
+    if (token) {
+      const existingCookie = headers.get('Cookie');
+      const sessionCookie = `__Secure-better-auth.session_token=${token}`;
+      headers.set('Cookie', existingCookie ? `${existingCookie}; ${sessionCookie}` : sessionCookie);
+    }

Richard Oliver Bray added 3 commits January 18, 2026 21:08
- Add trusted origins for mobile app (localhost:8081) and API (localhost:8787)
- Add Origin header to all /api/auth/* requests for CSRF protection
- Include empty JSON body ({}) in sign-out request
- Fixes INVALID_ORIGIN and UNSUPPORTED_MEDIA_TYPE errors during sign-out
The Origin header should be the mobile app's URL (localhost:8081) not the API URL (localhost:8787). This fixes sign-out errors caused by incorrect origin validation.
Changed APP_ORIGIN from localhost:8081 (web) to app://shieldtap for mobile app.
Added app://shieldtap to trustedOrigins in Better Auth configuration.
This fixes sign-out when testing on physical mobile devices.
Copy link

@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

🤖 Fix all issues with AI agents
In `@api/src/lib/auth.ts`:
- Line 25: Replace the hardcoded trustedOrigins array with an
environment-driven, origin-normalized list: read a comma-separated env var
(e.g., AUTH_TRUSTED_ORIGINS) and build the allowlist from it, and when
process.env.NODE_ENV === 'development' append localhost origins; for each entry
normalize using new URL(value).origin to strip paths and ensure consistent
origin comparisons before assigning to trustedOrigins (refer to the
trustedOrigins symbol in this module).

In `@mobile/src/api.ts`:
- Around line 174-176: When handling responses for requests whose path includes
'/sign-out' in the API flow, ensure you clear the local session token on
auth-failure statuses (401 and 403) before rethrowing or returning the error;
update the error/response handling in the same block that currently calls
clearSessionToken() so it also calls clearSessionToken() when response.status is
401 or 403 (reference the clearSessionToken function and the
path.includes('/sign-out') check) to prevent stale local tokens causing repeated
401s.

},
baseURL: env.BETTER_AUTH_URL,
secret: env.BETTER_AUTH_SECRET,
trustedOrigins: ['http://localhost:8081', 'http://localhost:8787'],
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Better Auth trustedOrigins configuration security production environment

💡 Result:

Concise production guidance for Better Auth trustedOrigins (security-focused)

  1. Only list exact, production HTTPS origins — avoid broad wildcards like "*.example.com" unless absolutely necessary. Use full scheme+host (https://app.example.com). [1][3]

  2. Do not include localhost/dev origins in production config or env — keep them separate for dev. [1][3]

  3. Prefer allowlist (trustedOrigins) + server-side redirect validation — never rely solely on the client or simple startsWith checks for callback/redirect parameters; re-validate the final redirect target against your allowlist before redirecting. [1][6]

  4. Avoid protocol-agnostic or double-slash redirects (//attacker.com) — normalize and parse callback URLs (require absolute https URLs) and reject URLs with missing host or scheme. This prevents open-redirect bypasses. [6][5]

  5. Minimize wildcard use and scope them tightly (e.g., https://*.internal.example.com only for internal subdomains) and prefer explicit host entries where possible. [1]

  6. Ensure cookies are secure in production: set useSecureCookies, SameSite=strict/lax as appropriate, and configure crossSubDomainCookies only when required and with a minimal domain. [0][4]

  7. Configure CORS to match trustedOrigins, require credentials only when needed, and set Access-Control-Allow-Origin to specific origins (not "*"). [4]

  8. Keep Better Auth up to date — several open-redirect/trustedOrigins bypasses have been reported and fixed; upgrade to patched versions (see advisories and Snyk/GitLab notes). Verify changelogs for fixes to origin-check/origin validation. [5][6][7]

  9. Add runtime defenses: logging/alerting for unexpected redirect/callback values, input size limits, and rate-limiting on auth endpoints. Use automated dependency scanning and monitor advisories. [6][7]

  10. Test: write unit/integration tests for origin-check and redirect flows, and run security tests (fuzzing for callbackURL) before release. [6]

Key references

  • Better Auth documentation: trustedOrigins, cookies and options. [1][3][4]
  • Public advisories / vulnerability reports about open-redirect / trustedOrigins bypasses — upgrade to fixed versions. [6][5][7]

If you want, I can:

  • review your current trustedOrigins list and config (paste it), or
  • produce a small hardened config example for production with exact settings.

🌐 Web query:

Better Auth documentation origin validation CORS trusted origins

💡 Result:

Short answer — Better Auth validates the Origin/Referer on incoming requests and blocks anything not in your trustedOrigins list. To allow browser requests from your frontend you must:

  • Add your frontend origin(s) to trustedOrigins (supports exact origins, protocoled wildcards, or a function returning origins). Example:
    trustedOrigins: ["https://app.example.com", "http://localhost:3000", "*.example.com"]. [1][2]

  • Ensure your server's CORS middleware is enabled before mounting Better Auth's handler (so Access-Control-* headers are sent). In frameworks like Nest/Express, call app.enableCors()/app.use(cors(...)) before app.use('/api/auth', toNodeHandler(auth)). Community reports confirm this ordering is required. [1][3]

  • Do NOT use disableOriginCheck or disableCSRFCheck in production (they disable origin validation / CSRF protections and open you to open-redirect/CSRF attacks). Use them only for local testing. [1]

Advanced:

  • You can provide a dynamic trustedOrigins function to look up IDP or multi-tenant origins at runtime. Plugins/endpoints can use ctx.context.isTrustedOrigin() to validate URLs. [2][3]

References:

  • Better Auth — Security (Origin validation, CSRF, disable flags). [1]
  • Better Auth — Options/trustedOrigins (static, wildcards, dynamic). [2]
  • Better Auth — Plugins / isTrustedOrigin usage and mounting notes. [3]

🌐 Web query:

Better Auth env configuration patterns development production

💡 Result:

Here are practical, production-safe patterns for auth/secret configuration across development, staging, and production — concise and actionable.

  1. Separate config vs secrets
  • Keep configuration (feature flags, non-sensitive settings) separate from secrets. Store secrets only in a secret manager; store non-sensitive config in a config service or env vars. [3][1]
  1. Don’t hard-code or commit secrets
  • Never check secrets into VCS. Keep an audited .env.example (no real values) for docs. Use pre-commit/githooks and git-secrets to block accidental commits. [6][4]
  1. Use a managed secret store in prod
  • Use Vault / AWS Secrets Manager / Azure Key Vault (or equivalent) for production secrets, with IAM-based access, automatic rotation, and audit logs. Keep short-lived credentials where possible. [2][3]
  1. Inject secrets at runtime (never bake into images)
  • Inject secrets into containers/VMs at runtime via platform integrations (K8s Secrets, cloud runtime injection, sidecar, or secret-agent) rather than embedding in build artifacts. This enables rotation without rebuilds. [2][3]
  1. CI/CD secrets only in pipeline vaults
  • Store sensitive CI/CD variables in the CI provider’s secret store (GitHub/GitLab Actions secrets, etc.). Provide least-privilege, time-limited tokens for deployments; avoid copying prod secrets into pipelines unless necessary and audited. [6][5]
  1. Local/dev ergonomics (isolated, safe)
  • Use local .env files or a developer secrets tool, but never reuse prod credentials. Provide a .env.example and a secure developer onboarding flow to mint test credentials or short-lived sandbox tokens. Consider a dev-mode local Vault or mocked secrets service. [6][7]
  1. Environment parity with isolation
  • Keep staging as close to prod as possible in architecture, but use separate credentials and accounts. Isolate networks and access policies so breaches in dev/staging can’t reach prod. [7][3]
  1. Validation, fail-fast, and secret schema
  • Validate required env values at startup (schema + type checks). Fail fast if a required secret is missing to avoid silent misconfiguration. Use tools like envalid/env-sentinel or runtime validators. [6][4]
  1. Rotation, auditing, and least privilege
  • Automate secret rotation and use conservative IAM/ACLs (least privilege). Enable auditing/alerting on secret access and anomalous usage. Revoke and rotate on suspicion of compromise. [3][2][7]
  1. Short-lived credentials & ephemeral access
  • Prefer short-lived credentials (token exchange, STS, dynamic secrets) over long-lived static keys; issue them on demand via your secret manager. [2][5]

Quick implementation checklist

  • Create .env.example and docs; block secret commits. [6]
  • Deploy a secret manager for prod (Vault/KeyVault/Secrets Manager). [2][3]
  • Wire CI/CD to fetch secrets from secure stores; avoid embedding secrets in build artifacts. [5]
  • Implement runtime injection, startup validation, logging/audit, and automated rotation. [2][3][7]

Sources

  • HashiCorp Vault recommended patterns (runtime secrets, rotation, least privilege). [1]
  • Microsoft Azure Key Vault best practices (store secrets, logging, rotation, do not store general config). [3]
  • Secrets management & staging best practices (isolation, rotation, monitoring). [7]
  • Env-sentinel/.env mistakes and CI/CD secret guidance (dev vs prod separation, validation, tooling). [6]
  • General secret-management guidance and CI patterns (CI secrets, short-lived tokens). [5]

If you want, I can: (a) map these patterns to a specific tech stack (e.g., Kubernetes + AWS), (b) give sample CI/CD steps and scripts, or (c) produce a checklist/template .env.example and Vault/Key Vault policy snippets. Which would you like?


Make trustedOrigins environment-driven (dev-only localhost) and normalize to origins.

Hardcoding localhost in trustedOrigins risks carrying dev origins into production. Better Auth validates all origins against this allowlist to prevent open-redirect and CSRF attacks; in production, only HTTPS origins should be included. Use an environment-configurable list that adds localhost only during development, and normalize each origin with new URL(...).origin to strip any path component and avoid mismatches.

🔧 Suggested refactor (env-configurable, origin-normalized)
 export interface AuthEnv {
   DB: D1Database;
   BETTER_AUTH_SECRET: string;
   BETTER_AUTH_URL: string;
+  BETTER_AUTH_TRUSTED_ORIGINS?: string; // comma-separated
   GOOGLE_CLIENT_ID: string;
   GOOGLE_CLIENT_SECRET: string;
   RESEND_API_KEY: string;
   DEV_MODE?: string;
 }
 
 export function createAuth(env: AuthEnv) {
+  const baseTrusted = (env.BETTER_AUTH_TRUSTED_ORIGINS ?? '')
+    .split(',')
+    .map((o) => o.trim())
+    .filter(Boolean)
+    .map((o) => new URL(o).origin);
+
+  const trustedOrigins =
+    env.DEV_MODE === 'true'
+      ? [...baseTrusted, 'http://localhost:8081', 'http://localhost:8787']
+      : baseTrusted;
+
   return betterAuth({
     database: {
       dialect: new D1Dialect({ database: env.DB }),
       type: 'sqlite',
     },
     baseURL: env.BETTER_AUTH_URL,
     secret: env.BETTER_AUTH_SECRET,
-    trustedOrigins: ['http://localhost:8081', 'http://localhost:8787'],
+    trustedOrigins,
🤖 Prompt for AI Agents
In `@api/src/lib/auth.ts` at line 25, Replace the hardcoded trustedOrigins array
with an environment-driven, origin-normalized list: read a comma-separated env
var (e.g., AUTH_TRUSTED_ORIGINS) and build the allowlist from it, and when
process.env.NODE_ENV === 'development' append localhost origins; for each entry
normalize using new URL(value).origin to strip paths and ensure consistent
origin comparisons before assigning to trustedOrigins (refer to the
trustedOrigins symbol in this module).

Richard Oliver Bray added 3 commits January 18, 2026 21:14
- Use wildcard (*) for trustedOrigins when DEV_MODE=true
- This allows Expo Go to use any exp:// URL (e.g., exp://192.168.5.151:8081)
- In production, only specific origins are trusted
- Made APP_ORIGIN configurable via EXPO_PUBLIC_APP_ORIGIN env var
Created NOTES.md for tracking production deployment requirements and mobile app origin configuration. This file contains internal notes for future reference and should not be committed.
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