expo-auth-session
Version: 55.0.4
Expo module for browser-based authentication using OAuth 2.0 and OpenID Connect. Supports authentication flows with providers like Google, Facebook, GitHub, and custom OAuth servers.
Installation
npx expo install expo-auth-session expo-web-browser expo-crypto
Usage
import * as AuthSession from 'expo-auth-session';
import * as WebBrowser from 'expo-web-browser';
import { Button } from 'react-native';
WebBrowser.maybeCompleteAuthSession();
const discovery = {
authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
tokenEndpoint: 'https://oauth2.googleapis.com/token',
};
export default function App() {
const [request, response, promptAsync] = AuthSession.useAuthRequest(
{
clientId: 'YOUR_CLIENT_ID',
redirectUri: AuthSession.makeRedirectUri({ useProxy: true }),
scopes: ['openid', 'profile', 'email'],
},
discovery
);
React.useEffect(() => {
if (response?.type === 'success') {
const { code } = response.params;
// Exchange code for token
}
}, [response]);
return (
<Button
title="Sign in with Google"
onPress={() => promptAsync()}
disabled={!request}
/>
);
}
API Reference
Hooks
useAuthRequest(config, discovery)
(config: AuthRequestConfig, discovery: DiscoveryDocument) => [AuthRequest | null, AuthSessionResult | null, (options?: AuthRequestPromptOptions) => Promise<AuthSessionResult>]
Hook for creating and managing auth requestsReturns [request, response, promptAsync]const [request, response, promptAsync] = AuthSession.useAuthRequest(
{
clientId: 'client-id',
redirectUri: AuthSession.makeRedirectUri(),
scopes: ['openid', 'profile'],
},
discovery
);
useAutoDiscovery(issuer)
(issuer: string) => DiscoveryDocument | null
Auto-discovers OAuth configuration from issuerconst discovery = AuthSession.useAutoDiscovery(
'https://accounts.google.com'
);
Methods
makeRedirectUri(options)
(options?: MakeRedirectUriOptions) => string
Generates redirect URI for your appconst redirectUri = AuthSession.makeRedirectUri({
scheme: 'myapp',
path: 'redirect',
useProxy: true,
});
exchangeCodeAsync(config, discovery)
(config: AccessTokenRequestConfig, discovery: DiscoveryDocument) => Promise<TokenResponse>
Exchanges authorization code for access tokenconst tokenResponse = await AuthSession.exchangeCodeAsync(
{
clientId: 'client-id',
code: authCode,
redirectUri,
},
discovery
);
refreshAsync(config, discovery)
(config: RefreshTokenRequestConfig, discovery: DiscoveryDocument) => Promise<TokenResponse>
Refreshes an access token using refresh tokenconst newToken = await AuthSession.refreshAsync(
{
clientId: 'client-id',
refreshToken: oldToken.refreshToken,
},
discovery
);
revokeAsync(config, discovery)
(config: RevokeTokenRequestConfig, discovery: DiscoveryDocument) => Promise<void>
Revokes an access or refresh tokenawait AuthSession.revokeAsync(
{
clientId: 'client-id',
token: accessToken,
},
discovery
);
fetchDiscoveryAsync(issuer)
(issuer: string) => Promise<DiscoveryDocument>
Fetches OAuth discovery documentconst discovery = await AuthSession.fetchDiscoveryAsync(
'https://accounts.google.com'
);
Types
AuthRequestConfig
Redirect URI for callback
OAuth response type (default: 'code')
State parameter for CSRF protection
PKCE code challenge method
TokenResponse
Refresh token (if available)
Token expiration time in seconds
Token type (usually “Bearer”)
Examples
Google Sign-In
import * as AuthSession from 'expo-auth-session';
import * as WebBrowser from 'expo-web-browser';
WebBrowser.maybeCompleteAuthSession();
const discovery = {
authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
tokenEndpoint: 'https://oauth2.googleapis.com/token',
revocationEndpoint: 'https://oauth2.googleapis.com/revoke',
};
export default function GoogleAuth() {
const [request, response, promptAsync] = AuthSession.useAuthRequest(
{
clientId: 'YOUR_GOOGLE_CLIENT_ID',
redirectUri: AuthSession.makeRedirectUri({ useProxy: true }),
scopes: ['openid', 'profile', 'email'],
responseType: AuthSession.ResponseType.Code,
},
discovery
);
React.useEffect(() => {
if (response?.type === 'success') {
exchangeCodeForToken(response.params.code);
}
}, [response]);
async function exchangeCodeForToken(code: string) {
const tokenResponse = await AuthSession.exchangeCodeAsync(
{
clientId: 'YOUR_GOOGLE_CLIENT_ID',
code,
redirectUri: AuthSession.makeRedirectUri({ useProxy: true }),
},
discovery
);
console.log('Access token:', tokenResponse.accessToken);
// Fetch user info
const userInfo = await fetch(
'https://www.googleapis.com/oauth2/v2/userinfo',
{
headers: { Authorization: `Bearer ${tokenResponse.accessToken}` },
}
).then(res => res.json());
console.log('User:', userInfo);
}
return (
<Button
title="Sign in with Google"
onPress={() => promptAsync()}
disabled={!request}
/>
);
}
GitHub OAuth
import * as AuthSession from 'expo-auth-session';
const discovery = {
authorizationEndpoint: 'https://github.com/login/oauth/authorize',
tokenEndpoint: 'https://github.com/login/oauth/access_token',
};
function GitHubAuth() {
const [request, response, promptAsync] = AuthSession.useAuthRequest(
{
clientId: 'YOUR_GITHUB_CLIENT_ID',
redirectUri: AuthSession.makeRedirectUri(),
scopes: ['user', 'repo'],
},
discovery
);
React.useEffect(() => {
if (response?.type === 'success') {
const { code } = response.params;
// Exchange code for token
}
}, [response]);
return <Button title="Sign in with GitHub" onPress={() => promptAsync()} />;
}
Auto Discovery
import * as AuthSession from 'expo-auth-session';
function OAuthWithDiscovery() {
const discovery = AuthSession.useAutoDiscovery(
'https://accounts.google.com'
);
const [request, response, promptAsync] = AuthSession.useAuthRequest(
{
clientId: 'client-id',
redirectUri: AuthSession.makeRedirectUri(),
scopes: ['openid', 'profile'],
},
discovery
);
// Rest of the implementation
}
Token Refresh
import * as AuthSession from 'expo-auth-session';
import * as SecureStore from 'expo-secure-store';
async function refreshAccessToken() {
const refreshToken = await SecureStore.getItemAsync('refreshToken');
if (!refreshToken) return null;
const discovery = await AuthSession.fetchDiscoveryAsync(
'https://accounts.google.com'
);
try {
const newTokens = await AuthSession.refreshAsync(
{
clientId: 'client-id',
refreshToken,
},
discovery
);
await SecureStore.setItemAsync('accessToken', newTokens.accessToken);
if (newTokens.refreshToken) {
await SecureStore.setItemAsync('refreshToken', newTokens.refreshToken);
}
return newTokens;
} catch (error) {
console.error('Token refresh failed:', error);
return null;
}
}
Complete Auth Flow
import * as AuthSession from 'expo-auth-session';
import * as WebBrowser from 'expo-web-browser';
import * as SecureStore from 'expo-secure-store';
import { useState, useEffect } from 'react';
import { View, Button, Text, StyleSheet } from 'react-native';
WebBrowser.maybeCompleteAuthSession();
const discovery = {
authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
tokenEndpoint: 'https://oauth2.googleapis.com/token',
revocationEndpoint: 'https://oauth2.googleapis.com/revoke',
};
export default function CompleteAuthExample() {
const [user, setUser] = useState<any>(null);
const [request, response, promptAsync] = AuthSession.useAuthRequest(
{
clientId: 'YOUR_CLIENT_ID',
redirectUri: AuthSession.makeRedirectUri({ useProxy: true }),
scopes: ['openid', 'profile', 'email'],
responseType: AuthSession.ResponseType.Code,
},
discovery
);
useEffect(() => {
checkStoredAuth();
}, []);
useEffect(() => {
if (response?.type === 'success') {
handleAuthResponse(response.params.code);
}
}, [response]);
async function checkStoredAuth() {
const token = await SecureStore.getItemAsync('accessToken');
if (token) {
await fetchUserInfo(token);
}
}
async function handleAuthResponse(code: string) {
const tokens = await AuthSession.exchangeCodeAsync(
{
clientId: 'YOUR_CLIENT_ID',
code,
redirectUri: AuthSession.makeRedirectUri({ useProxy: true }),
},
discovery
);
await SecureStore.setItemAsync('accessToken', tokens.accessToken);
if (tokens.refreshToken) {
await SecureStore.setItemAsync('refreshToken', tokens.refreshToken);
}
await fetchUserInfo(tokens.accessToken);
}
async function fetchUserInfo(accessToken: string) {
const response = await fetch(
'https://www.googleapis.com/oauth2/v2/userinfo',
{
headers: { Authorization: `Bearer ${accessToken}` },
}
);
const userInfo = await response.json();
setUser(userInfo);
}
async function handleLogout() {
const token = await SecureStore.getItemAsync('accessToken');
if (token) {
await AuthSession.revokeAsync(
{ clientId: 'YOUR_CLIENT_ID', token },
discovery
);
}
await SecureStore.deleteItemAsync('accessToken');
await SecureStore.deleteItemAsync('refreshToken');
setUser(null);
}
if (user) {
return (
<View style={styles.container}>
<Text style={styles.welcome}>Welcome, {user.name}!</Text>
<Text>{user.email}</Text>
<Button title="Logout" onPress={handleLogout} />
</View>
);
}
return (
<View style={styles.container}>
<Button
title="Sign in with Google"
onPress={() => promptAsync()}
disabled={!request}
/>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center', alignItems: 'center', gap: 10 },
welcome: { fontSize: 20, fontWeight: 'bold' },
});
| Platform | Supported |
|---|
| iOS | ✅ |
| Android | ✅ |
| Web | ✅ |
Configuration
app.json
{
"expo": {
"scheme": "myapp",
"plugins": [
[
"expo-auth-session",
{
"redirectUri": "myapp://redirect"
}
]
]
}
}
Best Practices
- Use PKCE: Always use PKCE for authorization code flow
- Secure Storage: Store tokens in SecureStore, not AsyncStorage
- Token Refresh: Implement token refresh logic for better UX
- Error Handling: Handle auth errors and cancellations gracefully
- Deep Linking: Configure proper URL schemes for redirects
Always call WebBrowser.maybeCompleteAuthSession() at the top of your component to handle redirect completion properly.
Resources