[GH-ISSUE #3743] signOut fails on Safari iOS with "Invalid Origin" error #27031

Closed
opened 2026-04-17 17:49:48 -05:00 by GiteaMirror · 4 comments
Owner

Originally created by @AyDevPro on GitHub (Aug 1, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/3743

Better Auth signOut fails on Safari iOS with "Invalid Origin" error

🐛 Bug Description

signOut() fails systematically on Safari iOS with HTTP 403: FORBIDDEN - {"code": "INVALID_ORIGIN", "message": "Invalid origin"} error, while working perfectly on desktop browsers.

🔄 Reproduction Steps

  1. Setup: Next.js 15 app with Better Auth v1.3.4, Docker development environment
  2. Device: iPhone with Safari iOS (tested on multiple devices)
  3. Action: Click logout/signOut button
  4. Result: Fails with INVALID_ORIGIN error

📱 Environment

  • Better Auth: v1.3.4
  • Framework: Next.js 15.4.4 with App Router
  • Browser: Safari iOS (mobile)
  • Development: Docker (localhost:3000)
  • Database: PostgreSQL with Prisma adapter

🔧 Configuration

// src/lib/auth.ts
export const auth = betterAuth({
  baseURL: process.env.BETTER_AUTH_URL ?? "http://localhost:3000",
  database: prismaAdapter(prisma, { provider: "postgresql" }),
  emailAndPassword: {
    enabled: true,
    minPasswordLength: 8,
    requireEmailVerification: false,
  },
  trustedOrigins: [
    "http://localhost:3000",
    "http://127.0.0.1:3000",
    "http://0.0.0.0:3000",
    "http://172.18.0.3:3000", // Docker network
    "http://host.docker.internal:3000",
    "http://localhost",
    "http://127.0.0.1",
    "http://192.168.1.1:3000",
    "http://10.0.0.1:3000",
  ],
  cookies: { 
    secure: false,
    sameSite: "lax",
  },
  plugins: [nextCookies()],
});

🚨 Error Details

Client-side error (Better Auth):

SignOut error callback - Message: Pas de message, Status: Pas de status, Code: Pas de code, Keys: response, responseText, request, error

Direct POST to /api/auth/sign-out:

HTTP 403: FORBIDDEN
Body: {"code": "INVALID_ORIGIN", "message": "Invalid origin"}

Safari iOS logs:

// Current origin: http://localhost:3000
// Current hostname: localhost  
// Current href: http://localhost:3000/dashboard

Desktop vs Mobile Behavior

Browser Login Logout Status
Chrome Desktop Works Works Perfect
Firefox Desktop Works Works Perfect
Safari Desktop Works Works Perfect
Safari iOS Works FAILS INVALID_ORIGIN
Chrome iOS Works FAILS INVALID_ORIGIN

🛠️ Workaround Implemented

Created custom endpoint /api/logout-safari that manually clears cookies and bypasses Better Auth origin validation:

// /api/logout-safari/route.ts
export async function POST(request: NextRequest) {
  const cookieStore = cookies();
  const response = NextResponse.json({ success: true });
  
  // Clear all auth cookies manually
  const cookiesToDelete = ['better-auth.session_token', 'session_token', ...];
  cookiesToDelete.forEach(cookieName => {
    response.cookies.set(cookieName, '', {
      expires: new Date(0),
      path: '/',
    });
  });
  
  return response;
}

🤔 Root Cause Analysis

The issue seems to be that Safari iOS sends an origin header that Better Auth doesn't recognize as valid, even when:

  • Using the exact same URL (localhost:3000)
  • Including all possible localhost variations in trustedOrigins
  • Login works perfectly with the same origin

💡 Suggested Solutions

  1. Debug logging: Add server-side logging to show exactly what origin Safari iOS sends
  2. More permissive dev mode: Allow bypassing origin validation in development
  3. Safari-specific handling: Better Auth could detect Safari iOS user agents and handle origins differently
  4. Documentation: Add Safari iOS troubleshooting section
  • Similar to #2203 (Invalid Origin on Vercel deployments)
  • May affect other mobile browsers beyond Safari iOS

🙏 Impact

This affects all Safari iOS users (significant mobile traffic) and requires implementing custom workarounds, undermining Better Auth's "just works" philosophy.


Note: Issue discovered during production development of judo attendance management system. Willing to provide additional debugging info or test fixes.

Originally created by @AyDevPro on GitHub (Aug 1, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/3743 # Better Auth signOut fails on Safari iOS with "Invalid Origin" error ## 🐛 Bug Description **signOut() fails systematically on Safari iOS** with `HTTP 403: FORBIDDEN - {"code": "INVALID_ORIGIN", "message": "Invalid origin"}` error, while working perfectly on desktop browsers. ## 🔄 Reproduction Steps 1. **Setup**: Next.js 15 app with Better Auth v1.3.4, Docker development environment 2. **Device**: iPhone with Safari iOS (tested on multiple devices) 3. **Action**: Click logout/signOut button 4. **Result**: Fails with INVALID_ORIGIN error ## 📱 Environment - **Better Auth**: v1.3.4 - **Framework**: Next.js 15.4.4 with App Router - **Browser**: Safari iOS (mobile) - **Development**: Docker (localhost:3000) - **Database**: PostgreSQL with Prisma adapter ## 🔧 Configuration ```typescript // src/lib/auth.ts export const auth = betterAuth({ baseURL: process.env.BETTER_AUTH_URL ?? "http://localhost:3000", database: prismaAdapter(prisma, { provider: "postgresql" }), emailAndPassword: { enabled: true, minPasswordLength: 8, requireEmailVerification: false, }, trustedOrigins: [ "http://localhost:3000", "http://127.0.0.1:3000", "http://0.0.0.0:3000", "http://172.18.0.3:3000", // Docker network "http://host.docker.internal:3000", "http://localhost", "http://127.0.0.1", "http://192.168.1.1:3000", "http://10.0.0.1:3000", ], cookies: { secure: false, sameSite: "lax", }, plugins: [nextCookies()], }); ``` ## 🚨 Error Details **Client-side error (Better Auth):** ``` SignOut error callback - Message: Pas de message, Status: Pas de status, Code: Pas de code, Keys: response, responseText, request, error ``` **Direct POST to /api/auth/sign-out:** ``` HTTP 403: FORBIDDEN Body: {"code": "INVALID_ORIGIN", "message": "Invalid origin"} ``` **Safari iOS logs:** ```javascript // Current origin: http://localhost:3000 // Current hostname: localhost // Current href: http://localhost:3000/dashboard ``` ## ✅ Desktop vs Mobile Behavior | Browser | Login | Logout | Status | |---------|-------|--------|--------| | Chrome Desktop | ✅ Works | ✅ Works | Perfect | | Firefox Desktop | ✅ Works | ✅ Works | Perfect | | Safari Desktop | ✅ Works | ✅ Works | Perfect | | Safari iOS | ✅ Works | ❌ FAILS | INVALID_ORIGIN | | Chrome iOS | ✅ Works | ❌ FAILS | INVALID_ORIGIN | ## 🛠️ Workaround Implemented Created custom endpoint `/api/logout-safari` that manually clears cookies and bypasses Better Auth origin validation: ```typescript // /api/logout-safari/route.ts export async function POST(request: NextRequest) { const cookieStore = cookies(); const response = NextResponse.json({ success: true }); // Clear all auth cookies manually const cookiesToDelete = ['better-auth.session_token', 'session_token', ...]; cookiesToDelete.forEach(cookieName => { response.cookies.set(cookieName, '', { expires: new Date(0), path: '/', }); }); return response; } ``` ## 🤔 Root Cause Analysis The issue seems to be that **Safari iOS sends an origin header that Better Auth doesn't recognize** as valid, even when: - Using the exact same URL (localhost:3000) - Including all possible localhost variations in `trustedOrigins` - Login works perfectly with the same origin ## 💡 Suggested Solutions 1. **Debug logging**: Add server-side logging to show exactly what origin Safari iOS sends 2. **More permissive dev mode**: Allow bypassing origin validation in development 3. **Safari-specific handling**: Better Auth could detect Safari iOS user agents and handle origins differently 4. **Documentation**: Add Safari iOS troubleshooting section ## 📚 Related Issues - Similar to #2203 (Invalid Origin on Vercel deployments) - May affect other mobile browsers beyond Safari iOS ## 🙏 Impact This affects **all Safari iOS users** (significant mobile traffic) and requires implementing custom workarounds, undermining Better Auth's "just works" philosophy. --- **Note**: Issue discovered during production development of judo attendance management system. Willing to provide additional debugging info or test fixes.
GiteaMirror added the lockedbug labels 2026-04-17 17:49:48 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Aug 1, 2025):

Hi @AyDevPro! I'm Dosu and I’m helping the better-auth team.

This is a known pain point: Better Auth’s origin validation is strict and matches the Origin (or Referer) header exactly (including protocol and port) against your trustedOrigins list. There’s no special handling for Safari iOS or mobile browsers, and any mismatch—even subtle ones—will cause a 403 INVALID_ORIGIN error. Safari and Chrome on iOS are known to sometimes send nonstandard or unexpected Origin headers, which can trip up strict checks even if the visible URL matches your config (middleware code, discussion).

There’s no built-in dev mode or automatic relaxation for local/mobile development. If the Origin header is missing, validation is skipped, but if it’s present and doesn’t match, the request is denied. The only way to bypass this is to set disableCSRFCheck: true in advanced options, but this is a security risk and not recommended outside of isolated dev environments (types).

Debugging tip: Add server-side logging to print the actual Origin and Referer headers received from Safari iOS during signOut. This will show you exactly what’s being sent and why it fails validation. For example, in your sign-out API route, log request.headers.get('origin') and request.headers.get('referer').

Best practices: For local/mobile dev, keep your trustedOrigins list as broad as possible (including all localhost variations and Docker IPs), but be aware that some mobile browsers may still send unexpected values. For production, always use your real domain and avoid public suffix domains (like render.com) to prevent Safari cookie and origin issues (Safari cross-domain discussion).

If you want to propose a more flexible dev mode or Safari/mobile-specific handling, that would be a good feature request—currently, Better Auth enforces strict CSRF protection for all platforms.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

<!-- gh-comment-id:3145853483 --> @dosubot[bot] commented on GitHub (Aug 1, 2025): <!-- Greeting --> Hi @AyDevPro! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> This is a known pain point: Better Auth’s origin validation is strict and matches the Origin (or Referer) header exactly (including protocol and port) against your trustedOrigins list. There’s no special handling for Safari iOS or mobile browsers, and any mismatch—even subtle ones—will cause a 403 INVALID_ORIGIN error. Safari and Chrome on iOS are known to sometimes send nonstandard or unexpected Origin headers, which can trip up strict checks even if the visible URL matches your config ([middleware code](https://github.com/better-auth/better-auth/blob/a2474faac6eca88f0b28b2db792bb95c92857128/packages/better-auth/src/api/middlewares/origin-check.ts), [discussion](https://github.com/better-auth/better-auth/discussions/2257)). There’s no built-in dev mode or automatic relaxation for local/mobile development. If the Origin header is missing, validation is skipped, but if it’s present and doesn’t match, the request is denied. The only way to bypass this is to set `disableCSRFCheck: true` in advanced options, but this is a security risk and not recommended outside of isolated dev environments ([types](https://github.com/better-auth/better-auth/blob/a2474faac6eca88f0b28b2db792bb95c92857128/packages/better-auth/src/types/options.ts)). **Debugging tip:** Add server-side logging to print the actual Origin and Referer headers received from Safari iOS during signOut. This will show you exactly what’s being sent and why it fails validation. For example, in your sign-out API route, log `request.headers.get('origin')` and `request.headers.get('referer')`. **Best practices:** For local/mobile dev, keep your trustedOrigins list as broad as possible (including all localhost variations and Docker IPs), but be aware that some mobile browsers may still send unexpected values. For production, always use your real domain and avoid public suffix domains (like render.com) to prevent Safari cookie and origin issues ([Safari cross-domain discussion](https://github.com/better-auth/better-auth/discussions/2826)). If you want to propose a more flexible dev mode or Safari/mobile-specific handling, that would be a good feature request—currently, Better Auth enforces strict CSRF protection for all platforms. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/01a5a726-0cab-4527-960c-1e0e6fe28714?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/01a5a726-0cab-4527-960c-1e0e6fe28714?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/01a5a726-0cab-4527-960c-1e0e6fe28714?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/01a5a726-0cab-4527-960c-1e0e6fe28714?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/01a5a726-0cab-4527-960c-1e0e6fe28714?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/01a5a726-0cab-4527-960c-1e0e6fe28714?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/01a5a726-0cab-4527-960c-1e0e6fe28714?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/3743)
Author
Owner

@himself65 commented on GitHub (Aug 2, 2025):

This usually happened when you lost a cookie or a session before you called this API. Could you please check login login-related code to make sure if user has logged in successfully?

Imo, 172.18.0.3 is a Docker private IP, which is not the real origin in the frontend

<!-- gh-comment-id:3146253901 --> @himself65 commented on GitHub (Aug 2, 2025): This usually happened when you lost a cookie or a session before you called this API. Could you please check login login-related code to make sure if user has logged in successfully? Imo, `172.18.0.3` is a Docker private IP, which is not the real origin in the frontend
Author
Owner

@himself65 commented on GitHub (Aug 11, 2025):

How do you test the iOS on your phone or simulator? I suppose in such a device, the domain will not be localhost or a Docker network IP address. I would suggest you check your local network router, or you try to deploy to a remote server, and give it a try

<!-- gh-comment-id:3174427297 --> @himself65 commented on GitHub (Aug 11, 2025): How do you test the iOS on your phone or simulator? I suppose in such a device, the domain will not be `localhost` or a Docker network IP address. I would suggest you check your local network router, or you try to deploy to a remote server, and give it a try
Author
Owner

@frectonz commented on GitHub (Aug 14, 2025):

Closing this as state, if you are still facing the issue, please comment here with more information about the problem you are facing.

<!-- gh-comment-id:3190090701 --> @frectonz commented on GitHub (Aug 14, 2025): Closing this as state, if you are still facing the issue, please comment here with more information about the problem you are facing.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#27031