[GH-ISSUE #2058] Expo/iOS native: use browser for the sign-in / sign-up UI? #9032

Closed
opened 2026-04-13 04:19:04 -05:00 by GiteaMirror · 11 comments
Owner

Originally created by @MikeChongCan on GitHub (Mar 30, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/2058

Is this suited for github?

  • Yes, this is suited for github

Is there any way to configure expo/native to open the browser for sign in or sign up flow instead of purely native?

  • The current guide of expo integration: we must implement the sign in/up UI in react native
  • What we want: just start a browser for the whole auth flow and launch the app with an oauth code to exchange the tokens (access / refresh tokens)

It would make it much easier to update the UI of sign in/up forms and add more social providers without updating the native app.

Describe the solution you'd like

maybe some docs about a native app flow? or auth client in native?

Describe alternatives you've considered

N/A

Additional context

No response

Originally created by @MikeChongCan on GitHub (Mar 30, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/2058 ### Is this suited for github? - [x] Yes, this is suited for github ### Is your feature request related to a problem? Please describe. Is there any way to configure expo/native to open the browser for sign in or sign up flow instead of purely native? - The current guide of expo integration: we must implement the sign in/up UI in react native - What we want: just start a browser for the whole auth flow and launch the app with an oauth code to exchange the tokens (access / refresh tokens) It would make it much easier to update the UI of sign in/up forms and add more social providers without updating the native app. ### Describe the solution you'd like maybe some docs about a native app flow? or auth client in native? ### Describe alternatives you've considered N/A ### Additional context _No response_
GiteaMirror added the locked label 2026-04-13 04:19:04 -05:00
Author
Owner

@Kinfe123 commented on GitHub (Apr 6, 2025):

ig this issue is not related to better auth. please catch up with the recommended intergration flow here on better auth docs - https://www.better-auth.com/docs/integrations/expo

<!-- gh-comment-id:2781388481 --> @Kinfe123 commented on GitHub (Apr 6, 2025): ig this issue is not related to better auth. please catch up with the recommended intergration flow here on better auth docs - https://www.better-auth.com/docs/integrations/expo
Author
Owner

@MikeChongCan commented on GitHub (Apr 7, 2025):

ig this issue is not related to better auth. please catch up with the recommended intergration flow here on better auth docs - https://www.better-auth.com/docs/integrations/expo

Thanks for your reply

But it is related to better auth

https://www.better-auth.com/docs/integrations/expo#usage

The usage section only has an example of native UI
Not web UI

My question is about how to use WebUI
Coz it is way easier to change and deploy

<!-- gh-comment-id:2783874352 --> @MikeChongCan commented on GitHub (Apr 7, 2025): > ig this issue is not related to better auth. please catch up with the recommended intergration flow here on better auth docs - https://www.better-auth.com/docs/integrations/expo Thanks for your reply But it is related to better auth https://www.better-auth.com/docs/integrations/expo#usage The usage section only has an example of native UI Not web UI My question is about how to use WebUI Coz it is way easier to change and deploy
Author
Owner

@MikeChongCan commented on GitHub (Apr 8, 2025):

@Kinfe123 can you re-open this issue? The answer is not related to my question

<!-- gh-comment-id:2787123870 --> @MikeChongCan commented on GitHub (Apr 8, 2025): @Kinfe123 can you re-open this issue? The answer is not related to my question
Author
Owner

@Kinfe123 commented on GitHub (Apr 8, 2025):

Oh yeah I see .. it was not descriptive enough on last time I saw it and not related to better auth

<!-- gh-comment-id:2787150125 --> @Kinfe123 commented on GitHub (Apr 8, 2025): Oh yeah I see .. it was not descriptive enough on last time I saw it and not related to better auth
Author
Owner

@MikeChongCan commented on GitHub (Apr 8, 2025):

@Kinfe123 thanks! Let me edit it a little bit.
I would love to work on this if you folks have some hints. Like how to start an auth url in expo browsers with a callback to this app then exchange auth token to access keys / refresh keys

<!-- gh-comment-id:2787191124 --> @MikeChongCan commented on GitHub (Apr 8, 2025): @Kinfe123 thanks! Let me edit it a little bit. I would love to work on this if you folks have some hints. Like how to start an auth url in expo browsers with a callback to this app then exchange auth token to access keys / refresh keys
Author
Owner

@uguraktas commented on GitHub (Apr 26, 2025):

@imWildCat I will use better-auth as well. is there any best practices about it? and what do u think?

<!-- gh-comment-id:2831982939 --> @uguraktas commented on GitHub (Apr 26, 2025): @imWildCat I will use better-auth as well. is there any best practices about it? and what do u think?
Author
Owner

@MikeChongCan commented on GitHub (Apr 26, 2025):

@imWildCat I will use better-auth as well. is there any best practices about it? and what do u think?

I haven't got the time to look into re-implementing something like expo for better auth...
for my projects personally I would create another table for sessions of native clients and use web auth / redirect back to the app scheme because it is obviously faster to implement.

<!-- gh-comment-id:2832324616 --> @MikeChongCan commented on GitHub (Apr 26, 2025): > [@imWildCat](https://github.com/imWildCat) I will use better-auth as well. is there any best practices about it? and what do u think? I haven't got the time to look into re-implementing something like expo for better auth... for my projects personally I would create another table for sessions of native clients and use web auth / redirect back to the app scheme because it is obviously faster to implement.
Author
Owner

@MikeChongCan commented on GitHub (May 15, 2025):

Tried something like this but it didn't work.

The problem is like case 2 (web sign in with email) with redirect and cookie search param is not passed into the safari auth session's result, which is strange.

not sure whether folks like @Kinfe123 can chime in and help me figure this out...

// server expo web plugin.ts

// From: https://github.com/better-auth/better-auth/blob/main/packages/expo/src/index.ts
import type { BetterAuthPlugin } from "better-auth";
import { createAuthMiddleware } from "better-auth/api";

export interface ExpoWebAuthOptions {
	/**
	 * Override origin header for expo API routes
	 */
	overrideOrigin?: boolean;
}

export const expo = (options?: ExpoWebAuthOptions) => {
	return {
		id: "expo",
		init: (ctx) => {
			const trustedOrigins =
				process.env.NODE_ENV === "development"
					? [...(ctx.trustedOrigins || []), "exp://"]
					: ctx.trustedOrigins;
			return {
				options: {
					trustedOrigins,
				},
			};
		},
		// eslint-disable-next-line @typescript-eslint/no-unused-vars
		async onRequest(request, ctx) {
			if (!options?.overrideOrigin || request.headers.get("origin")) {
				return;
			}
			/**
			 * To bypass origin check from expo, we need to set the origin header to the expo-origin header
			 */
			const expoOrigin = request.headers.get("expo-origin");
			if (!expoOrigin) {
				return;
			}
			const req = request.clone();
			req.headers.set("origin", expoOrigin);
			return {
				request: req,
			};
		},
		hooks: {
			after: [
				{
					matcher(context) {
						return (
							context.path?.startsWith("/callback") ||
							context.path?.startsWith("/oauth2/callback")
						);
					},
					handler: createAuthMiddleware(async (ctx) => {
						const headers = ctx.context.responseHeaders;
						const location = headers?.get("location");
						if (!location) {
							return;
						}
						const trustedOrigins = ctx.context.trustedOrigins.filter(
							(origin: string) => !origin.startsWith("http"),
						);
						const isTrustedOrigin = trustedOrigins.some((origin: string) =>
							location?.startsWith(origin),
						);
						if (!isTrustedOrigin) {
							return;
						}
						const cookie = headers?.get("set-cookie");
						if (!cookie) {
							return;
						}
						const url = new URL(location);
						url.searchParams.set("cookie", cookie);
						ctx.setHeader("location", url.toString());
					}),
				},
			],
		},
	} satisfies BetterAuthPlugin;
};
// custom client.ts
import { useState, useCallback } from "react";
import * as WebBrowser from "expo-web-browser";
import * as Linking from "expo-linking";
import { authClient, cookieName, ExpoBetterAuthSecureStore } from "../lib/auth-client";
import { WEB_APP_AUTH_URL } from "../services/API_BASE_URL";
import { getSetCookie } from "@better-auth/expo/client";
import { AppToast } from "../lib/app-toast"; // Assuming AppToast exists and is importable

export const useMobileWebAuth = () => {
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const startWebAuth = useCallback(async () => {
    setError(null);
    setIsLoading(true);
    let sessionHandled = false; // Flag to ensure we only set error if not handled by cancel/success
    try {
      const mobileRedirectUri = Linking.createURL("auth");
      const webAuthUrl = `${WEB_APP_AUTH_URL}?isMobileFlow=true&mobileRedirectUri=${encodeURIComponent(mobileRedirectUri)}`;

      console.log("🚀 ~ startWebAuth ~ webAuthUrl:", webAuthUrl)
      const result = await WebBrowser.openAuthSessionAsync(webAuthUrl, mobileRedirectUri);

      if (result.type === "success") {
        sessionHandled = true;
        const url = new URL(result.url);
        console.log("🚀 ~ startWebAuth ~ url:", url)

        // NOTE: result.url here does not contain `cookie` it is just `scheme://auth` without anything


        const cookieHeaderValue = url.searchParams.get("cookie");

        if (!cookieHeaderValue) {
          console.error("Web auth success but no cookie received in redirect URL param.");
          setError("Authentication session ended without a valid session token.");
          return;
        }
        
        const sessionCookie = getSetCookie(cookieHeaderValue); 

        if (!sessionCookie) {
             console.error("Web auth success but failed to parse cookie from redirect URL param.");
             setError("Failed to process session token.");
             return;
        }

        await ExpoBetterAuthSecureStore.setItemAsync(cookieName, sessionCookie);
        authClient.$store.notify('$sessionSignal');
      } else if (result.type === "cancel" || result.type === "dismiss" || result.type === "locked") {
        sessionHandled = true;
        // AppToast.error("Sign-in attempt cancelled."); // User might not want a toast for simple cancel.
        console.log(`Web auth session ended with type: ${result.type}`);
      } else {
        // Handle other WebBrowser result types if necessary.
        console.warn("Unexpected result type from WebBrowserAuthSessionAsync:", result);
        setError("Sign-in attempt produced an unexpected result.");
      }
    } catch (err: any) {
      console.error("Error during web auth:", err);
      setError(err.message ?? "An unexpected error occurred during sign-in.");
    } finally {
      setIsLoading(false);
      if (!sessionHandled && !error) { // If flow was interrupted unexpectedly before result (e.g. app killed)
        // setError("The sign-in process was interrupted.");
      }
    }
  }, []); // Assuming WEB_APP_AUTH_URL, cookieName, AppToast, authClient are stable.

  const clearError = useCallback(() => setError(null), []);

  return { startWebAuth, isLoading, error, clearError };
}; 
<!-- gh-comment-id:2882189706 --> @MikeChongCan commented on GitHub (May 15, 2025): Tried something like this but it didn't work. The problem is like case 2 (web sign in with email) with redirect and `cookie` search param is not passed into the safari auth session's result, which is strange. not sure whether folks like @Kinfe123 can chime in and help me figure this out... ```ts // server expo web plugin.ts // From: https://github.com/better-auth/better-auth/blob/main/packages/expo/src/index.ts import type { BetterAuthPlugin } from "better-auth"; import { createAuthMiddleware } from "better-auth/api"; export interface ExpoWebAuthOptions { /** * Override origin header for expo API routes */ overrideOrigin?: boolean; } export const expo = (options?: ExpoWebAuthOptions) => { return { id: "expo", init: (ctx) => { const trustedOrigins = process.env.NODE_ENV === "development" ? [...(ctx.trustedOrigins || []), "exp://"] : ctx.trustedOrigins; return { options: { trustedOrigins, }, }; }, // eslint-disable-next-line @typescript-eslint/no-unused-vars async onRequest(request, ctx) { if (!options?.overrideOrigin || request.headers.get("origin")) { return; } /** * To bypass origin check from expo, we need to set the origin header to the expo-origin header */ const expoOrigin = request.headers.get("expo-origin"); if (!expoOrigin) { return; } const req = request.clone(); req.headers.set("origin", expoOrigin); return { request: req, }; }, hooks: { after: [ { matcher(context) { return ( context.path?.startsWith("/callback") || context.path?.startsWith("/oauth2/callback") ); }, handler: createAuthMiddleware(async (ctx) => { const headers = ctx.context.responseHeaders; const location = headers?.get("location"); if (!location) { return; } const trustedOrigins = ctx.context.trustedOrigins.filter( (origin: string) => !origin.startsWith("http"), ); const isTrustedOrigin = trustedOrigins.some((origin: string) => location?.startsWith(origin), ); if (!isTrustedOrigin) { return; } const cookie = headers?.get("set-cookie"); if (!cookie) { return; } const url = new URL(location); url.searchParams.set("cookie", cookie); ctx.setHeader("location", url.toString()); }), }, ], }, } satisfies BetterAuthPlugin; }; ``` ```ts // custom client.ts import { useState, useCallback } from "react"; import * as WebBrowser from "expo-web-browser"; import * as Linking from "expo-linking"; import { authClient, cookieName, ExpoBetterAuthSecureStore } from "../lib/auth-client"; import { WEB_APP_AUTH_URL } from "../services/API_BASE_URL"; import { getSetCookie } from "@better-auth/expo/client"; import { AppToast } from "../lib/app-toast"; // Assuming AppToast exists and is importable export const useMobileWebAuth = () => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState<string | null>(null); const startWebAuth = useCallback(async () => { setError(null); setIsLoading(true); let sessionHandled = false; // Flag to ensure we only set error if not handled by cancel/success try { const mobileRedirectUri = Linking.createURL("auth"); const webAuthUrl = `${WEB_APP_AUTH_URL}?isMobileFlow=true&mobileRedirectUri=${encodeURIComponent(mobileRedirectUri)}`; console.log("🚀 ~ startWebAuth ~ webAuthUrl:", webAuthUrl) const result = await WebBrowser.openAuthSessionAsync(webAuthUrl, mobileRedirectUri); if (result.type === "success") { sessionHandled = true; const url = new URL(result.url); console.log("🚀 ~ startWebAuth ~ url:", url) // NOTE: result.url here does not contain `cookie` it is just `scheme://auth` without anything const cookieHeaderValue = url.searchParams.get("cookie"); if (!cookieHeaderValue) { console.error("Web auth success but no cookie received in redirect URL param."); setError("Authentication session ended without a valid session token."); return; } const sessionCookie = getSetCookie(cookieHeaderValue); if (!sessionCookie) { console.error("Web auth success but failed to parse cookie from redirect URL param."); setError("Failed to process session token."); return; } await ExpoBetterAuthSecureStore.setItemAsync(cookieName, sessionCookie); authClient.$store.notify('$sessionSignal'); } else if (result.type === "cancel" || result.type === "dismiss" || result.type === "locked") { sessionHandled = true; // AppToast.error("Sign-in attempt cancelled."); // User might not want a toast for simple cancel. console.log(`Web auth session ended with type: ${result.type}`); } else { // Handle other WebBrowser result types if necessary. console.warn("Unexpected result type from WebBrowserAuthSessionAsync:", result); setError("Sign-in attempt produced an unexpected result."); } } catch (err: any) { console.error("Error during web auth:", err); setError(err.message ?? "An unexpected error occurred during sign-in."); } finally { setIsLoading(false); if (!sessionHandled && !error) { // If flow was interrupted unexpectedly before result (e.g. app killed) // setError("The sign-in process was interrupted."); } } }, []); // Assuming WEB_APP_AUTH_URL, cookieName, AppToast, authClient are stable. const clearError = useCallback(() => setError(null), []); return { startWebAuth, isLoading, error, clearError }; }; ```
Author
Owner

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

Hi, @imWildCat. I'm Dosu, and I'm helping the better-auth team manage their backlog and am marking this issue as stale.

Issue Summary

  • You are seeking guidance on implementing Expo/iOS native apps' sign-in/sign-up flows using an external browser (WebUI) instead of native UI to simplify updates and adding social providers.
  • Current Better Auth documentation primarily shows native UI examples, and you want best practices for OAuth flows that redirect back to the app with tokens.
  • You shared a custom Expo plugin and client code attempting to handle web auth sessions but encountered issues with missing cookie parameters in the redirect URL.
  • A maintainer acknowledged the initial misunderstanding of the issue but has not yet provided a direct solution.
  • Another user has also expressed interest in best practices for this approach, indicating community demand.

Next Steps

  • Please let me know if this issue is still relevant with the latest version of better-auth by commenting here to keep the discussion open.
  • If I do not hear back within 7 days, I will automatically close this issue to help keep the backlog manageable.

Thank you for your understanding and contribution!

<!-- gh-comment-id:3188994444 --> @dosubot[bot] commented on GitHub (Aug 14, 2025): Hi, @imWildCat. I'm [Dosu](https://dosu.dev), and I'm helping the better-auth team manage their backlog and am marking this issue as stale. **Issue Summary** - You are seeking guidance on implementing Expo/iOS native apps' sign-in/sign-up flows using an external browser (WebUI) instead of native UI to simplify updates and adding social providers. - Current Better Auth documentation primarily shows native UI examples, and you want best practices for OAuth flows that redirect back to the app with tokens. - You shared a custom Expo plugin and client code attempting to handle web auth sessions but encountered issues with missing cookie parameters in the redirect URL. - A maintainer acknowledged the initial misunderstanding of the issue but has not yet provided a direct solution. - Another user has also expressed interest in best practices for this approach, indicating community demand. **Next Steps** - Please let me know if this issue is still relevant with the latest version of better-auth by commenting here to keep the discussion open. - If I do not hear back within 7 days, I will automatically close this issue to help keep the backlog manageable. Thank you for your understanding and contribution!
Author
Owner

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

This is very dumb to use AI to spamming us, so I just close this issue. I have my own solution now and do not want to talk about it anymore.

<!-- gh-comment-id:3189412876 --> @MikeChongCan commented on GitHub (Aug 14, 2025): This is very dumb to use AI to spamming us, so I just close this issue. I have my own solution now and do not want to talk about it anymore.
Author
Owner

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

Thank you for your contribution, imWildCat! We appreciate your help in keeping the repository tidy.

<!-- gh-comment-id:3189414687 --> @dosubot[bot] commented on GitHub (Aug 14, 2025): Thank you for your contribution, imWildCat! We appreciate your help in keeping the repository tidy.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#9032