react-native-auth-kit
v0.1.0
Published
Unified biometric authentication + secure token management for React Native — Face ID, Touch ID, fingerprint unlock, Keychain/Keystore storage, JWT auto-refresh, and session timeout in one package.
Downloads
144
Maintainers
Readme
react-native-auth-kit
Biometric authentication and secure token management for React Native. Combines Face ID / Touch ID / fingerprint unlock, Keychain/Keystore storage, JWT access/refresh rotation, and session timeout into a single cohesive API.
Background
Most React Native apps that require authentication end up writing the same three pieces from scratch:
- A biometrics wrapper around
react-native-biometrics - A secure token store on top of
react-native-keychain - A fetch interceptor that adds
Authorizationheaders and refreshes on 401
Each piece is not hard alone, but wiring them together correctly — especially the race condition where two concurrent 401s both try to refresh the token — is where bugs get introduced. This package handles all three as a coordinated system.
Installation
npm install react-native-auth-kitPeer dependencies:
npm install react-native-keychain react-native-biometricsFollow the native setup for each:
Both require native linking. With Expo, use the managed workflow with expo-local-authentication and expo-secure-store instead.
Quick start
// Configure once at app startup — typically in App.tsx or your root navigator
import { AuthKitManager, createFetchInterceptor } from 'react-native-auth-kit';
const manager = AuthKitManager.getInstance();
manager.configure({
sessionTimeoutMs: 15 * 60 * 1000, // 15 minutes idle → session expired
biometricPromptTitle: 'Confirm your identity',
biometricPromptSubtitle: 'Use Face ID or Touch ID to continue',
onSessionExpired: () => {
// Navigate to login — runs when idle timer fires or refresh fails
RootNavigation.navigate('Login');
},
});// After login — store the tokens
const tokens = await api.login(email, password);
await manager.initialize({
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresAt: Date.now() + tokens.expires_in * 1000,
});// Wrap fetch with automatic Bearer injection and 401 refresh
const { fetch: authFetch } = createFetchInterceptor(
async (refreshToken) => {
const res = await fetch('/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: refreshToken }),
});
if (!res.ok) throw new Error('Refresh failed');
const data = await res.json();
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
expiresAt: Date.now() + data.expires_in * 1000,
};
}
);
// Use authFetch exactly like the global fetch — no other changes needed
const profile = await authFetch('/api/me').then(r => r.json());React hooks
useAuthKit
Full authentication state and actions. Use this in screens that need to react to auth changes.
function HomeScreen() {
const {
isAuthenticated,
isLoading,
biometryType, // 'FaceID' | 'TouchID' | 'Fingerprint' | 'None'
login,
logout,
authenticateWithBiometrics,
getAccessToken,
} = useAuthKit();
const handleBiometricUnlock = async () => {
const result = await authenticateWithBiometrics('Unlock to continue');
if (!result.success) {
Alert.alert('Authentication failed', result.error);
}
};
if (isLoading) return <LoadingScreen />;
if (!isAuthenticated) return <LoginScreen onLogin={login} />;
return (
<View>
<Button title={`Unlock with ${biometryType}`} onPress={handleBiometricUnlock} />
<Button title="Sign out" onPress={logout} />
</View>
);
}useBiometrics
Just the biometrics, without token management. Useful for screens that need re-authentication (payment confirmation, viewing sensitive data) without being tied to the session lifecycle.
function PaymentConfirmScreen() {
const { isAvailable, biometryType, authenticate } = useBiometrics();
const confirm = async () => {
if (!isAvailable) {
// Fall back to PIN or password prompt
return promptForPin();
}
const result = await authenticate('Confirm payment');
if (result.success) processPayment();
};
}Token management
Tokens are stored in iOS Keychain and Android Keystore — the same secure storage that password managers use. They are never written to AsyncStorage, SQLite, or the filesystem.
// TokenStore — JWT-aware operations
const store = new TokenStore();
await store.saveTokens({ accessToken, refreshToken, expiresAt });
const tokens = await store.getTokens();
const isExpired = TokenStore.isAccessTokenExpired(tokens, 30_000); // 30s bufferAccess token expiry is checked with a 30-second buffer before the actual expiry time, so the refresh happens before the token actually expires rather than after the first failed request.
The 401 refresh flow
createFetchInterceptor handles this sequence automatically:
Request → add Authorization: Bearer <access_token>
Response 401 → get refresh token from Keychain
→ call your refreshFn
→ save new tokens to Keychain
→ retry original request with new token
Response 401 again → call onRefreshFailure
→ call manager.logout() → clears tokens
→ return the 401 responseOnly one refresh can be in flight at a time. If two requests both 401 simultaneously, the second waits for the first refresh to complete and then uses the result rather than triggering a second refresh.
Biometric authentication
const bio = new BiometricAuth();
// Check availability
const { available, biometryType } = await bio.isAvailable();
// Prompt the user
const result = await bio.authenticate('Confirm your identity');
if (result.success) {
// Proceed
}
// Key management (for signing tokens with device keys)
await bio.createKeys();
const signature = await bio.signPayload('data-to-sign');
await bio.deleteKeys();BiometricAuth wraps react-native-biometrics and normalizes the response — you always get { success: boolean; error?: string } rather than needing to handle platform-specific result shapes.
SecureStore
Lower-level API for storing any string or JSON value in Keychain/Keystore:
const store = new SecureStore();
// Store a string
await store.set('user_id', '12345');
// Store an object (JSON serialized)
await store.setObject<UserProfile>('profile', { name: 'Alice', role: 'admin' });
// Retrieve
const userId = await store.get('user_id');
const profile = await store.getObject<UserProfile>('profile');
// Check existence without reading value
const hasSession = await store.hasKey('session_token');
// Remove a single key or clear all
await store.delete('user_id');
await store.clear();Session timeout
The idle timeout is reset every time resetSessionTimer() is called. Wire it to app state changes:
import { AppState } from 'react-native';
AppState.addEventListener('change', (state) => {
if (state === 'active') {
AuthKitManager.getInstance().resetSessionTimer();
}
});When the timer fires (no resetSessionTimer call within sessionTimeoutMs), onSessionExpired is called. The session is not automatically cleared — your handler decides what happens (navigate to login, show a lock screen, etc.). Call manager.logout() inside onSessionExpired to clear tokens.
Security notes
What is stored in Keychain/Keystore: Access token, refresh token, expiry timestamp. Nothing else.
What is not stored: Passwords, PINs, biometric data. The device OS owns biometric data; this package never sees it.
Jailbreak / root: react-native-keychain can be configured with BIOMETRY_CURRENT_SET access control, which restricts access to the item if biometrics change after storage. This is a meaningful protection against attackers adding their own fingerprint. See the keychain access control docs for configuration.
Token rotation: Every refresh returns a new refresh token. The old refresh token is discarded after the new one is saved. If the refresh API issues single-use tokens (recommended), a stolen refresh token can only be used once before it's invalidated.
License
MIT © Salil Gupta
