Skip to main content

Add authentication to your React Native app

Open in Cursor

This guide walks you through adding authentication and access control to a React Native (Expo) application using @lumoauth/sdk and the PKCE flow.


Before you start

You need:

  • A LumoAuth account with a configured tenant (sign up)
  • A registered OAuth application with a custom URI scheme redirect (e.g., myapp://auth/callback)
  • Expo SDK 49+ or bare React Native

Install dependencies

npm install @lumoauth/sdk
npx expo install expo-auth-session expo-secure-store expo-crypto expo-web-browser

For bare React Native (without Expo), use react-native-keychain instead of expo-secure-store.


Register your URI scheme

In app.json:

{
"expo": {
"scheme": "myapp"
}
}

Your redirect URI will be myapp://auth/callback.


Environment variables

EXPO_PUBLIC_LUMOAUTH_DOMAIN=https://app.lumoauth.dev
EXPO_PUBLIC_LUMOAUTH_TENANT=YOUR_TENANT_SLUG
EXPO_PUBLIC_LUMOAUTH_CLIENT_ID=YOUR_CLIENT_ID

Auth hook

Create hooks/useLumoAuth.ts:

import { useState, useEffect, useCallback } from 'react';
import * as SecureStore from 'expo-secure-store';
import * as WebBrowser from 'expo-web-browser';
import { AuthModule, LumoAuth } from '@lumoauth/sdk';

WebBrowser.maybeCompleteAuthSession();

const DOMAIN = process.env.EXPO_PUBLIC_LUMOAUTH_DOMAIN!;
const TENANT = process.env.EXPO_PUBLIC_LUMOAUTH_TENANT!;
const CLIENT_ID = process.env.EXPO_PUBLIC_LUMOAUTH_CLIENT_ID!;
const REDIRECT_URI = 'myapp://auth/callback';

const authModule = new AuthModule({ baseUrl: DOMAIN, tenantSlug: TENANT, clientId: CLIENT_ID });

export function useLumoAuth() {
const [accessToken, setAccessToken] = useState<string | null>(null);
const [isLoaded, setIsLoaded] = useState(false);

useEffect(() => {
SecureStore.getItemAsync('lumoauth_access_token').then(token => {
setAccessToken(token);
setIsLoaded(true);
});
}, []);

const signIn = useCallback(async () => {
const { url, codeVerifier, state } = await authModule.buildAuthorizationUrl({
redirectUri: REDIRECT_URI,
});

await SecureStore.setItemAsync('lumoauth_verifier', codeVerifier);
await SecureStore.setItemAsync('lumoauth_state', state);

const result = await WebBrowser.openAuthSessionAsync(url, REDIRECT_URI);
if (result.type !== 'success') return;

const params = new URL(result.url).searchParams;
const code = params.get('code');
const returnedState = params.get('state');
const savedState = await SecureStore.getItemAsync('lumoauth_state');
const storedVerifier = await SecureStore.getItemAsync('lumoauth_verifier');

if (!code || returnedState !== savedState || !storedVerifier) {
throw new Error('Invalid OAuth callback');
}

const tokens = await authModule.exchangeCodeForTokens({
code,
codeVerifier: storedVerifier,
redirectUri: REDIRECT_URI,
});

await SecureStore.setItemAsync('lumoauth_access_token', tokens.access_token);
if (tokens.refresh_token) {
await SecureStore.setItemAsync('lumoauth_refresh_token', tokens.refresh_token);
}
await SecureStore.deleteItemAsync('lumoauth_verifier');
await SecureStore.deleteItemAsync('lumoauth_state');
setAccessToken(tokens.access_token);
}, []);

const signOut = useCallback(async () => {
if (accessToken) authModule.revokeToken(accessToken, accessToken).catch(() => {});
await SecureStore.deleteItemAsync('lumoauth_access_token');
await SecureStore.deleteItemAsync('lumoauth_refresh_token');
setAccessToken(null);
}, [accessToken]);

const getClient = useCallback(() =>
new LumoAuth({
baseUrl: DOMAIN,
tenantSlug: TENANT,
clientId: CLIENT_ID,
token: () => SecureStore.getItemAsync('lumoauth_access_token').then(t => t || ''),
}), []
);

return { isSignedIn: !!accessToken, isLoaded, accessToken, signIn, signOut, getClient };
}

Use it in a screen

import { View, Button, ActivityIndicator } from 'react-native';
import { useLumoAuth } from '../hooks/useLumoAuth';

export default function HomeScreen() {
const { isSignedIn, isLoaded, signIn, signOut, getClient } = useLumoAuth();

if (!isLoaded) return <ActivityIndicator />;

async function checkPermission() {
const client = getClient();
const allowed = await client.permissions.check('documents.edit');
console.log('allowed:', allowed);
}

return isSignedIn ? (
<View>
<Button title="Check Permission" onPress={checkPermission} />
<Button title="Sign Out" onPress={signOut} />
</View>
) : (
<Button title="Sign In with LumoAuth" onPress={signIn} />
);
}