expo-auth-template
v1.1.0
Published
Reusable authentication library for Expo applications with OAuth and device token support
Maintainers
Readme
Expo Auth Template
A reusable authentication library for Expo applications that provides both frontend and backend authentication components, supporting OAuth providers (Azure AD, Google, Apple) and device token-based persistent authentication.
Features
- Multiple OAuth Providers: Support for Azure AD, Google, and Apple Sign-In
- Device Token Authentication: Persistent authentication using device tokens
- Automatic Token Refresh: Handles token expiration and refresh automatically
- Secure Storage: Cross-platform secure storage for tokens and user data
- React Integration: Easy-to-use hooks and context providers
- Backend Middleware: Ready-to-use authentication middleware for Cloudflare Workers
- TypeScript Support: Full TypeScript support with comprehensive types
Installation
npm install expo-auth-templateGoogle OAuth Setup
To use Google Sign-In, you'll need to:
Create a Google OAuth Client ID:
- Go to Google Cloud Console
- Create a new project or select an existing one
- Enable the Google+ API
- Go to "Credentials" → "Create Credentials" → "OAuth client ID"
- Choose "Web application" for web, or "iOS/Android" for mobile
- Add your redirect URIs:
- iOS:
com.yourapp://oauth - Web:
https://yourapp.com/authorhttp://localhost:8081/authfor development - Android:
com.yourapp:/oauth
- iOS:
Configure Environment Variables:
EXPO_PUBLIC_GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com EXPO_PUBLIC_GOOGLE_REDIRECT_URI_IOS=com.yourapp://oauth EXPO_PUBLIC_GOOGLE_REDIRECT_URI_WEB=https://yourapp.com/authUse the Google Sign-In in your app (see examples below)
Frontend Usage
Basic Setup
import { SessionProvider } from 'expo-auth-template/frontend';
export default function App() {
return (
<SessionProvider>
{/* Your app components */}
</SessionProvider>
);
}Using Authentication Hooks
import { useAuth, useApi } from 'expo-auth-template/frontend';
function MyComponent() {
const { session, user, signIn, signOut, isLoading } = useAuth();
const { fetchApi } = useApi();
if (isLoading) {
return <Text>Loading...</Text>;
}
if (!session) {
return <Button onPress={signIn}>Sign In</Button>;
}
return (
<View>
<Text>Welcome, {user?.name}!</Text>
<Button onPress={signOut}>Sign Out</Button>
</View>
);
}Configuring Auth Service
import { createAuthService, createApiService } from 'expo-auth-template/frontend';
const authService = createAuthService({
azureClientId: 'your-client-id',
azureRedirectUri: 'your-redirect-uri',
apiBaseUrl: 'https://api.example.com/',
});
const apiService = createApiService({
apiBaseUrl: 'https://api.example.com/',
});Sign In with Azure AD
import { useAuthRequest, useAutoDiscovery } from 'expo-auth-session';
import { exchangeCodeAsync } from 'expo-auth-session';
import { authService } from 'expo-auth-template/frontend';
function SignInScreen() {
const discovery = useAutoDiscovery('https://login.microsoftonline.com/common/v2.0');
const [request, response, promptAsync] = useAuthRequest(
{
clientId: 'your-client-id',
scopes: ['openid', 'profile', 'email', 'User.Read', 'offline_access'],
redirectUri: 'your-redirect-uri',
},
discovery,
);
useEffect(() => {
if (response?.type === 'success') {
exchangeCodeAsync(
{
clientId: 'your-client-id',
code: response.params.code,
redirectUri: 'your-redirect-uri',
},
discovery,
).then((tokens) => {
authService.signIn(tokens);
});
}
}, [response]);
return <Button onPress={() => promptAsync()}>Sign In with Microsoft</Button>;
}Sign In with Google
Using GoogleSigninButton Component (Recommended)
The GoogleSigninButton component provides a ready-to-use button that follows Google's branding guidelines and handles the complete OAuth flow with device token exchange:
import { GoogleSigninButton } from 'expo-auth-template/frontend';
import { useAuth } from 'expo-auth-template/frontend';
function SignInScreen() {
const { session, signOut } = useAuth();
if (session) {
return (
<View>
<Text>Welcome, {session.name}!</Text>
<Button onPress={signOut}>Sign Out</Button>
</View>
);
}
return (
<GoogleSigninButton
clientId={process.env.EXPO_PUBLIC_GOOGLE_CLIENT_ID}
apiBaseUrl={process.env.EXPO_PUBLIC_API_BASE_URL}
onSignInSuccess={() => console.log('Signed in!')}
onSignInError={(error) => console.error('Sign-in error:', error)}
/>
);
}GoogleSigninButton Props:
clientId?: string- Google OAuth client ID (defaults toEXPO_PUBLIC_GOOGLE_CLIENT_ID)redirectUri?: string- OAuth redirect URI (auto-detected by platform)apiBaseUrl?: string- API base URL for callback endpoint (defaults toEXPO_PUBLIC_API_BASE_URL)size?: 'small' | 'medium' | 'large'- Button size (default: 'large')theme?: 'light' | 'dark' | 'neutral'- Button theme following Google's branding guidelines (default: 'light')light: White background (#FFFFFF), #747775 border, #1F1F1F textdark: #131314 background, #8E918F border, #E3E3E3 textneutral: #F2F2F2 background, no border, #1F1F1F text
useCodeExchange?: boolean- Use code exchange (true) or direct token (false) (default: true)onSignInStart?: () => void- Callback when sign-in startsonSignInSuccess?: () => void- Callback when sign-in succeedsonSignInError?: (error: Error) => void- Callback when sign-in failsdisabled?: boolean- Disable the button
The button uses the official Google "G" logo and follows all Google branding requirements for app verification.
Using Google OAuth Directly (Legacy)
For custom implementations, you can use the OAuth flow directly:
import { useAuthRequest, useAutoDiscovery, exchangeCodeAsync } from 'expo-auth-session';
import * as WebBrowser from 'expo-web-browser';
import { authService } from 'expo-auth-template/frontend';
import { Platform } from 'react-native';
WebBrowser.maybeCompleteAuthSession();
const GOOGLE_CLIENT_ID = 'your-google-client-id';
const GOOGLE_REDIRECT_URI = Platform.select({
ios: 'com.yourapp://oauth',
web: `${globalThis?.location?.origin}/auth`,
default: 'com.yourapp://oauth',
});
const GOOGLE_DISCOVERY = {
authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
tokenEndpoint: 'https://oauth2.googleapis.com/token',
revocationEndpoint: 'https://oauth2.googleapis.com/revoke',
};
function GoogleSignInScreen() {
const [request, response, promptAsync] = useAuthRequest(
{
clientId: GOOGLE_CLIENT_ID,
scopes: ['openid', 'profile', 'email'],
redirectUri: GOOGLE_REDIRECT_URI,
responseType: 'code',
},
GOOGLE_DISCOVERY,
);
useEffect(() => {
if (response?.type === 'success') {
// Option 1: Exchange code for device token via backend callback
authService.signInWithGoogleCallback({
code: response.params.code,
redirect_uri: GOOGLE_REDIRECT_URI,
}).then(() => {
console.log('Signed in with device token!');
});
// Option 2: Exchange code for tokens client-side, then send to backend
exchangeCodeAsync(
{
clientId: GOOGLE_CLIENT_ID,
code: response.params.code,
redirectUri: GOOGLE_REDIRECT_URI,
},
GOOGLE_DISCOVERY,
).then((tokens) => {
authService.signInWithGoogleCallback({
id_token: tokens.idToken,
access_token: tokens.accessToken,
});
});
}
}, [response]);
return <Button onPress={() => promptAsync()}>Sign In with Google</Button>;
}Saving and Using Device Tokens
When using GoogleSigninButton or signInWithGoogleCallback, the device token is automatically saved and used for authentication:
import { useAuth, useApi } from 'expo-auth-template/frontend';
function MyComponent() {
const { session } = useAuth();
const { fetchApi } = useApi();
useEffect(() => {
// Device token is automatically included in API requests
// via the 'Device {token}' header format
async function loadData() {
try {
const data = await fetchApi('/api/protected');
console.log('Data:', data);
} catch (error) {
console.error('Failed to load data:', error);
}
}
if (session?.deviceToken) {
loadData();
}
}, [session]);
return <Text>Device token: {session?.deviceToken ? 'Loaded' : 'Not loaded'}</Text>;
}The device token is stored securely using expo-secure-store and is automatically included in all API requests made through useApi() or apiService.fetchApi().
Sign In with Apple
import * as AppleAuthentication from 'expo-apple-authentication';
import { authService } from 'expo-auth-template/frontend';
async function handleAppleSignIn() {
try {
const credential = await AppleAuthentication.signInAsync({
requestedScopes: [
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
AppleAuthentication.AppleAuthenticationScope.EMAIL,
],
});
if (credential.identityToken) {
await authService.signInWithApple(credential.identityToken, {
fullName: credential.fullName,
email: credential.email,
});
}
} catch (error) {
console.error('Apple Sign-In error:', error);
}
}Making Authenticated API Calls
import { useApi } from 'expo-auth-template/frontend';
function DataComponent() {
const { fetchApi, user } = useApi();
const [data, setData] = useState(null);
useEffect(() => {
async function loadData() {
try {
const result = await fetchApi('/api/data');
setData(result);
} catch (error) {
console.error('Failed to load data:', error);
}
}
loadData();
}, []);
return <Text>{JSON.stringify(data)}</Text>;
}Backend Usage
Cloudflare Workers Example
import { useAuth } from 'expo-auth-template/backend';
export default {
async fetch(request: Request, env: any, ctx?: ExecutionContext): Promise<Response> {
// Authenticate the request
const user = await useAuth(request, env, ctx);
if (!user) {
return new Response('Unauthorized', { status: 401 });
}
// User is authenticated, proceed with request handling
return new Response(JSON.stringify({ message: 'Hello', user }), {
headers: { 'Content-Type': 'application/json' },
});
},
};Custom Request Handler
import { useAuth } from 'expo-auth-template/backend';
async function handleRequest(request: Request, env: any): Promise<Response> {
const user = await useAuth(request, env);
if (!user) {
return new Response('Unauthorized', { status: 401 });
}
// Access authenticated user
console.log('Authenticated user:', user.email);
// Your request handling logic here
return new Response('OK');
}Google OAuth Callback Endpoint
The library provides a handleGoogleCallback function to handle Google OAuth callbacks and exchange tokens/codes for device tokens:
import { handleGoogleCallback } from 'expo-auth-template/backend';
import type { GoogleCallbackConfig } from 'expo-auth-template/backend';
export default {
async fetch(request: Request, env: any, ctx?: ExecutionContext): Promise<Response> {
const url = new URL(request.url);
// Handle Google OAuth callback
if (url.pathname === '/auth/google/callback' && request.method === 'POST') {
try {
const requestBody = await request.json();
// Configure callback handler
const config: GoogleCallbackConfig = {
createUserIfNotExists: true, // Automatically create users (default: true)
googleClientId: env.GOOGLE_CLIENT_ID,
googleClientSecret: env.GOOGLE_CLIENT_SECRET,
};
// Handle the callback - accepts either { code, redirect_uri } or { id_token, access_token }
const result = await handleGoogleCallback(env, requestBody, config);
// Return device token and user info
return new Response(JSON.stringify({
deviceToken: result.deviceToken,
user: result.user,
}), {
headers: { 'Content-Type': 'application/json' },
status: 200,
});
} catch (error) {
console.error('Google callback error:', error);
return new Response(JSON.stringify({
error: error instanceof Error ? error.message : 'Unknown error',
}), {
headers: { 'Content-Type': 'application/json' },
status: 400,
});
}
}
// Handle other routes...
return new Response('Not Found', { status: 404 });
},
};Configuration Options:
createUserIfNotExists?: boolean- Automatically create users if they don't exist (default:true)googleClientId?: string- Google OAuth client ID (defaults toenv.GOOGLE_CLIENT_ID)googleClientSecret?: string- Google OAuth client secret (defaults toenv.GOOGLE_CLIENT_SECRET, required for code exchange)
Request Body Formats:
The callback accepts either:
- Authorization code:
{ code: string, redirect_uri: string } - Direct tokens:
{ id_token?: string, access_token?: string }
Response Format:
{
deviceToken: string; // 64-character hex device token
user: {
id: string;
email: string;
name: string;
// ... other user fields
};
}Example: Restricting to Existing Users Only
const config: GoogleCallbackConfig = {
createUserIfNotExists: false, // Reject new users
googleClientId: env.GOOGLE_CLIENT_ID,
googleClientSecret: env.GOOGLE_CLIENT_SECRET,
};
const result = await handleGoogleCallback(env, requestBody, config);
// Will throw error if user doesn't existAPI Reference
Frontend Services
authService
Main authentication service singleton.
Methods:
signIn(tokens?: TokenResponse): Promise<UserSession | null>signInWithGoogle(tokens: TokenResponse): Promise<UserSession>signInWithGoogleCallback(data): Promise<UserSession>- Exchange Google OAuth code/tokens for device tokensignInWithApple(identityToken: string, user?: {...}): Promise<UserSession>signOut(): Promise<void>refreshTokens(): Promise<UserSession | null>getSession(): Promise<UserSession | null>getUser(): Promise<User | null>onSessionChange(callback): () => void- Subscribe to session changesonUserChange(callback): () => void- Subscribe to user changesonAuthError(callback): () => void- Subscribe to auth errors
apiService
HTTP client with automatic authentication.
Methods:
fetchApi(path, options?, customSession?, skipReadyCheck?, rawResponse?): Promise<any>downloadFile(path, options?, customSession?, skipReadyCheck?): Promise<Response>getCurrentUser(): Promise<User | null>assumeUser(targetUserId, reason?): Promise<string>- Admin impersonation
React Hooks
useAuth()
Returns authentication state and methods.
const {
session, // Current user session
user, // Current user data
authError, // Current auth error
isLoading, // Loading state
signIn, // Sign in method (Azure/Microsoft)
signInWithGoogle, // Sign in with Google method
signInWithApple, // Sign in with Apple method
signOut, // Sign out method
refreshTokens, // Refresh tokens method
} = useAuth();GoogleSigninButton
Ready-to-use Google Sign-In button component with proper branding.
<GoogleSigninButton
clientId="your-google-client-id"
apiBaseUrl="https://api.example.com/"
size="large"
theme="outline"
onSignInSuccess={() => console.log('Signed in!')}
onSignInError={(error) => console.error('Error:', error)}
/>useApi()
Returns API service methods and current user.
const {
user, // Current user data
fetchApi, // Make authenticated API calls
downloadFile, // Download files with auth
} = useApi();useSession()
Access session from context (alternative to useAuth).
Backend Middleware
useAuth(request, env, ctx?)
Authenticates a request and returns the user.
Parameters:
request: Request- The incoming requestenv: Env- Environment variables (must includeDB)ctx?: ExecutionContext- Optional execution context
Returns:
Promise<User | null>Returns null if authentication fails. The caller should return a 401 response in this case.
handleGoogleCallback(env, requestBody, config?)
Handles Google OAuth callback and exchanges tokens/code for device token.
Parameters:
env: Env- Environment variables (must includeDB,GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET)requestBody: GoogleCallbackRequest- Request body with either{ code, redirect_uri }or{ id_token, access_token }config?: GoogleCallbackConfig- Optional configuration
Returns:
Promise<GoogleCallbackResponse>Returns device token and user information. Throws error if authentication fails or user creation is disabled and user doesn't exist.
Database Schema
The library expects a database with the following tables:
users table
CREATE TABLE users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
role TEXT,
is_admin INTEGER DEFAULT 0,
organization_id TEXT,
created_at INTEGER,
updated_at INTEGER
);device_tokens table
CREATE TABLE device_tokens (
id TEXT PRIMARY KEY,
device_token TEXT UNIQUE NOT NULL,
user_id TEXT NOT NULL,
device_name TEXT,
device_platform TEXT,
user_agent TEXT,
ip_address TEXT,
last_used_at INTEGER,
expires_at INTEGER,
created_at INTEGER,
updated_at INTEGER,
FOREIGN KEY (user_id) REFERENCES users(id)
);Environment Variables
Frontend
EXPO_PUBLIC_AZURE_CLIENT_ID- Azure AD client IDEXPO_PUBLIC_GOOGLE_CLIENT_ID- Google OAuth client IDEXPO_PUBLIC_AUTH_REDIRECT_URI- OAuth redirect URIEXPO_PUBLIC_AUTH_REDIRECT_URI_IOS- iOS-specific redirect URIEXPO_PUBLIC_AUTH_REDIRECT_URI_WEB- Web-specific redirect URIEXPO_PUBLIC_GOOGLE_REDIRECT_URI- Google OAuth redirect URIEXPO_PUBLIC_GOOGLE_REDIRECT_URI_IOS- iOS-specific Google redirect URIEXPO_PUBLIC_API_BASE_URL- API base URL
Backend
DB- Database instance (D1, etc.)GOOGLE_CLIENT_ID- Google OAuth client ID (required for callback endpoint)GOOGLE_CLIENT_SECRET- Google OAuth client secret (required for code exchange)CREATE_USER_IF_NOT_EXISTS- Whether to auto-create users (default: 'true', set to 'false' to disable)
Security Considerations
- Device tokens are stored securely using
expo-secure-storeon native platforms - OAuth tokens are stored securely and automatically refreshed
- Device tokens can be revoked server-side
- All API requests include authentication headers automatically
- Token validation happens on both client and server
Publishing
To create a new release using git-flow:
Bump the version (this creates a commit and tag):
npm version patch # for bug fixes npm version minor # for new features npm version major # for breaking changesRun the release workflow:
npm run releaseThis will:
- Build the project
- Run tests
- Create a git-flow release branch
- Sync with main branch
- Finish the release (merge to main and develop)
- Push all branches and tags to GitHub
- Publish to npm
- Checkout develop and push
Or combine both steps:
npm version patch && npm run releaseNote: This requires git-flow to be installed and initialized in your repository.
License
MIT
