
Native vs. Browser OAuth in Expo: A Decision Guide for Social Login
The browser bounce
Your user taps "Sign in with Google." The app flings them to Safari. They authenticate. Safari bounces them back. Maybe. On a good day.
On a bad day, the redirect silently fails. One Expo team with 50,000+ users reported that roughly 30% of their Android sign-in attempts returned a DISMISS result, meaning the user completed authentication but never made it back to the app (Expo GitHub Issue #23781, 2024). That's an incident report, not a universal rate, but the underlying race condition in expo-auth-session remains unresolved and other teams have flagged similar behavior.
This is the core tension behind social login in Expo: two architectures, two very different tradeoffs.
Browser-based OAuth opens a system browser tab (Safari on iOS, Chrome on Android), the user authenticates there, and a deep link redirects them back to your app. It's the pattern most tutorials teach. It works with any provider. And it carries real friction.
Native OAuth skips the browser entirely. On Android, Google's Credential Manager handles authentication inside the app. On iOS, ASAuthorization presents a native bottom sheet. No redirect. No app switching. The user stays where they are.
Both patterns work. The right choice depends on which problems you're willing to own.
Industry surveys suggest that around 77% of users prefer signing in with existing accounts rather than creating new credentials (LoginRadius Infographic, 2024). Google alone accounts for roughly 75% of all social logins across the Auth0/Okta platform (Okta/Auth0 Social Login Report, 2024). So whichever approach you pick, it very likely needs to work well for Google at minimum.
There's a broader signal here too: desktop converts at roughly 4.8% versus 2.9% on mobile, and auth friction is a contributor to that gap (Corbado, 2026). Every extra tap, every context switch, every "Open in Safari?" prompt chips away at conversion.
This guide is for Expo and React Native developers choosing between browser and native OAuth for production social login. We'll walk through both architectures with working code, compare the tradeoffs, and cover the platform requirements that'll shape your decision.
How browser-based OAuth works in Expo
The browser OAuth flow follows a predictable sequence:
- User taps "Sign in with Google" in your app.
- Your app opens a system browser tab (via
expo-web-browser). - The browser loads the provider's consent screen.
- User authenticates and grants permissions.
- The provider redirects to your app's deep link (e.g.,
myapp://dashboard). expo-web-browsercatches the redirect and passes data back.- Your app creates a session from the returned tokens.
Here's what that looks like with Clerk's useSSO hook:
// components/BrowserGoogleSignIn.tsx
import React from 'react'
import * as WebBrowser from 'expo-web-browser'
import * as Linking from 'expo-linking'
import { useSSO } from '@clerk/expo'
import { useRouter } from 'expo-router'
import { Pressable, StyleSheet, Text, View } from 'react-native'
const useWarmUpBrowser = () => {
React.useEffect(() => {
void WebBrowser.warmUpAsync()
return () => {
void WebBrowser.coolDownAsync()
}
}, [])
}
WebBrowser.maybeCompleteAuthSession()
export function BrowserGoogleSignIn() {
useWarmUpBrowser()
const { startSSOFlow } = useSSO()
const router = useRouter()
const handleSignIn = async () => {
try {
const { createdSessionId, setActive } = await startSSOFlow({
strategy: 'oauth_google',
redirectUrl: Linking.createURL('/dashboard', { scheme: 'myapp' }),
})
if (createdSessionId && setActive) {
await setActive({ session: createdSessionId })
router.replace('/')
}
} catch (err) {
console.error('Browser OAuth error:', JSON.stringify(err, null, 2))
}
}
return (
<View>
<Pressable style={styles.button} onPress={handleSignIn}>
<Text style={styles.text}>Sign in with Google</Text>
</Pressable>
</View>
)
}
const styles = StyleSheet.create({
button: {
backgroundColor: '#4285F4',
padding: 14,
borderRadius: 8,
alignItems: 'center',
},
text: { color: '#fff', fontWeight: '600', fontSize: 16 },
})The useWarmUpBrowser hook pre-loads the browser on Android for a snappier open. WebBrowser.maybeCompleteAuthSession() handles the redirect when the app resumes.
Several libraries support this pattern. expo-auth-session provides low-level OAuth utilities. react-native-app-auth wraps AppAuth for both platforms. Auth0's React Native SDK uses browser-only OAuth. Amplify/Cognito routes through a Hosted UI in the browser. Descope uses browser-based OIDC for Expo as well.
Where browser OAuth shines
Standards-based. It works with any OAuth 2.0 provider. If the provider supports web login, it works.
Simpler initial setup. No native module configuration, no config plugins, no platform-specific client IDs. You can prototype in Expo Go (though Expo's own docs warn against relying on it for production OAuth).
Provider-agnostic. One pattern covers Google, GitHub, Discord, Notion, whatever. Same code shape for all of them.
Where browser OAuth breaks
The pain points here aren't theoretical. They're documented.
SDK 53 breakage. Expo SDK 53 broke browser-based Google OAuth. An Expo maintainer stated plainly: "we no longer maintain any libraries to support google auth" (Expo GitHub Issue #38666, 2025). The recommended fix was migrating to native sign-in. Expo's own Google authentication guide now points developers to @react-native-google-signin/google-signin instead of expo-auth-session (Expo Google Authentication Guide).
The Android DISMISS bug. As mentioned, one team reported ~30% of Android sign-in attempts returning DISMISS. The issue remains open (Expo GitHub Issue #23781, 2024). A conversion killer, plain and simple.
Deep link fragility. Android's intent system, iOS Universal Links, custom URL schemes: each has its own failure modes. A misconfigured assetlinks.json or apple-app-site-association file silently swallows redirects.
UX friction. iOS shows a system prompt asking whether to open the browser. Users see a flash of Safari. The whole thing feels like leaving the app, because they are.
How native OAuth works in Expo
Native OAuth replaces the browser redirect with platform APIs. The flow is shorter and stays inside your app:
- User taps "Sign in with Google."
- The app calls the platform's native authentication API.
- Android: Credential Manager displays an account picker. iOS:
ASAuthorizationpresents a native sheet. - User selects an account (often with biometric confirmation).
- The platform returns an ID token directly to the app.
- Your app exchanges the token for a session.
No browser. No redirect. No deep links to debug.
Native Google Sign-In
On Android, this goes through Credential Manager. On iOS, it uses Google's native SDK (which wraps ASAuthorization under the hood).
Here's native Google Sign-In with Clerk:
// components/NativeGoogleSignIn.tsx
import { useSignInWithGoogle } from '@clerk/expo/google'
import { useRouter } from 'expo-router'
import { Alert, Platform, Pressable, StyleSheet, Text } from 'react-native'
export function NativeGoogleSignIn() {
const { startGoogleAuthenticationFlow } = useSignInWithGoogle()
const router = useRouter()
if (Platform.OS !== 'ios' && Platform.OS !== 'android') return null
const handleGoogleSignIn = async () => {
try {
const { createdSessionId, setActive } = await startGoogleAuthenticationFlow()
if (createdSessionId && setActive) {
await setActive({ session: createdSessionId })
router.replace('/')
}
} catch (err: any) {
if (err.code === 'SIGN_IN_CANCELLED' || err.code === '-5') return
Alert.alert('Error', err.message || 'Google Sign-In failed')
}
}
return (
<Pressable style={styles.button} onPress={handleGoogleSignIn}>
<Text style={styles.text}>Sign in with Google</Text>
</Pressable>
)
}
const styles = StyleSheet.create({
button: {
backgroundColor: '#4285F4',
padding: 14,
borderRadius: 8,
alignItems: 'center',
},
text: { color: '#fff', fontWeight: '600', fontSize: 16 },
})The useSignInWithGoogle hook handles all the platform wiring. No expo-web-browser, no expo-linking, no redirect URL construction.
Native Apple Sign-In
Apple Sign-In is native on iOS (Face ID bottom sheet) but falls back to browser OAuth on Android. Here's a component that handles both:
// components/NativeAppleSignIn.tsx
import { useSignInWithApple } from '@clerk/expo/apple'
import { useSSO } from '@clerk/expo'
import { useRouter } from 'expo-router'
import { Alert, Platform, Pressable, StyleSheet, Text } from 'react-native'
export function NativeAppleSignIn() {
const { startAppleAuthenticationFlow } = useSignInWithApple()
const { startSSOFlow } = useSSO()
const router = useRouter()
const handleAppleSignIn = async () => {
try {
if (Platform.OS === 'ios') {
const { createdSessionId, setActive } = await startAppleAuthenticationFlow()
if (createdSessionId && setActive) {
await setActive({ session: createdSessionId })
router.replace('/')
}
} else {
const { createdSessionId, setActive } = await startSSOFlow({
strategy: 'oauth_apple',
})
if (createdSessionId && setActive) {
await setActive({ session: createdSessionId })
router.replace('/')
}
}
} catch (err: any) {
if (err?.message?.includes('ERR_REQUEST_CANCELED')) return
if (err?.code === 'ERR_CANCELED') return
Alert.alert('Error', err.message || 'Apple Sign-In failed')
}
}
return (
<Pressable style={styles.button} onPress={handleAppleSignIn}>
<Text style={styles.text}>Continue with Apple</Text>
</Pressable>
)
}
const styles = StyleSheet.create({
button: {
backgroundColor: '#000',
padding: 14,
borderRadius: 8,
alignItems: 'center',
},
text: { color: '#fff', fontWeight: '600', fontSize: 16 },
})Platform detection routes iOS to the native ASAuthorization sheet and Android to browser-based OAuth. Your users don't know or care about the difference.
Native library options
Going native means picking how much you want to assemble yourself.
DIY multi-library approach. You can wire @react-native-google-signin/google-signin (the Credential Manager version is paywalled behind a sponsor license), expo-apple-authentication, and expo-crypto together manually. It works. It's a lot of plumbing.
Supabase supports native Apple and Google, but you're wiring 3+ packages together yourself (Apple guide, Google guide).
Firebase has a similar story. @react-native-firebase/auth handles the token exchange, but you're still configuring native modules individually (Firebase social auth docs).
Stytch offers a React Native SDK with native Google (Android via Credential Manager) and Apple (iOS) support, but it doesn't include an Expo config plugin and doesn't support native Google Sign-In on iOS (Stytch Mobile SDKs).
Clerk bundles native Google and Apple into a single package with an Expo config plugin. useSignInWithGoogle and useSignInWithApple handle platform detection and token exchange. AuthView (currently in beta) goes further, rendering a complete sign-in UI with native providers built in. (Native Google docs, Native Apple docs)
Where native OAuth shines
No redirect chain. The entire flow happens inside your app. No deep links to misconfigure, no DISMISS race conditions, no "Open in Safari?" prompts. Redirect-related failure modes don't apply because there's no redirect.
Faster. Credential Manager on Android often auto-selects the user's Google account, skipping the consent screen entirely. Fewer steps, fewer network round-trips.
Platform-native feel. The account picker and biometric prompts look like they belong. Because they do.
The passkey bridge
Here's where native gets interesting beyond just OAuth. The same platform APIs that power native sign-in also power passkeys.
Android's Credential Manager is a single API surface for both Google Sign-In and passkeys (Android Credential Manager docs). iOS's ASAuthorizationController handles Apple Sign-In and passkeys through the same framework (Apple ASAuthorizationController docs). Apple's iOS 26 Account Creation API pushes this further, unifying account creation and passkey enrollment (WWDC 2025, Session 279).
Browser OAuth and native passkeys operate in completely different layers. In-app browser tabs during an OAuth redirect don't participate in the native passkey ecosystem (Corbado Native App Passkeys, 2025). So if passkeys are on your roadmap (and they probably should be), native OAuth puts you on the right foundation.
The numbers back this up. More than 15 billion online accounts can now use passkeys (FIDO Alliance, 2025). 69% of users have created at least one passkey (FIDO Alliance, 2025). And 87% of businesses are actively deploying passkeys (FIDO Alliance Passkey Index, 2025).
For Expo passkey support, Clerk offers @clerk/expo-passkeys (Clerk Expo Passkeys docs). The community react-native-passkeys package (GitHub) is another option. Both require development builds; Expo Go can't access the native APIs (Authsignal Blog, 2025).
Here's the minimal Clerk passkey code:
import { useUser, useSignIn } from '@clerk/expo'
// Creating a passkey
const { user } = useUser()
await user.createPasskey()
// Signing in with a passkey
const { signIn } = useSignIn()
await signIn.authenticateWithPasskey({ flow: 'discoverable' })Two calls. No browser, no redirect, no deep links.
Side-by-side comparison
Here's how the three approaches stack up across the dimensions that matter:
The core call for each approach boils down to one line.
Browser OAuth:
const result = await startSSOFlow({
strategy: 'oauth_google',
redirectUrl: Linking.createURL('/dashboard', { scheme: 'myapp' }),
})Native hooks:
const result = await startGoogleAuthenticationFlow()AuthView (beta):
<AuthView mode="signInOrUp" />Native approaches require an Expo config plugin. Here's the relevant app.json configuration:
{
"expo": {
"plugins": ["expo-router", "expo-secure-store", "expo-apple-authentication", "@clerk/expo"]
}
}The @clerk/expo plugin handles the native Google Sign-In configuration. expo-apple-authentication enables the Apple Sign-In entitlement. Neither plugin works in Expo Go, which brings us to the next section.
Expo Go vs. development builds
Expo Go is great for prototyping. Tap a QR code, see your app. But it can't run custom native code, and native OAuth requires custom native code.
Expo Go limitations for OAuth:
- No config plugins. The
@clerk/expoandexpo-apple-authenticationplugins need to modify native project files. Expo Go ships with a fixed set of native modules. - Browser OAuth works in Expo Go, but with caveats. Expo's own documentation warns that OAuth in Expo Go is unreliable for production use.
- No passkey support.
@clerk/expo-passkeysandreact-native-passkeysboth need native modules that Expo Go doesn't include.
Development builds are the answer. They're custom versions of Expo Go that include your project's native modules. The workflow change is minimal:
- Run
npx expo prebuildto generate native projects. - Run
npx expo run:iosornpx expo run:androidinstead ofnpx expo start. - You still get hot reload, the dev menu, and fast iteration.
The trade-off is real but small. You lose the QR-code-and-go simplicity of Expo Go. You gain access to every native API your app needs. For any app shipping to the App Store or Play Store, you'll need development builds anyway.
When browser OAuth still makes sense: Early prototyping in Expo Go. Supporting providers that don't offer native SDKs (Discord, GitHub, Notion). Web platform targets where native APIs don't exist.
App Store compliance
Apple and Google both have opinions about how authentication should work in mobile apps. Getting these wrong means rejection.
Apple's Guidelines
Guideline 4.8 requires that apps offering third-party social login must also offer Sign in with Apple as an option. Exceptions exist: apps that exclusively use your company's own account system, education or enterprise apps using existing credentials, government or industry-backed identity systems, and apps that are clients for a specific third-party service (e.g., a Gmail client doesn't need Apple Sign-In) (Apple Developer, 2025).
Guideline 4.0 (Design) is trickier. Apple has rejected apps that present authentication through an embedded web view or browser redirect when a native experience is expected. Auth0 users have reported App Store rejections tied to their browser-based login flow (Auth0 Community, 2024). AWS Amplify users have seen similar issues (Amplify GitHub Issue #13668, 2024). The pattern: Apple reviewers flag browser-based login as a substandard experience.
Native Sign in with Apple removes the browser-redirect UX that triggered these rejections. The Face ID bottom sheet aligns with what Apple reviewers expect to see.
Google's WebView policy
Google blocks OAuth sign-in from embedded WebViews (not system browser tabs, but actual WebView components). This has been policy since 2021. expo-web-browser uses SFSafariViewController on iOS and Chrome Custom Tabs on Android, which are allowed. But if you're using a raw WebView, Google will block the request.
The RFC 8252 nuance
RFC 8252 recommends external browser tabs (not WebViews) as the secure pattern for mobile OAuth. This is what expo-web-browser implements. Native OAuth sidesteps the entire redirect pattern, which means RFC 8252's guidance about redirect URIs, PKCE, and browser security doesn't directly apply. Native APIs handle security through platform attestation and the Credential Management API instead.
Both approaches comply with platform requirements. Native just happens to also satisfy the "native experience" preference that app store reviewers increasingly expect.
Decision framework
When to use browser OAuth
- You're prototyping in Expo Go and need quick social login.
- You support providers without native SDKs (GitHub, Discord, Notion, LinkedIn).
- Your app targets the web in addition to iOS/Android.
- You want the simplest possible initial setup.
- You're using a provider that only supports browser flows (Descope, Amplify Hosted UI).
When to use native OAuth
- You're shipping to the App Store or Play Store (you're already building dev builds).
- Google and Apple are your primary social login providers.
- You want to avoid the redirect-related failure modes documented on Android.
- Passkeys are on your roadmap.
- You want the fastest possible sign-in UX.
- App Store reviewers have flagged your browser-based login.
The "start browser, migrate native" path
Many teams start with browser OAuth during early development, then migrate to native before launch. That's a reasonable approach. Here's what the migration looks like:
- Set up a development build. Run
npx expo prebuildand switch fromnpx expo starttonpx expo run:ios/npx expo run:android. - Add config plugins. Add
@clerk/expo,expo-apple-authentication, andexpo-secure-storeto the plugins array inapp.json. - Configure platform credentials. Set up Google OAuth client IDs for iOS and Android in the Google Cloud Console. Enable Sign in with Apple in your Apple Developer account.
- Swap the hook calls. Replace
useSSOwithuseSignInWithGoogleanduseSignInWithApple. Removeexpo-web-browserandexpo-linkingimports. - Add platform fallbacks. For providers that don't have native SDKs, keep
useSSOas the fallback. - Test on physical devices. Credential Manager and
ASAuthorizationbehave differently in simulators.
Provider comparison (verified March 2026)
Clerk is the only provider in this comparison with an Expo config plugin that handles both Google and Apple native sign-in from a single package. Supabase and Firebase support native flows but require assembling multiple packages manually. Provider capabilities and free tier limits change; check each provider's current pricing page before making commitments.