React Native'de Auth0 Oturum Yönetimi ve Biyometrik Giriş
Production React Native uygulamalarında Auth0, biyometrik kimlik doğrulama ve doğru token yaşam döngüsü ile güvenli oturum yönetimi için adım adım kılavuz.
Mobil uygulamalarda oturum yönetimi, web geliştiricilerinin nadiren karşılaştığı zorluklar çıkarır. Bir mobil uygulama; arka plan durumlarını, ağ kesintilerini, biyometrik kimlik doğrulamayı ve platforma özgü güvenlik kısıtlamalarını, kullanıcı deneyimini bozmadan yönetmek zorundadır. React Native tarafında ayakta kalan varsayılan şu: refresh token rotasyonunu Auth0 tenant’ına bırakın, token’ları Keychain veya Keystore içinde biyometrik kapının arkasında tutun ve 401’e tepki vermek yerine süre dolmadan yenileyin.
Parçalar şunlar: zamanlayıcıyla ve ağ geri geldiğinde çalışan bir token yöneticisi, platformun güvenli deposunun önünde duran bir biyometrik kapı, boşta kalma zaman aşımını uygulayan bir app-state hook’u ve bunları bir araya getiren bir auth context. Her adım bir öncekinin üzerine biner; kod sırayla okunmak üzere yazıldı.
Ön Gereksinimler#
Başlamadan önce şunlara sahip olduğunuzdan emin olun:
- React Native geliştirme ortamı yapılandırılmış (React Native 0.78+)
- Yapılandırılmış uygulamaya sahip Auth0 hesabı
- iOS ve Android geliştirme ortamları kurulu
- OAuth 2.0 ve JWT token’larının temel anlayışı
- React Native navigasyon ve state yönetimine aşinalık
Gerekli paketler:
{
"react-native": "^0.78.0",
"react-native-auth0": "^4.6.0",
"react-native-biometrics": "^3.0.1",
"react-native-keychain": "^10.0.0",
"@react-native-async-storage/async-storage": "^1.21.0",
"@react-native-community/netinfo": "^11.2.0",
"react-native-background-fetch": "^4.2.0"
}
Uyumluluk notları:
- React Native New Architecture: Paketlerin tamamı New Architecture (Fabric/TurboModules) ile uyumludur
- Expo: Expo dev build’leriyle uyumludur (native bağımlılıklar için custom development client gerekir)
- iOS arka plan kısıtı: Arka plan fetch’i sistemin belirlediği aralıklarda çalışır; token yenileme için güvenilir bir yol değildir
Adım 1: Oturum Yönetimi Mimarisini Anlamak#
Devamının tamamı iki şeye dayanır: oturumun geçtiği durumlar ve her token’a atanan ömür.
Oturum Durumları ve Yaşam Döngüsü#
Mobil uygulamalar, çeşitli kimlik doğrulama senaryolarını yönetmek için ayrıntılı bir durum modeli gerektirir:
// types/AuthState.ts
export enum UserState {
UNAUTHENTICATED = 'unauthenticated',
AUTHENTICATING = 'authenticating',
AUTHENTICATED = 'authenticated',
SESSION_EXPIRED = 'session_expired',
REQUIRES_VERIFICATION = 'requires_verification',
CHECKING_AUTH = 'checking_auth',
LOGGING_OUT = 'logging_out'
}
export interface AuthState {
userState: UserState;
user: User | null;
tokens: {
accessToken: string | null;
refreshToken: string | null;
idToken: string | null;
expiresAt: number | null;
};
lastActivity: number;
biometricEnabled: boolean;
sessionStartTime: number;
}
export interface User {
id: string;
email: string;
name: string;
picture?: string;
emailVerified: boolean;
}
Token Mimarisi#
Ömürleri farklı üç token var ve üçü de uygulama kodunda değil, Auth0 tenant’ında yapılandırılır. Aşağıdaki değerler bir mobil istemci için işe yarar bir başlangıç noktasıdır:
- Access Token (15 dakika): API yetkilendirmesi için kullanılır
- Refresh Token (30 gün mutlak ömür): Yeni access token’ları almak için kullanılır
- ID Token (1 saat): Kullanıcı profil claim’lerini taşır
Adım 2: Auth0 Entegrasyonunu Kurmak#
// services/auth/AuthService.ts
import Auth0 from 'react-native-auth0';
import Config from 'react-native-config';
class AuthService {
private auth0: Auth0;
private readonly AUTH0_SCOPE = 'openid profile email offline_access';
constructor() {
this.auth0 = new Auth0({
domain: Config.AUTH0_DOMAIN,
clientId: Config.AUTH0_CLIENT_ID,
});
}
async login(): Promise<Credentials> {
try {
const credentials = await this.auth0.webAuth.authorize({
scope: this.AUTH0_SCOPE,
audience: Config.AUTH0_AUDIENCE,
prompt: 'login',
// Mobil için ek parametreler
parameters: {
device: 'mobile'
}
});
return this.processCredentials(credentials);
} catch (error) {
if (error.error === 'a0.session.user_cancelled') {
throw new Error('Kullanıcı girişi iptal etti');
}
throw error;
}
}
async refreshTokens(refreshToken: string): Promise<Credentials> {
try {
// Rotasyon tenant ayarıdır; SDK yalnızca refresh token'ı takas eder
const credentials = await this.auth0.auth.refreshToken({
refreshToken,
scope: this.AUTH0_SCOPE
});
return this.processCredentials(credentials);
} catch (error) {
if (error.error === 'invalid_grant') {
throw new Error('Refresh token süresi dolmuş veya iptal edilmiş');
}
throw error;
}
}
async logout(): Promise<void> {
// Auth0 oturumunu ve çerezleri temizle
await this.auth0.webAuth.clearSession();
}
async getUserInfo(accessToken: string): Promise<User> {
const userInfo = await this.auth0.auth.userInfo({ token: accessToken });
return {
id: userInfo.sub,
email: userInfo.email,
name: userInfo.name || userInfo.nickname,
picture: userInfo.picture,
emailVerified: userInfo.email_verified
};
}
private processCredentials(credentials: any): Credentials {
return {
accessToken: credentials.accessToken,
refreshToken: credentials.refreshToken,
idToken: credentials.idToken,
expiresIn: credentials.expiresIn,
expiresAt: Date.now() + (credentials.expiresIn * 1000),
tokenType: credentials.tokenType,
scope: credentials.scope
};
}
}
export default new AuthService();
Adım 3: Güvenli Token Depolamayı Uygulamak#
Keychain ve Keystore token baytlarını saklar; AsyncStorage yalnızca kaybı sorun olmayan metadata’yı görür.
// services/storage/SecureStorage.ts
import * as Keychain from 'react-native-keychain';
import AsyncStorage from '@react-native-async-storage/async-storage';
import CryptoJS from 'crypto-js';
class SecureStorage {
private readonly KEYCHAIN_SERVICE = 'com.yourapp.oauth';
private readonly KEYCHAIN_ACCESS_GROUP = 'group.com.yourapp';
async storeTokens(tokens: TokenSet): Promise<void> {
try {
// Hassas token'ları Keychain'de sakla
await Keychain.setInternetCredentials(
this.KEYCHAIN_SERVICE,
'tokens',
JSON.stringify({
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
expiresAt: tokens.expiresAt
}),
{
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_CURRENT_SET,
accessGroup: this.KEYCHAIN_ACCESS_GROUP,
authenticationPrompt: 'Hesabınıza erişmek için kimlik doğrulayın'
}
);
// Hassas olmayan verileri AsyncStorage'da sakla
await AsyncStorage.setItem('@auth_metadata', JSON.stringify({
expiresAt: tokens.expiresAt,
scope: tokens.scope,
tokenType: tokens.tokenType
}));
} catch (error) {
console.error('Tokenlar güvenli şekilde saklanamadı:', error);
throw new Error('Güvenli depolama başarısız');
}
}
async getTokens(): Promise<TokenSet | null> {
try {
const credentials = await Keychain.getInternetCredentials(
this.KEYCHAIN_SERVICE
);
if (!credentials) {
return null;
}
const tokens = JSON.parse(credentials.password);
const metadata = await AsyncStorage.getItem('@auth_metadata');
return {
...tokens,
...(metadata ? JSON.parse(metadata) : {})
};
} catch (error) {
console.error('Tokenlar alınamadı:', error);
return null;
}
}
async clearTokens(): Promise<void> {
await Keychain.resetInternetCredentials(this.KEYCHAIN_SERVICE);
await AsyncStorage.removeItem('@auth_metadata');
}
async hasStoredCredentials(): Promise<boolean> {
try {
const credentials = await Keychain.hasInternetCredentials(
this.KEYCHAIN_SERVICE
);
return credentials;
} catch {
return false;
}
}
}
export default new SecureStorage();
Adım 4: Biyometrik Kimlik Doğrulamayı Uygulamak#
react-native-biometrics, yukarıdaki depolama katmanının önünde duran kapıdır.
// services/auth/BiometricService.ts
import ReactNativeBiometrics, { BiometryTypes } from 'react-native-biometrics';
import AsyncStorage from '@react-native-async-storage/async-storage';
import SecureStorage from '../storage/SecureStorage';
import AuthService from './AuthService';
class BiometricService {
private rnBiometrics: ReactNativeBiometrics;
constructor() {
this.rnBiometrics = new ReactNativeBiometrics({
allowDeviceCredentials: true
});
}
async isBiometricAvailable(): Promise<{
available: boolean;
biometryType: BiometryTypes | null;
}> {
try {
const { available, biometryType } = await this.rnBiometrics.isSensorAvailable();
return { available, biometryType };
} catch (error) {
console.error('Biyometrik kontrolü başarısız:', error);
return { available: false, biometryType: null };
}
}
async authenticateWithBiometrics(): Promise<AuthenticationResult> {
try {
const { available, biometryType } = await this.isBiometricAvailable();
if (!available) {
return {
success: false,
error: 'Biyometrik kimlik doğrulama mevcut değil'
};
}
// Kimlik doğrulama için imza oluştur
const { success, signature } = await this.rnBiometrics.createSignature({
promptMessage: 'Hesabınıza erişmek için kimlik doğrulayın',
payload: Date.now().toString(),
cancelButtonText: 'İptal'
});
if (!success) {
return {
success: false,
error: 'Biyometrik kimlik doğrulama başarısız'
};
}
// Başarılı kimlik doğrulamadan sonra saklanan token'ları al
const tokens = await SecureStorage.getTokens();
if (!tokens) {
return {
success: false,
error: 'Saklanan kimlik bilgileri bulunamadı'
};
}
// Token'ların yenilenmesi gerekip gerekmediğini kontrol et
if (this.shouldRefreshTokens(tokens)) {
const refreshedTokens = await AuthService.refreshTokens(tokens.refreshToken);
await SecureStorage.storeTokens(refreshedTokens);
return { success: true, tokens: refreshedTokens };
}
return { success: true, tokens };
} catch (error) {
console.error('Biyometrik kimlik doğrulama hatası:', error);
return {
success: false,
error: error.message || 'Bilinmeyen hata oluştu'
};
}
}
async enrollBiometrics(): Promise<boolean> {
try {
const { available } = await this.isBiometricAvailable();
if (!available) {
return false;
}
// Biyometrik kayıt için anahtar çifti oluştur
const { publicKey } = await this.rnBiometrics.createKeys();
// Gelecekteki doğrulama için genel anahtarı sakla
await AsyncStorage.setItem('@biometric_public_key', publicKey);
return true;
} catch (error) {
console.error('Biyometrik kayıt başarısız:', error);
return false;
}
}
async deleteBiometricKeys(): Promise<void> {
await this.rnBiometrics.deleteKeys();
await AsyncStorage.removeItem('@biometric_public_key');
}
private shouldRefreshTokens(tokens: TokenSet): boolean {
const REFRESH_THRESHOLD = 5 * 60 * 1000; // 5 dakika
const timeUntilExpiry = tokens.expiresAt - Date.now();
return timeUntilExpiry < REFRESH_THRESHOLD;
}
}
export default new BiometricService();
Adım 5: Token Yöneticisini Oluşturmak#
// services/auth/TokenManager.ts
import NetInfo from '@react-native-community/netinfo';
import BackgroundFetch from 'react-native-background-fetch';
import AuthService from './AuthService';
import SecureStorage from '../storage/SecureStorage';
class TokenManager {
private refreshTimer: NodeJS.Timeout | null = null;
private refreshPromise: Promise<void> | null = null;
private readonly REFRESH_THRESHOLD = 5 * 60 * 1000; // 5 dakika
private readonly MAX_RETRY_ATTEMPTS = 3;
private readonly RETRY_DELAY_BASE = 1000; // 1 saniye
async initialize(): Promise<void> {
// Otomatik token yenilemeyi ayarla
await this.scheduleTokenRefresh();
// Token yenileme için arka plan fetch'i yapılandır
await this.configureBackgroundRefresh();
// Ağ değişikliklerini dinle
NetInfo.addEventListener(this.handleNetworkChange);
}
async scheduleTokenRefresh(): Promise<void> {
// Mevcut zamanlayıcıyı temizle
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
this.refreshTimer = null;
}
const tokens = await SecureStorage.getTokens();
if (!tokens || !tokens.expiresAt) {
return;
}
const timeUntilExpiry = tokens.expiresAt - Date.now();
const refreshTime = Math.max(0, timeUntilExpiry - this.REFRESH_THRESHOLD);
if (refreshTime > 0) {
this.refreshTimer = setTimeout(() => {
this.performTokenRefresh();
}, refreshTime);
} else {
// Token'ın hemen yenilenmesi gerekiyor
await this.performTokenRefresh();
}
}
async performTokenRefresh(): Promise<void> {
// Eş zamanlı yenileme girişimlerini önle
if (this.refreshPromise) {
return this.refreshPromise;
}
this.refreshPromise = this.doRefreshWithRetry();
try {
await this.refreshPromise;
} finally {
this.refreshPromise = null;
}
}
private async doRefreshWithRetry(): Promise<void> {
let lastError: Error | null = null;
for (let attempt = 0; attempt < this.MAX_RETRY_ATTEMPTS; attempt++) {
try {
// Ağ bağlantısını kontrol et
const netInfo = await NetInfo.fetch();
if (!netInfo.isConnected) {
throw new Error('Ağ bağlantısı yok');
}
// Mevcut token'ları al
const tokens = await SecureStorage.getTokens();
if (!tokens || !tokens.refreshToken) {
throw new Error('Refresh token mevcut değil');
}
// Yenilemeyi gerçekleştir
const newTokens = await AuthService.refreshTokens(tokens.refreshToken);
// Yeni token'ları sakla
await SecureStorage.storeTokens(newTokens);
// Sonraki yenilemeyi planla
await this.scheduleTokenRefresh();
// Başarı olayını yayınla
this.emitTokenRefreshSuccess();
return;
} catch (error) {
lastError = error;
if (error.message === 'Refresh token süresi dolmuş veya iptal edilmiş') {
// Bundan kurtulunamaz - kullanıcının tekrar giriş yapması gerekiyor
this.handleSessionExpired();
return;
}
if (attempt < this.MAX_RETRY_ATTEMPTS - 1) {
// Üstel geri çekilme gecikmesini hesapla
const delay = this.RETRY_DELAY_BASE * Math.pow(2, attempt);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Tüm yeniden denemeler başarısız oldu
console.error('Token yenileme tüm denemelerden sonra başarısız:', lastError);
this.handleSessionExpired();
}
private async configureBackgroundRefresh(): Promise<void> {
await BackgroundFetch.configure({
minimumFetchInterval: 15, // 15 dakika
forceAlarmManager: false,
stopOnTerminate: false,
enableHeadless: true,
startOnBoot: true,
requiredNetworkType: BackgroundFetch.NETWORK_TYPE_ANY
}, async (taskId) => {
try {
await this.performTokenRefresh();
} catch (error) {
console.error('Arka plan token yenilemesi başarısız:', error);
} finally {
BackgroundFetch.finish(taskId);
}
}, (taskId) => {
console.error('Arka plan fetch zaman aşımı');
BackgroundFetch.finish(taskId);
});
// iOS'ta arka plan fetch'i ciddi biçimde kısıtlıdır ve güvenilir çalışmayabilir
// Kritik token yenileme senaryoları için sessiz push bildirimlerini değerlendirin
}
private handleNetworkChange = async (state: any) => {
if (state.isConnected && this.refreshPromise === null) {
// Ağ yeniden bağlandı, token'ların yenilenmesi gerekip gerekmediğini kontrol et
const tokens = await SecureStorage.getTokens();
if (tokens && this.shouldRefreshTokens(tokens)) {
await this.performTokenRefresh();
}
}
};
private shouldRefreshTokens(tokens: TokenSet): boolean {
const timeUntilExpiry = tokens.expiresAt - Date.now();
return timeUntilExpiry < this.REFRESH_THRESHOLD;
}
private handleSessionExpired(): void {
// Token'ları temizle
SecureStorage.clearTokens();
// Oturum sona erdi olayını yayınla
this.emitSessionExpired();
}
private emitTokenRefreshSuccess(): void {
// Uygulamanın işlemesi için olay yayınla
EventEmitter.emit('auth:token-refreshed');
}
private emitSessionExpired(): void {
// Uygulamanın işlemesi için olay yayınla
EventEmitter.emit('auth:session-expired');
}
cleanup(): void {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
this.refreshTimer = null;
}
BackgroundFetch.stop();
}
}
export default new TokenManager();
Adım 6: Uygulama Durum Geçişlerini Yönetmek#
Boşta kalma zaman aşımı ve arka plandan dönüşteki biyometrik kontrol, ikisi de burada AppState geçişlerine bağlı olarak çalışır.
// hooks/useAppStateAuth.ts
import { useEffect, useRef } from 'react';
import { AppState, AppStateStatus } from 'react-native';
import BiometricService from '../services/auth/BiometricService';
import TokenManager from '../services/auth/TokenManager';
interface AppStateAuthConfig {
idleTimeout?: number; // milisaniye
requireBiometricOnForeground?: boolean;
onSessionExpired?: () => void;
onAuthRequired?: () => void;
}
export const useAppStateAuth = (config: AppStateAuthConfig = {}) => {
const {
idleTimeout = 30 * 60 * 1000, // varsayılan 30 dakika
requireBiometricOnForeground = true,
onSessionExpired,
onAuthRequired
} = config;
const appState = useRef(AppState.currentState);
const backgroundTime = useRef<number>(0);
useEffect(() => {
const handleAppStateChange = async (nextAppState: AppStateStatus) => {
// Uygulama ön plana geliyor
if (
appState.current.match(/inactive|background/) &&
nextAppState === 'active'
) {
const timeInBackground = Date.now() - backgroundTime.current;
if (timeInBackground > idleTimeout) {
// Oturum zaman aşımına uğradı
onSessionExpired?.();
} else if (requireBiometricOnForeground && timeInBackground > 60000) {
// Arka planda 1 dakikadan fazla kaldıysa biyometrik iste
const result = await BiometricService.authenticateWithBiometrics();
if (!result.success) {
onAuthRequired?.();
} else {
// Gerekirse token'ları yenile
await TokenManager.scheduleTokenRefresh();
}
} else {
// Sadece token'ların güncel olduğundan emin ol
await TokenManager.scheduleTokenRefresh();
}
}
// Uygulama arka plana gidiyor
else if (
appState.current === 'active' &&
nextAppState.match(/inactive|background/)
) {
backgroundTime.current = Date.now();
}
appState.current = nextAppState;
};
const subscription = AppState.addEventListener('change', handleAppStateChange);
return () => {
subscription.remove();
};
}, [idleTimeout, requireBiometricOnForeground, onSessionExpired, onAuthRequired]);
};
Adım 7: Auth Context’i Uygulamak#
// contexts/AuthContext.tsx
import React, { createContext, useContext, useEffect, useReducer } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import AuthService from '../services/auth/AuthService';
import BiometricService from '../services/auth/BiometricService';
import SecureStorage from '../services/storage/SecureStorage';
import TokenManager from '../services/auth/TokenManager';
import { useAppStateAuth } from '../hooks/useAppStateAuth';
import { AuthState, UserState } from '../types/AuthState';
interface AuthContextValue {
state: AuthState;
login: () => Promise<void>;
logout: () => Promise<void>;
authenticateWithBiometrics: () => Promise<boolean>;
enableBiometrics: () => Promise<boolean>;
checkAuthStatus: () => Promise<void>;
}
const AuthContext = createContext<AuthContextValue | null>(null);
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth AuthProvider içinde kullanılmalıdır');
}
return context;
};
const authReducer = (state: AuthState, action: any): AuthState => {
switch (action.type) {
case 'SET_USER_STATE':
return { ...state, userState: action.payload };
case 'SET_AUTH_DATA':
return {
...state,
userState: UserState.AUTHENTICATED,
user: action.payload.user,
tokens: action.payload.tokens,
sessionStartTime: Date.now(),
lastActivity: Date.now()
};
case 'CLEAR_AUTH':
return {
...state,
userState: UserState.UNAUTHENTICATED,
user: null,
tokens: {
accessToken: null,
refreshToken: null,
idToken: null,
expiresAt: null
},
sessionStartTime: 0,
lastActivity: 0
};
case 'SET_BIOMETRIC_ENABLED':
return { ...state, biometricEnabled: action.payload };
default:
return state;
}
};
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [state, dispatch] = useReducer(authReducer, {
userState: UserState.CHECKING_AUTH,
user: null,
tokens: {
accessToken: null,
refreshToken: null,
idToken: null,
expiresAt: null
},
lastActivity: 0,
biometricEnabled: false,
sessionStartTime: 0
});
// Uygulama durum değişikliklerini yönet
useAppStateAuth({
idleTimeout: 30 * 60 * 1000,
requireBiometricOnForeground: state.biometricEnabled,
onSessionExpired: () => {
dispatch({ type: 'CLEAR_AUTH' });
},
onAuthRequired: () => {
dispatch({ type: 'SET_USER_STATE', payload: UserState.REQUIRES_VERIFICATION });
}
});
// İlk auth durumunu kontrol et
useEffect(() => {
checkAuthStatus();
// Oturum sona erdi olaylarını dinle
const handleSessionExpired = () => {
dispatch({ type: 'CLEAR_AUTH' });
};
EventEmitter.addListener('auth:session-expired', handleSessionExpired);
return () => {
EventEmitter.removeListener('auth:session-expired', handleSessionExpired);
};
}, []);
const checkAuthStatus = async () => {
try {
dispatch({ type: 'SET_USER_STATE', payload: UserState.CHECKING_AUTH });
const tokens = await SecureStorage.getTokens();
if (!tokens || !tokens.accessToken) {
dispatch({ type: 'CLEAR_AUTH' });
return;
}
// Token'ların süresi dolmuş mu kontrol et
if (tokens.expiresAt && tokens.expiresAt < Date.now()) {
// Yenilemeyi dene
try {
await TokenManager.performTokenRefresh();
const newTokens = await SecureStorage.getTokens();
if (newTokens) {
const user = await AuthService.getUserInfo(newTokens.accessToken);
dispatch({
type: 'SET_AUTH_DATA',
payload: { user, tokens: newTokens }
});
// Token yöneticisini başlat
await TokenManager.initialize();
}
} catch {
dispatch({ type: 'CLEAR_AUTH' });
}
} else {
// Token'lar hala geçerli
const user = await AuthService.getUserInfo(tokens.accessToken);
dispatch({
type: 'SET_AUTH_DATA',
payload: { user, tokens }
});
// Token yöneticisini başlat
await TokenManager.initialize();
}
// Biyometrik kaydı kontrol et
const { available } = await BiometricService.isBiometricAvailable();
if (available) {
const enrolled = await AsyncStorage.getItem('@biometric_enrolled');
dispatch({ type: 'SET_BIOMETRIC_ENABLED', payload: enrolled === 'true' });
}
} catch (error) {
console.error('Auth durumu kontrolü başarısız:', error);
dispatch({ type: 'CLEAR_AUTH' });
}
};
const login = async () => {
try {
dispatch({ type: 'SET_USER_STATE', payload: UserState.AUTHENTICATING });
const credentials = await AuthService.login();
await SecureStorage.storeTokens(credentials);
const user = await AuthService.getUserInfo(credentials.accessToken);
dispatch({
type: 'SET_AUTH_DATA',
payload: { user, tokens: credentials }
});
// Token yöneticisini başlat
await TokenManager.initialize();
// Biyometrik kaydı öner
const { available } = await BiometricService.isBiometricAvailable();
if (available && !state.biometricEnabled) {
// Kullanıcıya biyometrikleri etkinleştirmesini iste
// Bu genellikle bir modal gösterir veya ayarlara yönlendirir
}
} catch (error) {
dispatch({ type: 'CLEAR_AUTH' });
throw error;
}
};
const logout = async () => {
try {
dispatch({ type: 'SET_USER_STATE', payload: UserState.LOGGING_OUT });
// Token yöneticisini temizle
TokenManager.cleanup();
// Auth0 oturumunu temizle
await AuthService.logout();
// Saklanan token'ları temizle
await SecureStorage.clearTokens();
// Etkinse biyometrik anahtarları temizle
if (state.biometricEnabled) {
await BiometricService.deleteBiometricKeys();
}
dispatch({ type: 'CLEAR_AUTH' });
} catch (error) {
console.error('Çıkış hatası:', error);
// Bir şey başarısız olsa bile çıkışı zorla
dispatch({ type: 'CLEAR_AUTH' });
}
};
const authenticateWithBiometrics = async (): Promise<boolean> => {
const result = await BiometricService.authenticateWithBiometrics();
if (result.success && result.tokens) {
const user = await AuthService.getUserInfo(result.tokens.accessToken);
dispatch({
type: 'SET_AUTH_DATA',
payload: { user, tokens: result.tokens }
});
return true;
}
return false;
};
const enableBiometrics = async (): Promise<boolean> => {
const enrolled = await BiometricService.enrollBiometrics();
if (enrolled) {
await AsyncStorage.setItem('@biometric_enrolled', 'true');
dispatch({ type: 'SET_BIOMETRIC_ENABLED', payload: true });
return true;
}
return false;
};
const value = {
state,
login,
logout,
authenticateWithBiometrics,
enableBiometrics,
checkAuthStatus
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};
Adım 8: Korumalı Route’lar Oluşturmak#
Kullanıcının gördüğü ekran tamamen userState’e bağlıdır.
// navigation/AuthNavigator.tsx
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { useAuth } from '../contexts/AuthContext';
import { UserState } from '../types/AuthState';
// Ekranları içe aktar
import LoginScreen from '../screens/LoginScreen';
import BiometricPromptScreen from '../screens/BiometricPromptScreen';
import HomeScreen from '../screens/HomeScreen';
import LoadingScreen from '../screens/LoadingScreen';
const Stack = createNativeStackNavigator();
export const AuthNavigator: React.FC = () => {
const { state } = useAuth();
if (state.userState === UserState.CHECKING_AUTH) {
return <LoadingScreen />;
}
return (
<NavigationContainer>
<Stack.Navigator screenOptions={{ headerShown: false }}>
{state.userState === UserState.AUTHENTICATED ? (
<Stack.Group>
<Stack.Screen name="Home" component={HomeScreen} />
{/* Diğer kimlik doğrulamalı ekranları ekle */}
</Stack.Group>
) : state.userState === UserState.REQUIRES_VERIFICATION ? (
<Stack.Screen name="BiometricPrompt" component={BiometricPromptScreen} />
) : (
<Stack.Screen name="Login" component={LoginScreen} />
)}
</Stack.Navigator>
</NavigationContainer>
);
};
Hata Modları ve Çözümleri#
Token Yenileme Hataları#
invalid_grant hatasıyla dönen bir yenileme çağrısı, neredeyse her zaman token’ın daha önce iptal edildiğini ya da süresinin dolduğunu gösterir; aşağıdaki kod isteğin sessizce başarısız olmasına izin vermek yerine bu durumu açıkça ele alır.
// TokenManager'da
if (error.error === 'invalid_grant') {
// Yerel token'ları temizle
await SecureStorage.clearTokens();
// Yeniden kimlik doğrulamayı zorla
EventEmitter.emit('auth:session-expired');
}
Biyometrik Kimlik Doğrulama Döngüsü#
Başarısız bir biyometrik kontrol, aynı istemi hemen yeniden tetiklememeli.
class BiometricService {
private failureCount = 0;
private readonly MAX_FAILURES = 3;
async authenticateWithBackoff(): Promise<AuthenticationResult> {
if (this.failureCount >= this.MAX_FAILURES) {
// Parola kimlik doğrulamasına geri dön
return { success: false, error: 'Maksimum deneme aşıldı' };
}
const result = await this.authenticateWithBiometrics();
if (!result.success) {
this.failureCount++;
} else {
this.failureCount = 0;
}
return result;
}
}
iOS’ta Arka Plan Yenilemesi#
iOS’ta kritik güncellemeleri sessiz push bildirimleri taşır. Arka plan yenilemesine tek başına güvenilemez, çünkü işletim sistemi arka planda ne kadar çalışma süresi verileceğini sıkı şekilde sınırlar.
// AppDelegate.m
- (void)application:(UIApplication *)application
didReceiveRemoteNotification:(NSDictionary *)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
// Token yenilemesini tetikle
[RNBackgroundFetch performFetchWithCompletionHandler:completionHandler];
}
Ağ Durumu İşleme#
Kararsız bir bağlantıda token yenilemesi istek ortasında başarısız olabilir. Yenileme çağrısını kuyruğa alın ve ağ izleyicisi cihazın tekrar çevrimiçi olduğunu bildirdiğinde yeniden çalıştırın.
class NetworkAwareTokenManager {
private refreshQueue: Array<() => Promise<void>> = [];
private isOnline = true;
constructor() {
NetInfo.addEventListener(state => {
const wasOffline = !this.isOnline;
this.isOnline = state.isConnected;
if (wasOffline && this.isOnline) {
this.processQueue();
}
});
}
async queueRefresh(): Promise<void> {
return new Promise((resolve, reject) => {
const task = async () => {
try {
await this.performTokenRefresh();
resolve();
} catch (error) {
reject(error);
}
};
if (this.isOnline) {
task();
} else {
this.refreshQueue.push(task);
}
});
}
private async processQueue(): Promise<void> {
while (this.refreshQueue.length > 0) {
const task = this.refreshQueue.shift();
if (task) await task();
}
}
}
Auth Akışını Sertleştirmek#
Token Depolama Güvenliği#
iOS ve Android’in güvenli depolama için kendi temel gereksinimleri var.
- iOS:
kSecAccessControlBiometryCurrentSetile Keychain - Android: Kullanıcı kimlik doğrulaması gerektiren Android Keystore
Sertifika Sabitleme#
Auth0 endpoint’leri için sertifika sabitleme, React Native 0.78+ ve Network Security Config gerektirir.
<!-- ios/YourApp/Info.plist -->
<key>NSAppTransportSecurity</key>
<dict>
<key>NSPinnedDomains</key>
<dict>
<key>your-tenant.auth0.com</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSPinnedCAIdentities</key>
<array>
<dict>
<key>SPKI-SHA256-BASE64</key>
<string>YOUR_PIN_HERE</string>
</dict>
</array>
</dict>
</dict>
</dict>
<!-- android/app/src/main/res/xml/network_security_config.xml -->
<network-security-config>
<domain-config>
<domain includeSubdomains="true">your-tenant.auth0.com</domain>
<pin-set>
<pin digest="SHA-256">YOUR_PIN_HERE</pin>
</pin-set>
</domain-config>
</network-security-config>
Jailbreak/Root Tespiti#
import JailMonkey from 'jail-monkey';
const checkDeviceSecurity = (): SecurityStatus => {
if (JailMonkey.isJailBroken()) {
return { secure: false, reason: 'Cihaz jailbreak/root yapılmış' };
}
if (JailMonkey.isDebuggedMode()) {
return { secure: false, reason: 'Debugger tespit edildi' };
}
return { secure: true };
};
// Not: React Native New Architecture ile bazı tespit yöntemleri güncelleme gerektirebilir
// Hedef cihazlarda ve işletim sistemi sürümlerinde iyice test edin
Token Rotasyonu#
// Auth0 Dashboard > Applications > Settings > Refresh Token Rotation
{
"rotation": {
"enabled": true,
"leeway": 60
}
}
Güvenli İletişim#
Her token işlemi şifreli bir kanaldan geçmeli.
class SecureAPIClient {
private async makeSecureRequest(url: string, options: RequestInit): Promise<Response> {
const tokens = await SecureStorage.getTokens();
if (!tokens || !tokens.accessToken) {
throw new Error('Geçerli access token yok');
}
const response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${tokens.accessToken}`,
'X-Request-ID': generateRequestId(),
'X-App-Version': getAppVersion()
}
});
if (response.status === 401) {
// Token süresi dolmuş olabilir, yenilemeyi dene
await TokenManager.performTokenRefresh();
// İsteği yeni token ile tekrar dene
const newTokens = await SecureStorage.getTokens();
return fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${newTokens.accessToken}`
}
});
}
return response;
}
}
Yenileme ve Depolama Yükünü Azaltmak#
Token Önbellekleme Stratejisi#
class TokenCache {
private cache: Map<string, { value: any; expires: number }> = new Map();
set(key: string, value: any, ttl: number): void {
this.cache.set(key, {
value,
expires: Date.now() + ttl
});
}
get(key: string): any | null {
const item = this.cache.get(key);
if (!item) return null;
if (Date.now() > item.expires) {
this.cache.delete(key);
return null;
}
return item.value;
}
}
TTL’li bellek içi önbellek gibi bu yapı, her çağrıda Keychain’i tekrar okumayı önler.
Toplu Token İşlemleri#
class BatchedSecureStorage {
private pendingWrites: Map<string, any> = new Map();
private writeTimer: NodeJS.Timeout | null = null;
async set(key: string, value: any): Promise<void> {
this.pendingWrites.set(key, value);
this.scheduleWrite();
}
private scheduleWrite(): void {
if (this.writeTimer) return;
this.writeTimer = setTimeout(() => {
this.flushWrites();
this.writeTimer = null;
}, 100);
}
private async flushWrites(): Promise<void> {
const writes = Array.from(this.pendingWrites.entries());
this.pendingWrites.clear();
await Promise.all(
writes.map(([key, value]) =>
Keychain.setInternetCredentials(this.SERVICE, key, value)
)
);
}
}
Yazmaları böyle toplulaştırmak uygulamanın Keychain’e ne sıklıkla dokunduğunu azaltır.
Önleyici Token Yenileme#
Süresi dolmadan yenilenen bir token, çağrıyı eş zamanlı bir round trip’te asla bloklamaz.
class PreemptiveTokenManager {
private readonly PREEMPTIVE_REFRESH_WINDOW = 10 * 60 * 1000; // 10 dakika
async getValidToken(): Promise<string> {
const tokens = await SecureStorage.getTokens();
if (!tokens) {
throw new Error('Token mevcut değil');
}
const timeUntilExpiry = tokens.expiresAt - Date.now();
// Pencere içindeyse önleyici olarak yenile
if (timeUntilExpiry < this.PREEMPTIVE_REFRESH_WINDOW) {
// Yenilemenin tamamlanmasını bekleme
this.performTokenRefresh().catch(console.error);
}
return tokens.accessToken;
}
}
Varsayılanı Genişletmek#
Çekirdek akış oturunca birkaç ekleme onu tamamlar:
- Çok faktörlü kimlik doğrulama (TOTP veya SMS tabanlı 2FA)
- Cihaz güveni: parmak izi ve güvenilir cihaz yönetimi
- Oturum analitiği: süre, yenileme kalıpları ve güvenlik olayları
- Çevrimdışı destek: yerel token doğrulama ve senkronizasyon kuyruğu
- Uzun süren oturumlar için biyometrik yedek mekanizma ve değişiklik algılama
Bunu production’a taşımadan önce Auth0 tenant’ını mobil için yapılandırın, token yenileme hataları için izleme kurun, hata takibi ve uyarı sistemini devreye alın, güvenlik olaylarını loglayın ve uygulamanın gerçekten hedeflediği cihaz ve işletim sistemi sürümlerinde test edin.
Sonuç#
Bir Auth0 tenant’ıyla konuşan çoğu React Native istemcisinde varsayılan ayakta kalır. Kararsız ağları, uzun süre arka planda kalmayı ve iptal edilmiş refresh token’ları, kullanıcıyı gereksiz yere giriş ekranına atmadan atlatır.
İki durumda bu varsayılandan sapın. Uygulamanın uzun süre tamamen çevrimdışı kullanılabilir kalması gerekiyorsa kısa ömürlü access token’lar sizi yerel token doğrulamaya ve bir senkronizasyon kuyruğuna zorlar; bu da depolama tasarımını değiştirir. Arka plan yenilemesi bir kolaylık değil de sistemin taşıyıcı parçasıysa iOS arka plan fetch’i bu yükü kaldıramaz: o yolu sessiz push bildirimlerine taşıyın.
Kaynaklar#
- Auth0 React Native SDK Hızlı Başlangıç (yeni sekmede açılır) - Token rotasyonu ve güvenli depolama yapılandırması dahil resmi entegrasyon rehberi
- Auth0 React Native SDK - GitHub (yeni sekmede açılır) - react-native-auth0 kütüphanesi için kaynak, örnekler ve değişiklik günlüğü
- JSON Web Token’ları - Auth0 Belgeleri (yeni sekmede açılır) - Erişim ve yenileme token’ı yönetimi için JWT yapısı ve doğrulaması
- Erişim Token’ı Alma - Auth0 Belgeleri (yeni sekmede açılır) - Token yenileme ve sessiz kimlik doğrulama kalıpları
- expo-local-authentication - Expo Belgeleri (yeni sekmede açılır) - React Native için biyometrik prompt, Face ID ve Touch ID API’si
- Expo Uygulamanıza Oturum Açma Ekleme - Auth0 Belgeleri (yeni sekmede açılır) - Expo geliştirme derlemeleri ve biyometrik yedek ile Auth0 entegrasyonu
- OAuth 2.0 Yetkilendirme Çerçevesi - RFC 6749 (yeni sekmede açılır) - Auth0 mobil token akışlarının temelini oluşturan, PKCE’li yetkilendirme kodu akışı
İlgili yazılar
React Native Expo uygulamasına Sentry ekleme rehberi: SDK kurulumu, Expo Router enstrümantasyonu, session replay ve EAS için source map yükleme.
react-native · expo · monitoring +2
TypeScript ile internal sistemleriniz için custom MCP server geliştirin: authentication, monitoring ve Kubernetes deployment örnekleriyle uçtan uca rehber.
typescript · mcp · nodejs +5
Micro frontend'lerde Auth0 multi-audience authentication: token yönetim stratejileri ve React Native'de WebView tabanlı uygulamalarla silent authentication.
authentication · oauth2 · react-native +1
E-posta, şifre ve SMS OTP kullanan SaaS ekipleri için Cognito passkey geçiş rehberi: yapılandırma, enrollment ve SMS'in yerini alan dört basamaklı recovery merdiveni.
authentication · security · aws +2
Sunucu güdümlü arayüz, sunucu tarafı kompozisyonun mobil karşılığıdır. Zor kısmı JSON render etmek değil, eski sürümlerde ayakta kalan sürümlenmiş bileşen sözleşmesidir.
mobile · react-native · architecture