Back to list
aiskillstore

rn-auth

by aiskillstore

Security-audited skills for Claude, Codex & Claude Code. One-click install, quality verified.

102🍴 3📅 Jan 23, 2026

SKILL.md


name: rn-auth description: React Native authentication patterns for Expo apps. Use when implementing login flows, Google/Apple sign-in, token management, session handling, or debugging auth issues in Expo/React Native.

React Native Authentication (Expo)

Core Patterns

Expo AuthSession for OAuth

Use expo-auth-session with expo-web-browser for OAuth flows:

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

Token Storage

Use expo-secure-store for tokens (not AsyncStorage):

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

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

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

// 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)

# 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"),
    }

Debugging Auth Issues

Check redirect URI configuration

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

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

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

Score

Total Score

60/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

+5
最近の活動

1ヶ月以内に更新

+10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

+5

Reviews

💬

Reviews coming soon