npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

expo-auth-template

v1.1.0

Published

Reusable authentication library for Expo applications with OAuth and device token support

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-template

Google OAuth Setup

To use Google Sign-In, you'll need to:

  1. 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/auth or http://localhost:8081/auth for development
      • Android: com.yourapp:/oauth
  2. 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/auth
  3. Use 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 to EXPO_PUBLIC_GOOGLE_CLIENT_ID)
  • redirectUri?: string - OAuth redirect URI (auto-detected by platform)
  • apiBaseUrl?: string - API base URL for callback endpoint (defaults to EXPO_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 text
    • dark: #131314 background, #8E918F border, #E3E3E3 text
    • neutral: #F2F2F2 background, no border, #1F1F1F text
  • useCodeExchange?: boolean - Use code exchange (true) or direct token (false) (default: true)
  • onSignInStart?: () => void - Callback when sign-in starts
  • onSignInSuccess?: () => void - Callback when sign-in succeeds
  • onSignInError?: (error: Error) => void - Callback when sign-in fails
  • disabled?: 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 to env.GOOGLE_CLIENT_ID)
  • googleClientSecret?: string - Google OAuth client secret (defaults to env.GOOGLE_CLIENT_SECRET, required for code exchange)

Request Body Formats:

The callback accepts either:

  1. Authorization code: { code: string, redirect_uri: string }
  2. 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 exist

API 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 token
  • signInWithApple(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 changes
  • onUserChange(callback): () => void - Subscribe to user changes
  • onAuthError(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 request
  • env: Env - Environment variables (must include DB)
  • 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 include DB, 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 ID
  • EXPO_PUBLIC_GOOGLE_CLIENT_ID - Google OAuth client ID
  • EXPO_PUBLIC_AUTH_REDIRECT_URI - OAuth redirect URI
  • EXPO_PUBLIC_AUTH_REDIRECT_URI_IOS - iOS-specific redirect URI
  • EXPO_PUBLIC_AUTH_REDIRECT_URI_WEB - Web-specific redirect URI
  • EXPO_PUBLIC_GOOGLE_REDIRECT_URI - Google OAuth redirect URI
  • EXPO_PUBLIC_GOOGLE_REDIRECT_URI_IOS - iOS-specific Google redirect URI
  • EXPO_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-store on 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:

  1. 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 changes
  2. Run the release workflow:

    npm run release

    This 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 release

    Note: This requires git-flow to be installed and initialized in your repository.

License

MIT