EXPO_TOKEN environment variable for EAS CLI authentication
Access tokens can be used with EAS CLI by setting the EXPO_TOKEN environment variable before running commands, for example: EXPO_TOKEN=my_token eas build. The EXPO_TOKEN authentication method takes precedence over username and password if both are configured. The eas login command is only used for username and password authentication.
Revoking access tokens to block account access
If an access token is accidentally leaked, it can be revoked without changing the username and password. Revoking the token blocks all access to the account using that token. To revoke a token, go to the Access Token page on the Expo dashboard and delete the token.
Access tokens for CI and script automation
Access tokens are recommended for CI pipelines and scripts instead of username and password credentials. Tokens allow managing each integration point separately and can be revoked independently if compromised. They should be treated with the same care as user passwords.
Personal access tokens for account management
Personal access tokens can be created from the Access tokens page on the Expo dashboard. Anyone with a personal access token can perform actions on behalf of the token owner, including all content on their personal account and any personal accounts or organizations they have been granted access to.
Robot users for account-owned resources
Accounts can create robot users to take actions on resources owned by the account. Robot users can be assigned a role to limit their authorized actions. Robot users cannot sign in to any Expo products, cannot own projects themselves, and can only authenticate via an access token.
Magic links require deep linking
With magic links, the user receives an email containing a link that redirects them back into the app. A key detail is deep linking: since users leave the app to check their email, the link must open the app and route them to the correct screen. If deep linking fails, the session cannot be validated and the login flow breaks. Expo Router handles deep linking automatically for most cases, so you usually don't need to configure anything extra to make magic links work properly.
One-time passcode (OTP) authentication
An alternative to magic links is sending a one-time passcode by email or SMS. Instead of clicking a link, the user copies the code and manually returns to the app to enter it. This must happen within a specific time window before the code expires. There is no deep linking involved—the user stays in control of the flow. Newer versions of Android and iOS automatically detect passcodes in incoming messages, enabling autofill suggestions above the keyboard for seamless entry.
Magic links and OTP valid for app store review
Magic links and passcodes are both valid authentication methods for Google Play Store and Apple App Store reviews. You can submit your app with either of these methods as the only option and get approved, even before adding social or OAuth login options.
OAuth 2.0 definition and benefits
OAuth 2.0 is a widely used, secure protocol that allows an app to access user information from another service without needing to handle passwords. It allows users to log in using their existing accounts from services like Google, Apple, and GitHub with a single tap, which saves time, builds trust, and removes the need to manage passwords.
OAuth flow with authorization server
OAuth works by introducing an authorization server that acts as a secure middleman. Instead of giving the app their password, users log in through this server and approve access to specific data (like their name or email). The server then issues a temporary code, which the app can exchange for a secure access token.
Custom OAuth with Expo API Routes
The preferred method for a client to obtain an authorization grant is to use an authorization server as an intermediary, which can be built using Expo API Routes. This setup allows you to: start the login flow using AuthSession, receive the auth code in your API Route, exchange the code for a token securely, generate a custom JWT with your own logic, return that token to the client, and store sessions using cookies (web) or JWTs (native). This can be deployed instantly using EAS Hosting (free to start).
Expo API Routes for OAuth
Expo Router API Routes allow you to write server-side logic directly inside your Expo app. You can define functions that handle requests like an Express or Next.js backend, with no need for an external server. This makes it easy to securely handle sensitive parts of the auth flow, like authorization code exchange, directly within your app. Since these routes run on the server, you can safely manage secrets, issue JWTs, and validate tokens.
Expo AuthSession for OAuth login flow
Expo AuthSession is a client-side package that helps you open a web browser or native modal to start the OAuth login flow. It handles redirection, parses the authorization response, and brings the user back into the app. It is the tool that kicks off the flow and talks to your API Route after the user authorizes access.
Native OAuth implementations
Some providers offer native APIs to handle sign-in flow directly within the app. Google offers a native Sign in with Google experience on Android. Apple provides Sign in with Apple, which uses a native bottom sheet and Face ID on iOS. These are available through expo-apple-authentication and related Google authentication guides.
Session management with cookies and JWTs
After receiving the ID token from a provider like Google or Apple, you generate a custom JWT on the server using Expo API Routes. For Android and iOS apps, the token can be stored securely using expo-secure-store. For web apps, it can be set as a secure cookie to maintain the session. On every request, the token is sent back to the server, where the signature is verified and the expiration is checked. This session model keeps the backend stateless, scalable, and secure.
JWT customization with Expo API Routes
When generating a custom JWT on the server using Expo API Routes, you have full control over the session, including: structuring the payload using consistent fields across providers, customizing expiration times, and signing the token with a secret key so your server can verify it later.
Better Auth Expo integration
BetterAuth is a modern, open-source authentication provider built for developers. It integrates smoothly with Expo, and they offer a guide showing how to use it with Expo API Routes for full control. It works well with any provider and deploys easily with EAS Hosting.
Clerk Expo authentication support
Clerk is a powerful, full-featured authentication service with excellent Expo support. It includes email/password, passcodes, magic links, OAuth providers, and passkeys. Clerk offers a native Expo module that handles much of the integration automatically.
Supabase authentication with Expo
Supabase provides a full backend platform, including a built-in authentication service that works with any OAuth provider. It integrates well with Expo apps and includes support for email, magic links, and more.
AWS Cognito with Expo
AWS Cognito is Amazon's solution for managing user pools and identity. It connects seamlessly with other AWS services and can be integrated into Expo apps using AWS Amplify. It requires more configuration, but it is robust and scalable.
Firebase Authentication with Expo
Firebase Authentication is Google's auth platform and supports email, magic links, and OAuth providers. It works with React Native through react-native-firebase, which is compatible with Expo development builds.
Biometrics for authentication enhancement
Biometrics like Face ID and Touch ID can be used to unlock the app or confirm identity after a valid session is established. These are not authentication methods on their own, but act as a local gate that makes re-authentication faster and more secure. React Native provides access to biometric APIs through libraries like expo-local-authentication or react-native-biometrics.
Authentication implementation strategy
Starting simple is often the best approach. Shipping your app with something like email authentication using a magic link or one-time passcode is often more than enough to get through the App Store review process and start collecting feedback from real users. Modern solutions like OAuth, biometrics, and passkeys are not required, but they can be excellent additions once your core system is in place. The key is to build authentication that fits your current needs while staying flexible enough to grow with your product.
Passkeys for passwordless authentication
Passkeys are a new, passwordless way to log in to apps and websites. Backed by Apple, Google, and Microsoft, they use platform-level cryptography and biometrics to authenticate users without passwords. Passkeys offer a seamless and secure experience, but they require a user to already be authenticated before registering one. They also require extra configuration if you are not using a provider that handles them. React Native passkey support is available through react-native-passkeys, and Clerk offers Passkeys for Expo.
Expo Router protected routes vs redirects
Expo Router v5 introduced protected routes, which prevent users from accessing certain screens unless they are authenticated. This feature works well for client-side navigation and simplifies setup. If using an older version of Expo Router, redirects can be used instead and provide the same result but require more manual configuration. Redirects are still supported in Expo Router v5 for backward compatibility.
Authentication as navigation level check
Any authentication system needs to separate public screens (such as login or signup) from protected screens (such as home or profile). At the navigation level, it comes down to a simple check: is the user authenticated? This can be simulated using a hardcoded boolean value during development, then replaced with a real authentication flow.
Email and password authentication services
Services offering built-in email and password authentication include Clerk, Supabase, Cognito, Firebase, and Better Auth. Most of these have generous free tiers. The biggest advantage of these services is their ease of integration, as they usually offer clear documentation, starter kits, and prebuilt components.
Email and password sufficient for app store review
Adding email and password authentication is usually enough to pass App Store and Play Store review. However, if you include 'Sign in with Google,' Apple may reject your app unless you also support 'Sign in with Apple.' The same rule applies in reverse on Google Play—if you include 'Sign in with Apple,' you must support alternatives.
Disable prompt until request is defined
Be sure to disable the prompt until request is defined in AuthSession.useAuthRequest().
WebBrowser.maybeCompleteAuthSession() required for web popup dismissal
Call WebBrowser.maybeCompleteAuthSession() to dismiss the web popup when using the AuthSession API. If you forget to add this then the popup window will not close. This method should be invoked on the page that the auth popup gets redirected to on web; on native this does nothing.
AuthSession.makeRedirectUri() for universal platform support
Create redirects with AuthSession.makeRedirectUri() which handles the heavy lifting involved with universal platform support. Behind the scenes, it uses expo-linking.
AuthSession.useAuthRequest() hook for building auth requests
Build requests using AuthSession.useAuthRequest(), the hook allows for async setup which means mobile browsers won't block the authentication.
promptAsync() only invokable in user interaction on web
You can only invoke promptAsync in user interaction on the web, otherwise browsers will block it.
Development Build required for OAuth testing instead of Expo Go
Expo Go cannot be used for local development and testing of OAuth or OpenID Connect-enabled apps due to the inability to customize your app scheme. You can instead use a Development Build, which enables an Expo Go-like development experience and supports OAuth redirects back to your app after login in a manner that works just like it would in production.
OAuth 2 authorization code grant flow overview
Most providers use the OAuth 2 standard for secure authentication and authorization. In the authorization code grant, the identity provider returns a one-time code. This code is then exchanged for the user's access token.
Client application code is not secure for storing secrets
Your client application code is not a secure place to store secrets. It is necessary to exchange the authorization code in a server such as with API routes or React Server Components. This will allow you to securely store and use a client secret to access the provider's token endpoint.
GitHub OAuth config details
GitHub OAuth 2.0 provider details: Supports PKCE, auto discovery not available. Provider only allows one redirect URI per app, requiring an individual app for every method: standalone/development build uses com.your.app:/*, web uses https://yourwebsite.com/*. The redirectUri requires two slashes (://). revocationEndpoint is dynamic and requires your config.clientId at https://github.com/settings/connections/applications/<CLIENT_ID>.
Okta OpenID config details
Okta OpenID provider details: Supports PKCE, auto discovery available. You cannot define a custom redirectUri; Okta will provide you with one.
GitHub auth example with useAuthRequest
Example showing GitHub OAuth authentication:
```tsx
import { useEffect } from 'react';
import * as WebBrowser from 'expo-web-browser';
import { makeRedirectUri, useAuthRequest } from 'expo-auth-session';
import { Button } from 'react-native';
WebBrowser.maybeCompleteAuthSession();
const discovery = {
authorizationEndpoint: 'https://github.com/login/oauth/authorize',
tokenEndpoint: 'https://github.com/login/oauth/access_token',
revocationEndpoint: 'https://github.com/settings/connections/applications/<CLIENT_ID>',
};
export default function App() {
const [request, response, promptAsync] = useAuthRequest(
{
clientId: 'CLIENT_ID',
scopes: ['identity'],
redirectUri: makeRedirectUri({
scheme: 'your.app'
}),
},
discovery
);
useEffect(() => {
if (response?.type === 'success') {
const { code } = response.params;
}
}, [response]);
return (
<Button
disabled={!request}
title="Login"
onPress={() => {
promptAsync();
}}
/>
);
}
```
Okta auth example with useAutoDiscovery
Example showing Okta OpenID authentication:
```tsx
import { useEffect } from 'react';
import * as WebBrowser from 'expo-web-browser';
import { makeRedirectUri, useAuthRequest, useAutoDiscovery } from 'expo-auth-session';
import { Button, Platform } from 'react-native';
WebBrowser.maybeCompleteAuthSession();
export default function App() {
const discovery = useAutoDiscovery('https://<OKTA_DOMAIN>.com/oauth2/default');
const [request, response, promptAsync] = useAuthRequest(
{
clientId: 'CLIENT_ID',
scopes: ['openid', 'profile'],
redirectUri: makeRedirectUri({
native: 'com.okta.<OKTA_DOMAIN>:/callback',
}),
},
discovery
);
useEffect(() => {
if (response?.type === 'success') {
const { code } = response.params;
}
}, [response]);
return (
<Button
disabled={!request}
title="Login"
onPress={() => {
promptAsync();
}}
/>
);
}
```
Redirect URI patterns for standalone/development builds
Standalone/development build redirect URI pattern: yourscheme://path. In some cases there will be anywhere between 1 to 3 slashes (/). Used in existing React Native apps with npx expo prebuild, standalone builds in the App or Play Store, or testing locally with eas build or npx expo run:android/ios.
makeRedirectUri() examples for native and web platforms
Common redirect URI creation patterns using makeRedirectUri():
- your.app://redirect -> makeRedirectUri({ scheme: 'your.app', path: 'redirect' })
- your.app:/// -> makeRedirectUri({ scheme: 'your.app', isTripleSlashed: true })
- your.app:/authorize -> makeRedirectUri({ native: 'your.app:/authorize' })
- your.app://auth?foo=bar -> makeRedirectUri({ scheme: 'your.app', path: 'auth', queryParams: { foo: 'bar' } })
- exp://u.expo.dev/[project-id]?channel-name=[channel-name]&runtime-version=[runtime-version] -> makeRedirectUri()
Recommend defining the scheme property at least; the entire URL can be overridden in apps by passing the native property. Often this will be used for providers like Google or Okta which require you to use a custom native URI redirect. You can add, list, and open URI schemes using npx uri-scheme. If you change the expo.scheme, run npx expo prebuild --clean to regenerate the native projects with the new scheme, then rebuild with npx expo run:android and npx expo run:ios.
WebBrowser.warmUpAsync() for Android browser preinitialization
On Android you can optionally warm up the web browser before it's used with WebBrowser.warmUpAsync(). This allows the browser app to pre-initialize itself in the background, which can significantly speed up prompting the user for authentication.
WebBrowser.coolDownAsync() for Android memory optimization
On Android, cool down the browser when the component unmounts with WebBrowser.coolDownAsync() to help improve memory on low-end Android devices.
Browser warming example with useEffect
Example of warming and cooling down the browser on Android:
```tsx
import { useEffect } from 'react';
import * as WebBrowser from 'expo-web-browser';
function App() {
useEffect(() => {
WebBrowser.warmUpAsync();
return () => {
WebBrowser.coolDownAsync();
};
}, []);
// Do authentication ...
}
```
Implicit flow no longer recommended due to security risks
Implicit flow is no longer recommended due to inherent security risks, including the risk of access token injection. Most providers now support the authorization code with PKCE (Proof Key for Code Exchange) extension to securely exchange an authorization code for an access token within your client app code. expo-auth-session still supports Implicit flow for legacy code purposes.
Implicit flow with ResponseType.Token example
Example of legacy Implicit flow implementation:
```tsx
import { useEffect } from 'react';
import * as WebBrowser from 'expo-web-browser';
import { makeRedirectUri, useAuthRequest, ResponseType } from 'expo-auth-session';
WebBrowser.maybeCompleteAuthSession();
const discovery = {
authorizationEndpoint: 'https://accounts.spotify.com/authorize',
};
function App() {
const [request, response, promptAsync] = useAuthRequest(
{
responseType: ResponseType.Token,
clientId: 'CLIENT_ID',
scopes: ['user-read-email', 'playlist-modify-public'],
redirectUri: makeRedirectUri({
scheme: 'your.app'
}),
},
discovery
);
useEffect(() => {
if (response && response.type === 'success') {
const token = response.params.access_token;
}
}, [response]);
return <Button disabled={!request} onPress={() => promptAsync()} title="Login" />;
}
```
expo-secure-store for secure token storage on native platforms
On native platforms such as Android and iOS, you can secure things like access tokens locally using expo-secure-store. It provides native access to encrypted SharedPreferences on Android and keychain services on iOS. This is different to AsyncStorage which is not secure. There is no web equivalent to this functionality. You can store your authentication results and rehydrate them later to avoid having to prompt the user to login again.
Secure token storage example with SecureStore
Example of storing auth tokens securely using expo-secure-store:
```tsx
import * as SecureStore from 'expo-secure-store';
const MY_SECURE_AUTH_STATE_KEY = 'MySecureAuthStateKey';
function App() {
const [, response] = useAuthRequest({});
useEffect(() => {
if (response && response.type === 'success') {
const auth = response.params;
const storageValue = JSON.stringify(auth);
if (Platform.OS !== 'web') {
SecureStore.setItemAsync(MY_SECURE_AUTH_STATE_KEY, storageValue);
}
}
}, [response]);
// More login code...
}
```
Authentication in mobile apps definition
Authentication in mobile apps refers to how you identify who a user is, manage sign-up or sign-in flows, and maintain their authenticated session across app launches and across multiple devices.
Authentication SDKs purpose
Authentication SDKs and libraries help you add authentication flows to your Expo and React Native projects, so you do not need to build your own custom auth backend.
Some auth providers require custom native code and development builds
Some authentication providers require custom native code and aren't supported in Expo Go. Use a development build when needed.
Clerk native Sign in with Google setup
To add native Sign in with Google buttons to a custom flow, install @clerk/expo-google-signin and expo-crypto.
Clerk native Sign in with Apple setup
For native Sign in with Apple buttons, install expo-apple-authentication and expo-crypto.
Clerk Publishable Key environment variable
Add your Clerk Publishable Key to a .env file with the EXPO_PUBLIC_ prefix: EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_your-key-here. The EXPO_PUBLIC_ prefix is required because Expo inlines these values at build time. Clerk's Publishable Key is safe to expose. Do not put Secret Keys behind the EXPO_PUBLIC_ prefix.
ClerkProvider setup in Expo Router
Wrap your app in <ClerkProvider> in your root layout file (src/app/_layout.tsx with Expo Router) and pass the Publishable Key. Pass tokenCache explicitly from @clerk/expo/token-cache. In Core 3, publishableKey is required on <ClerkProvider> for Expo apps because environment variables inside node_modules are not inlined during production React Native builds. tokenCache persists the user's session across app restarts using expo-secure-store.
Clerk integration approaches comparison
Clerk supports three approaches: Hosted authentication (opens Clerk Account Portal in browser, runs in Expo Go, fastest setup), Native UI components (drop-in <AuthView />, <UserButton />, <UserProfileView /> from @clerk/expo/native, does not run in Expo Go, complete native UI), and Custom flow (your own screens with hooks like useSignUp() and useSignIn(), runs in Expo Go, maximum control).
@clerk/expo native UI components status
The native UI components in @clerk/expo/native are currently in beta. They render with Jetpack Compose on Android and SwiftUI on iOS and synchronize the signed-in session back to the JavaScript SDK so all @clerk/expo hooks (such as useAuth() and useUser()) stay in sync.
Clerk Expo SDK requirements
@clerk/expo Core 3 has a peer dependency of expo: >=53 <56. @clerk/expo 4.x supports Expo SDK 54 and later.
Development build required for native Clerk features
The native UI components and native sign-in hooks require a development build. Hosted authentication and custom flow approaches work in Expo Go.