-
Notifications
You must be signed in to change notification settings - Fork 0
Fix authentication: implement manual session token management for React Native #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
…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
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the 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. 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughAdds 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
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this 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:
Redundant
response.okcheck (Line 159): This condition is always true here since!response.okthrows at line 142-148.Silent failures on token operations: If
saveSessionTokenorclearSessionTokenthrows (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.
There was a problem hiding this 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); + }
- 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.
There was a problem hiding this 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.
api/src/lib/auth.ts
Outdated
| }, | ||
| baseURL: env.BETTER_AUTH_URL, | ||
| secret: env.BETTER_AUTH_SECRET, | ||
| trustedOrigins: ['http://localhost:8081', 'http://localhost:8787'], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
Better Auth trustedOrigins configuration security production environment
💡 Result:
Concise production guidance for Better Auth trustedOrigins (security-focused)
-
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]
-
Do not include localhost/dev origins in production config or env — keep them separate for dev. [1][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]
-
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]
-
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]
-
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]
-
Configure CORS to match trustedOrigins, require credentials only when needed, and set Access-Control-Allow-Origin to specific origins (not "*"). [4]
-
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]
-
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]
-
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.
- 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]
- 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]
- 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]
- 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]
- 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]
- 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]
- 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]
- 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]
- 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]
- 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).
- 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.
Summary
Fixes 401 Unauthorized error when accessing
/api/meafter login in the React Native app.Problem
React Native's
fetchdoesn't automatically handle cookies like browsers do. When users signed in:Set-Cookie: __Secure-better-auth.session_token=.../api/me) failed with 401Solution
Implemented manual session token management:
expo-secure-storefor secure token storagesession.tsutility for token lifecycle (save/get/clear/extract)api.tsto:Technical Details
__Secure-better-auth.session_tokencookieCookie: __Secure-better-auth.session_token=<token>Testing
Test the login flow:
/api/mereturns 200 with user data (previously 401)Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.