
Expo Google Sign-In Without a WebView: The Native Approach Using Clerk
This is the first part of a two-part series on implementing native Google Sign-In in Expo apps. This part covers the shift from browser-based OAuth to native sign-in, prerequisites, setting up Google Cloud and the Clerk Dashboard, initializing the Expo project, and implementing Google Sign-In using the pre-built <AuthView /> component.
Google Sign-In in Expo apps has always meant browser redirects, custom URL schemes, and a fragile chain of callbacks. Clerk's native Google Sign-In changes that. On Android, it uses Credential Manager — no browser at all. On iOS, configuring the EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME environment variable enables ASAuthorization, Apple's native credential picker, instead of the default system browser sheet. With both platforms configured, the user taps one button, picks their Google account from a system-level sheet, and they're signed in.
This guide walks through the complete setup: Google Cloud credentials, Clerk Dashboard configuration, and a working Expo app with native Google Sign-In, email+OTP authentication, user profile management, and sign-out. Every code example targets @clerk/expo Core 3 and the current stable Expo SDK.
What Is Native Google Sign-In and Why It Matters for Expo Apps
Browser-Based OAuth: The Standard Approach and Its Problems in Expo
The standard OAuth flow in Expo uses expo-auth-session to open a system browser (ASWebAuthenticationSession on iOS, Chrome Custom Tabs on Android). The user authenticates in that browser and gets redirected back to the app via a deep link.
This works, but the failure modes are real:
- Redirect handling breaks. Different callback URIs for development, preview, and production. One mismatch and the user lands nowhere.
- Android dismiss race conditions. Developers have reported Android redirect reliability issues where the browser dismisses before the callback completes (expo/expo#23781).
- SDK upgrades break auth. Expo SDK 53 introduced regressions in Google login flows that affected existing
expo-auth-sessionimplementations (expo/expo#38666). - The
auth.expo.ioproxy is gone. The Google provider that relied on it has been deprecated since SDK 49 (expo/expo#21084).
Google blocked OAuth from embedded WebViews on September 30, 2021, returning disallowed_useragent errors (Google Developers Blog, Jun 2021). Google continued enforcing this policy through 2023: remaining apps using embedded WebViews saw warnings starting in February 2023, with final blocking on July 24, 2023 (Google Support FAQ). The system browser approach (expo-auth-session) was never blocked, but it still opens a browser. Native sign-in avoids a browser entirely.
Three tiers of Google authentication exist in mobile apps:
- Embedded WebView (blocked by Google since 2021)
- System browser via ASWebAuthenticationSession/Chrome Custom Tabs (what
expo-auth-sessiondoes) - Native credential picker via ASAuthorization/Credential Manager (what Clerk's native flow does)
This article covers tier 3: no browser at all.
What Native Google Sign-In Actually Is
ASAuthorization on iOS
Apple's ASAuthorization framework presents a system-level credential picker, the same UI used for passkeys and Sign in with Apple. When Clerk's @clerk/expo config plugin is configured with an iOS URL scheme (EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME), useSignInWithGoogle() uses ASAuthorization to present the native Google account picker. No browser opens.
This configuration step is optional. Without it, iOS falls back to ASWebAuthenticationSession, which opens a system browser sheet. The difference is a single environment variable.
Credential Manager on Android
Credential Manager is Google's Jetpack library (androidx.credentials) that surfaces a system bottom sheet with the user's Google accounts. No browser opens. An ID token is produced directly by the OS.
Credential Manager replaces 5 deprecated APIs: the legacy Google Sign-In SDK (play-services-auth), Smart Lock for Passwords, One Tap sign-in, the Sign in with Google button, and FIDO2 local credentials. SDK removal is scheduled for May 2026; API calls will fail as early as July 2028 (Android Developers Blog, Sep 2024).
Why Native Sign-In Is Better Than Browser-Based OAuth
User experience. No context switch. No redirect failures. No browser tab left open. The conversion numbers back this up:
- Pinterest saw a 126% sign-up increase on Android after adopting Google One Tap (Google Case Study).
- Reddit reported a 185% overall conversion increase combining Sign in with Google and One Tap (Google Case Study).
- Zoho achieved 6x faster logins after migrating to Credential Manager, with 31% month-over-month passkey adoption growth (Android Developers Blog, May 2025).
Security. The native flow runs in a sandboxed system process that the app can't intercept. No redirect URI to spoof. No PKCE complexity exposed to the developer. RFC 8252 (IETF BCP 212) states that native apps "MUST NOT use embedded user-agents" for OAuth. The OAuth 2.1 draft (March 2026) makes PKCE mandatory for all clients.
Reliability. No auth.expo.io proxy dependency. No dismiss race conditions on Android. No production-vs-development differences in redirect handling.
Prerequisites and Requirements
Tools and Accounts You Need
- Node.js 20.9.0+
- Expo CLI (
npx expo) - EAS CLI (
npm install -g eas-cli) and an Expo account - A Clerk account
- A Google Cloud Console account
- iOS: Xcode 16+, Apple Developer account (for device testing)
- Android: Android Studio, physical device or emulator with Google Play Services
Environment Variable Checklist
Your .env file needs these values (collected during the setup steps below):
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
EXPO_PUBLIC_CLERK_GOOGLE_WEB_CLIENT_ID=...
EXPO_PUBLIC_CLERK_GOOGLE_IOS_CLIENT_ID=...
EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME=com.googleusercontent.apps...
EXPO_PUBLIC_CLERK_GOOGLE_ANDROID_CLIENT_ID=...Compatibility: Clerk Core 3, @clerk/expo, and Expo SDK
- All code uses Core 3 import paths:
@clerk/expo,@clerk/expo/google,@clerk/expo/native - Native Google Sign-In requires
@clerk/expo3.1+ - Native components (
AuthView,UserButton,UserProfileView) require Expo SDK 53+ - React 18 or 19
Development Build vs. Expo Go
Native Google Sign-In requires a development build because Expo Go ships a fixed native layer that can't load custom TurboModules like NativeClerkGoogleSignIn.
To create a development build:
npx expo install expo-dev-client
npx expo run:iosOr use EAS Build:
eas build --profile development --platform iosHow to Add Google Sign-In to an Expo App Without a Browser Redirect
Three main approaches exist. Here's how they compare:
Option 1: Manual OAuth with expo-auth-session
The browser-based approach. Opens a system browser, handles the OAuth redirect, and returns a token. You manage session creation, token storage, and refresh yourself. Every Expo SDK upgrade risks breaking the redirect chain.
Option 2: @react-native-google-signin/google-signin (DIY)
A React Native library that wraps Google's native SDKs. Gives you the native Google UI, but you still own session management, user state, and sign-out logic. Its modern Credential Manager integration ships only in the paid Universal Sign In tier ($79–$249/year); the free version is limited to the deprecated legacy Android Sign-In SDK.
Option 3: Clerk Native Google Sign-In (Recommended)
Native Google Sign-In is built into @clerk/expo. On Android it uses Credential Manager. On iOS, the native path uses ASAuthorization when configured. Clerk handles the token exchange, session creation, and signed-in state after the provider returns.
Why Clerk Is the Right Choice for Expo Authentication
- Fewest moving parts. Dashboard config, environment variables, one hook or component. That's it.
- Pre-built native UI.
<AuthView />renders SwiftUI on iOS and Jetpack Compose on Android. Google, Apple, email, phone, passkeys, and MFA are handled automatically. - Uses Google's current recommended APIs. Credential Manager (not the deprecated legacy SDK).
- Built-in session management. User profiles, sign-out, and token refresh come included.
- Automatic transfer flow. If someone signs in with Google but doesn't have an account, one is created. If they sign up but already have an account, they're signed in. No separate screens needed.
- Minimal dependency surface.
@clerk/expowith its peer dependencies (expo-secure-store,expo-auth-session,expo-web-browser) plusexpo-cryptofor the hook approach. AuthView doesn't needexpo-crypto.
Clerk's native flow still goes through Clerk's backend for token verification and session creation. For how this works under the hood, see How Clerk Works.
Setting Up Clerk for Native Google Sign-In
Step 1: Create a Clerk Application and Enable Native API
- Go to the Clerk Dashboard and create a new application (or select an existing one).
- Navigate to the Native Applications page and confirm that Native API is enabled.
- Copy your Publishable Key from the API Keys page.
Native components and native sign-in hooks depend on Native API being enabled. Skip this and every native call silently fails.
Step 2: Enable Google and Register Native Applications
- In the Clerk Dashboard, go to Social Connections > Google > Use custom credentials.
- You'll configure the Client IDs here after creating them in Google Cloud Console (next step).
- On the Native Applications page:
- iOS: Add your Team ID and Bundle ID (must match
ios.bundleIdentifierinapp.config.ts) - Android: Add your package name (must match
android.packageinapp.config.ts) and SHA-256 certificate fingerprint
- iOS: Add your Team ID and Bundle ID (must match
Step 3: Create a Google Cloud Project and OAuth Credentials
OAuth Consent Screen
Before creating client IDs, Google Cloud asks you to configure an OAuth consent screen.
Common blockers at this step:
- The app is still in testing mode (only test users can authenticate)
- Your Google account isn't listed as a test user
- You haven't completed production publishing for broader access
Set the consent screen to "External" and add your own email as a test user. You can publish to production later.
iOS Client ID
- Go to Google Cloud Console > APIs & Services > Credentials > Create OAuth Client ID
- Application type: iOS
- Bundle ID: must match your
app.config.tsios.bundleIdentifierexactly - Save and note:
- The iOS Client ID (goes into
EXPO_PUBLIC_CLERK_GOOGLE_IOS_CLIENT_ID) - The reversed client ID (goes into
EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEMEfor the native callback path)
- The iOS Client ID (goes into
Android Client ID and SHA-1 Fingerprint
- Application type: Android
- Package name: must match your
app.config.tsandroid.package - SHA-1 fingerprint: get it from your signing keystore
Three different SHA-1 values exist depending on how you build. For local development, read it from the Android debug keystore:
# Debug keystore (local development)
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass androidWhen EAS manages your Android signing, pull the fingerprint from your EAS credentials instead:
# EAS managed keystore
eas credentials --platform androidFor production, find the App Signing key SHA-1 in Google Play Console > Release > Setup > App Integrity.
- Create a Web Application OAuth Client ID too. Clerk uses it server-side for token verification. Add the Authorized Redirect URI from the Clerk Dashboard to this web client.
Configuration Validation Checklist
Before your first build, confirm:
- Bundle ID matches in Google Cloud Console, Clerk Native Applications, and
app.config.ts - Android package name matches in Google Cloud Console, Clerk Native Applications, and
app.config.ts - Web, iOS, and Android Client IDs are in the correct environment variables
- SHA-1 is registered in Google Cloud Console for each Android signing identity
- SHA-256 is registered in the Clerk Dashboard Native Applications page for each Android signing identity
- iOS reversed client ID is used as
EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME - Native API is enabled on the Clerk Dashboard Native Applications page
Building the Complete Expo App
Project Initialization
npx create-expo-app expo-clerk-google-signin
cd expo-clerk-google-signinInstalling Dependencies
For the hook approach (custom UI, used in the complete app below):
npx expo install @clerk/expo expo-secure-store expo-crypto expo-auth-session expo-web-browser expo-dev-clientFor the AuthView approach (pre-built native UI):
npx expo install @clerk/expo expo-secure-store expo-auth-session expo-web-browser expo-dev-clientAuthView doesn't need expo-crypto or useSignInWithGoogle. It handles everything internally. The expo-auth-session and expo-web-browser packages are peer dependencies of @clerk/expo and are required even when using native components.
Configuring app.config.ts
import { ExpoConfig } from 'expo/config'
const config: ExpoConfig = {
name: 'expo-clerk-google-signin',
slug: 'expo-clerk-google-signin',
version: '1.0.0',
scheme: 'expo-clerk-google-signin',
ios: {
bundleIdentifier: 'com.yourcompany.expoclerkgooglesignin',
supportsTablet: true,
},
android: {
package: 'com.yourcompany.expoclerkgooglesignin',
adaptiveIcon: {
foregroundImage: './assets/adaptive-icon.png',
backgroundColor: '#ffffff',
},
},
plugins: ['@clerk/expo'],
extra: {
EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME: process.env.EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME,
},
}
export default configThe @clerk/expo config plugin auto-injects the clerk-ios (Swift) and clerk-android (Kotlin) native SDKs, sets the iOS deployment target to 17.0, and configures the iOS URL scheme for the native Google callback.
Setting Up ClerkProvider in Your App Entry Point
// app/_layout.tsx
import { ClerkProvider } from '@clerk/expo'
import { tokenCache } from '@clerk/expo/token-cache'
import { Slot } from 'expo-router'
const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!
if (!publishableKey) {
throw new Error('Add EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY to your .env file')
}
export default function RootLayout() {
return (
<ClerkProvider publishableKey={publishableKey} tokenCache={tokenCache}>
<Slot />
</ClerkProvider>
)
}The tokenCache uses expo-secure-store under the hood, which encrypts session tokens before storing them on the device. Without it, Clerk stores the active session token in memory only and it won't persist across app restarts.
The publishableKey must be passed explicitly because environment variables aren't automatically inlined in React Native production builds the way they are on web.
Using the Native AuthView Component for Google Sign-In
<AuthView /> is the fastest way to add Google Sign-In. It renders SwiftUI on iOS and Jetpack Compose on Android, with every auth method enabled in your Clerk Dashboard available automatically. Zero auth code required.
// app/(auth)/sign-in.tsx
import { AuthView } from '@clerk/expo/native'
import { useAuth } from '@clerk/expo'
import { useRouter } from 'expo-router'
import { useEffect } from 'react'
import { View, StyleSheet } from 'react-native'
export default function SignInScreen() {
const { isLoaded, isSignedIn } = useAuth({ treatPendingAsSignedOut: false })
const router = useRouter()
useEffect(() => {
if (isLoaded && isSignedIn) {
router.replace('/(home)')
}
}, [isLoaded, isSignedIn])
return (
<View style={styles.container}>
<AuthView mode="signInOrUp" />
</View>
)
}
const styles = StyleSheet.create({
container: { flex: 1 },
})AuthView fills its parent container. Style the parent View to control size and position.
When you read auth state next to <AuthView />, pass { treatPendingAsSignedOut: false } to useAuth(), matching Clerk's native-component examples. By default, a pending session — for example, when Organizations are enabled and the user hasn't selected one yet — counts as signed-out, so the redirect would stall until the session became fully active. <AuthView /> resolves those session tasks in its own native UI, so treating a pending session as signed-in is the right behavior here. The isLoaded guard keeps the effect from acting on auth state before Clerk has hydrated from the token cache.
Customizing the AuthView Appearance
Three modes are available:
signIn: sign-in flows onlysignUp: sign-up flows onlysignInOrUp: auto-determines based on whether an account exists (default)
The isDismissible prop controls whether the native dismiss button appears; it defaults to true. Set it to false to force a required, non-dismissible flow. When you present <AuthView /> inside a React Native <Modal>, wire its onDismiss callback (and the Modal's onRequestClose) to your visibility state so the dismiss button closes the modal. Keep the <Modal> mounted at the same level as both your signed-in and signed-out UI — if it lives inside a branch that unmounts when auth state changes, the modal can disappear mid-flow before the transition completes.
Which social login providers appear is controlled entirely by your Clerk Dashboard configuration. Enable Google, Apple, or any other provider there, and AuthView picks it up automatically.
In the next part of this series, we will explore building a custom authentication UI using the useSignInWithGoogle hook, combining Google Sign-In with Email/OTP authentication, managing user profiles and sessions, and handling production deployments and errors.
Frequently Asked Questions
In this series
- Expo Google Sign-In Without a WebView: The Native Approach Using Clerk (you are here)
- Expo Google Sign-In Without a WebView: The Native Approach Using Clerk - Part 2