rn-auth

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

React Native Authentication (Expo)

React Native 认证(Expo)

Core Patterns

核心模式

Expo AuthSession for OAuth

用于OAuth的Expo AuthSession

Use
expo-auth-session
with
expo-web-browser
for OAuth flows:
typescript
import * as AuthSession from 'expo-auth-session';
import * as WebBrowser from 'expo-web-browser';
import * as Google from 'expo-auth-session/providers/google';

// Critical: Call this at module level for proper redirect handling
WebBrowser.maybeCompleteAuthSession();

// Inside component
const [request, response, promptAsync] = Google.useAuthRequest({
  iosClientId: 'YOUR_IOS_CLIENT_ID.apps.googleusercontent.com',
  webClientId: 'YOUR_WEB_CLIENT_ID.apps.googleusercontent.com', // For backend verification
  scopes: ['profile', 'email'],
});
expo-auth-session
expo-web-browser
配合使用以实现OAuth流程:
typescript
import * as AuthSession from 'expo-auth-session';
import * as WebBrowser from 'expo-web-browser';
import * as Google from 'expo-auth-session/providers/google';

// Critical: Call this at module level for proper redirect handling
WebBrowser.maybeCompleteAuthSession();

// Inside component
const [request, response, promptAsync] = Google.useAuthRequest({
  iosClientId: 'YOUR_IOS_CLIENT_ID.apps.googleusercontent.com',
  webClientId: 'YOUR_WEB_CLIENT_ID.apps.googleusercontent.com', // For backend verification
  scopes: ['profile', 'email'],
});

Common Pitfalls

常见陷阱

  1. Missing
    maybeCompleteAuthSession()
    - Auth redirects fail silently without this at module level
  2. Wrong client ID - iOS needs the iOS client ID, but backend verification needs the web client ID
  3. Scheme mismatch -
    app.json
    scheme must match Google Cloud Console redirect URI
  4. Expo Go vs standalone - Different redirect URIs; use
    AuthSession.makeRedirectUri()
    to handle both
  1. 缺少
    maybeCompleteAuthSession()
    - 如果不在模块级别调用此方法,认证重定向会静默失败
  2. 客户端ID错误 - iOS需要使用iOS客户端ID,但后端验证需要使用Web客户端ID
  3. Scheme不匹配 -
    app.json
    中的scheme必须与Google Cloud Console中的重定向URI匹配
  4. Expo Go与独立构建差异 - 两者的重定向URI不同;使用
    AuthSession.makeRedirectUri()
    来处理这两种情况

Token Storage

令牌存储

Use
expo-secure-store
for tokens (not AsyncStorage):
typescript
import * as SecureStore from 'expo-secure-store';

const TOKEN_KEY = 'auth_token';
const REFRESH_KEY = 'refresh_token';

export const tokenStorage = {
  async save(token: string, refresh?: string) {
    await SecureStore.setItemAsync(TOKEN_KEY, token);
    if (refresh) {
      await SecureStore.setItemAsync(REFRESH_KEY, refresh);
    }
  },
  
  async get() {
    return SecureStore.getItemAsync(TOKEN_KEY);
  },
  
  async getRefresh() {
    return SecureStore.getItemAsync(REFRESH_KEY);
  },
  
  async clear() {
    await SecureStore.deleteItemAsync(TOKEN_KEY);
    await SecureStore.deleteItemAsync(REFRESH_KEY);
  },
};
使用
expo-secure-store
存储令牌(而非AsyncStorage):
typescript
import * as SecureStore from 'expo-secure-store';

const TOKEN_KEY = 'auth_token';
const REFRESH_KEY = 'refresh_token';

export const tokenStorage = {
  async save(token: string, refresh?: string) {
    await SecureStore.setItemAsync(TOKEN_KEY, token);
    if (refresh) {
      await SecureStore.setItemAsync(REFRESH_KEY, refresh);
    }
  },
  
  async get() {
    return SecureStore.getItemAsync(TOKEN_KEY);
  },
  
  async getRefresh() {
    return SecureStore.getItemAsync(REFRESH_KEY);
  },
  
  async clear() {
    await SecureStore.deleteItemAsync(TOKEN_KEY);
    await SecureStore.deleteItemAsync(REFRESH_KEY);
  },
};

Auth Context Pattern

认证上下文模式

typescript
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';

type AuthState = {
  token: string | null;
  user: User | null;
  isLoading: boolean;
  signIn: (token: string, user: User) => Promise<void>;
  signOut: () => Promise<void>;
};

const AuthContext = createContext<AuthState | null>(null);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [token, setToken] = useState<string | null>(null);
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    // Restore session on mount
    async function restore() {
      try {
        const savedToken = await tokenStorage.get();
        if (savedToken) {
          // Validate token with backend before trusting it
          const userData = await validateToken(savedToken);
          setToken(savedToken);
          setUser(userData);
        }
      } catch {
        await tokenStorage.clear();
      } finally {
        setIsLoading(false);
      }
    }
    restore();
  }, []);

  const signIn = async (newToken: string, userData: User) => {
    await tokenStorage.save(newToken);
    setToken(newToken);
    setUser(userData);
  };

  const signOut = async () => {
    await tokenStorage.clear();
    setToken(null);
    setUser(null);
  };

  return (
    <AuthContext.Provider value={{ token, user, isLoading, signIn, signOut }}>
      {children}
    </AuthContext.Provider>
  );
}

export const useAuth = () => {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error('useAuth must be inside AuthProvider');
  return ctx;
};
typescript
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';

type AuthState = {
  token: string | null;
  user: User | null;
  isLoading: boolean;
  signIn: (token: string, user: User) => Promise<void>;
  signOut: () => Promise<void>;
};

const AuthContext = createContext<AuthState | null>(null);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [token, setToken] = useState<string | null>(null);
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    // Restore session on mount
    async function restore() {
      try {
        const savedToken = await tokenStorage.get();
        if (savedToken) {
          // Validate token with backend before trusting it
          const userData = await validateToken(savedToken);
          setToken(savedToken);
          setUser(userData);
        }
      } catch {
        await tokenStorage.clear();
      } finally {
        setIsLoading(false);
      }
    }
    restore();
  }, []);

  const signIn = async (newToken: string, userData: User) => {
    await tokenStorage.save(newToken);
    setToken(newToken);
    setUser(userData);
  };

  const signOut = async () => {
    await tokenStorage.clear();
    setToken(null);
    setUser(null);
  };

  return (
    <AuthContext.Provider value={{ token, user, isLoading, signIn, signOut }}>
      {children}
    </AuthContext.Provider>
  );
}

export const useAuth = () => {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error('useAuth must be inside AuthProvider');
  return ctx;
};

Protected Routes with Expo Router

使用Expo Router的受保护路由

typescript
// app/_layout.tsx
import { Slot, useRouter, useSegments } from 'expo-router';
import { useAuth } from '@/contexts/auth';
import { useEffect } from 'react';

export default function RootLayout() {
  const { token, isLoading } = useAuth();
  const segments = useSegments();
  const router = useRouter();

  useEffect(() => {
    if (isLoading) return;

    const inAuthGroup = segments[0] === '(auth)';

    if (!token && !inAuthGroup) {
      router.replace('/(auth)/login');
    } else if (token && inAuthGroup) {
      router.replace('/(app)/home');
    }
  }, [token, isLoading, segments]);

  if (isLoading) {
    return <LoadingScreen />;
  }

  return <Slot />;
}
typescript
// app/_layout.tsx
import { Slot, useRouter, useSegments } from 'expo-router';
import { useAuth } from '@/contexts/auth';
import { useEffect } from 'react';

export default function RootLayout() {
  const { token, isLoading } = useAuth();
  const segments = useSegments();
  const router = useRouter();

  useEffect(() => {
    if (isLoading) return;

    const inAuthGroup = segments[0] === '(auth)';

    if (!token && !inAuthGroup) {
      router.replace('/(auth)/login');
    } else if (token && inAuthGroup) {
      router.replace('/(app)/home');
    }
  }, [token, isLoading, segments]);

  if (isLoading) {
    return <LoadingScreen />;
  }

  return <Slot />;
}

Backend Integration

后端集成

Sending Auth Headers

发送认证请求头

typescript
// api/client.ts
import { tokenStorage } from '@/utils/tokenStorage';

const API_BASE = process.env.EXPO_PUBLIC_API_URL;

async function authFetch(path: string, options: RequestInit = {}) {
  const token = await tokenStorage.get();
  
  const response = await fetch(`${API_BASE}${path}`, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      ...(token && { Authorization: `Bearer ${token}` }),
      ...options.headers,
    },
  });

  if (response.status === 401) {
    // Token expired - try refresh or force logout
    const refreshed = await attemptTokenRefresh();
    if (!refreshed) {
      await tokenStorage.clear();
      // Trigger auth state update (emit event or use callback)
    }
  }

  return response;
}
typescript
// api/client.ts
import { tokenStorage } from '@/utils/tokenStorage';

const API_BASE = process.env.EXPO_PUBLIC_API_URL;

async function authFetch(path: string, options: RequestInit = {}) {
  const token = await tokenStorage.get();
  
  const response = await fetch(`${API_BASE}${path}`, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      ...(token && { Authorization: `Bearer ${token}` }),
      ...options.headers,
    },
  });

  if (response.status === 401) {
    // Token expired - try refresh or force logout
    const refreshed = await attemptTokenRefresh();
    if (!refreshed) {
      await tokenStorage.clear();
      // Trigger auth state update (emit event or use callback)
    }
  }

  return response;
}

Google Token Verification (FastAPI backend)

Google令牌验证(FastAPI后端)

python
undefined
python
undefined

For reference: backend should verify Google tokens like this

For reference: backend should verify Google tokens like this

from google.oauth2 import id_token from google.auth.transport import requests
def verify_google_token(token: str, client_id: str) -> dict: """Verify Google ID token and return user info.""" idinfo = id_token.verify_oauth2_token( token, requests.Request(), client_id # Use WEB client ID here, not iOS ) return { "google_id": idinfo["sub"], "email": idinfo["email"], "name": idinfo.get("name"), }
undefined
from google.oauth2 import id_token from google.auth.transport import requests
def verify_google_token(token: str, client_id: str) -> dict: """Verify Google ID token and return user info.""" idinfo = id_token.verify_oauth2_token( token, requests.Request(), client_id # Use WEB client ID here, not iOS ) return { "google_id": idinfo["sub"], "email": idinfo["email"], "name": idinfo.get("name"), }
undefined

Debugging Auth Issues

调试认证问题

Check redirect URI configuration

检查重定向URI配置

typescript
// Log the redirect URI being used
console.log('Redirect URI:', AuthSession.makeRedirectUri());
Compare this with what's configured in:
  • Google Cloud Console > Credentials > OAuth 2.0 Client IDs
  • app.json
    scheme field
typescript
// Log the redirect URI being used
console.log('Redirect URI:', AuthSession.makeRedirectUri());
将此URI与以下位置的配置进行对比:
  • Google Cloud Console > 凭据 > OAuth 2.0 客户端ID
  • app.json
    中的scheme字段

Common error patterns

常见错误模式

ErrorLikely Cause
"redirect_uri_mismatch"Redirect URI in console doesn't match app
Auth popup opens but nothing happensMissing
maybeCompleteAuthSession()
Works in Expo Go, fails in buildUsing Expo Go redirect URI in standalone config
Token validation fails on backendUsing iOS client ID instead of web client ID for verification
错误可能原因
"redirect_uri_mismatch"控制台中的重定向URI与应用不匹配
认证弹窗打开但无反应缺少
maybeCompleteAuthSession()
在Expo Go中正常工作,构建后失败在独立构建配置中使用了Expo Go的重定向URI
令牌在后端验证失败验证时使用了iOS客户端ID而非Web客户端ID

Test auth flow

测试认证流程

  1. Clear all tokens:
    await tokenStorage.clear()
  2. Force kill app
  3. Reopen and verify redirect to login
  4. Complete sign-in flow
  5. Force kill and reopen - should stay logged in
  1. 清除所有令牌:
    await tokenStorage.clear()
  2. 强制关闭应用
  3. 重新打开应用,验证是否重定向到登录页
  4. 完成登录流程
  5. 强制关闭并重新打开应用 - 应保持登录状态