jpskill.com
🛠️ 開発・MCP コミュニティ

rn-auth

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.

⚡ おすすめ: コマンド1行でインストール(60秒)

下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。

🍎 Mac / 🐧 Linux
mkdir -p ~/.claude/skills && cd ~/.claude/skills && curl -L -o rn-auth.zip https://jpskill.com/download/17856.zip && unzip -o rn-auth.zip && rm rn-auth.zip
🪟 Windows (PowerShell)
$d = "$env:USERPROFILE\.claude\skills"; ni -Force -ItemType Directory $d | Out-Null; iwr https://jpskill.com/download/17856.zip -OutFile "$d\rn-auth.zip"; Expand-Archive "$d\rn-auth.zip" -DestinationPath $d -Force; ri "$d\rn-auth.zip"

完了後、Claude Code を再起動 → 普通に「動画プロンプト作って」のように話しかけるだけで自動発動します。

💾 手動でダウンロードしたい(コマンドが難しい人向け)
  1. 1. 下の青いボタンを押して rn-auth.zip をダウンロード
  2. 2. ZIPファイルをダブルクリックで解凍 → rn-auth フォルダができる
  3. 3. そのフォルダを C:\Users\あなたの名前\.claude\skills\(Win)または ~/.claude/skills/(Mac)へ移動
  4. 4. Claude Code を再起動

⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。

🎯 このSkillでできること

下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。

📦 インストール方法 (3ステップ)

  1. 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
  2. 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
  3. 3. 展開してできたフォルダを、ホームフォルダの .claude/skills/ に置く
    • · macOS / Linux: ~/.claude/skills/
    • · Windows: %USERPROFILE%\.claude\skills\

Claude Code を再起動すれば完了。「このSkillを使って…」と話しかけなくても、関連する依頼で自動的に呼び出されます。

詳しい使い方ガイドを見る →
最終更新
2026-05-18
取得日時
2026-05-18
同梱ファイル
1

📖 Skill本文(日本語訳)

※ 原文(英語/中国語)を Gemini で日本語化したものです。Claude 自身は原文を読みます。誤訳がある場合は原文をご確認ください。

React Native 認証 (Expo)

コアパターン

OAuth のための Expo AuthSession

OAuth フローには、expo-auth-sessionexpo-web-browser と共に使用します。

import * as AuthSession from 'expo-auth-session';
import * as WebBrowser from 'expo-web-browser';
import * as Google from 'expo-auth-session/providers/google';

// 重要: 正しいリダイレクト処理のために、モジュールレベルでこれを呼び出してください
WebBrowser.maybeCompleteAuthSession();

// コンポーネント内
const [request, response, promptAsync] = Google.useAuthRequest({
  iosClientId: 'YOUR_IOS_CLIENT_ID.apps.googleusercontent.com',
  webClientId: 'YOUR_WEB_CLIENT_ID.apps.googleusercontent.com', // バックエンド検証用
  scopes: ['profile', 'email'],
});

よくある落とし穴

  1. maybeCompleteAuthSession() の欠落 - これがモジュールレベルにないと、認証リダイレクトは何も表示されずに失敗します
  2. 間違ったクライアント ID - iOS は iOS クライアント ID が必要ですが、バックエンド検証には Web クライアント ID が必要です
  3. スキームの不一致 - app.json のスキームは、Google Cloud Console のリダイレクト URI と一致する必要があります
  4. Expo Go vs スタンドアロン - リダイレクト URI が異なります。AuthSession.makeRedirectUri() を使用して両方に対応します

トークンストレージ

トークンには expo-secure-store を使用します (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);
  },
};

認証コンテキストパターン

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(() => {
    // マウント時にセッションを復元
    async function restore() {
      try {
        const savedToken = await tokenStorage.get();
        if (savedToken) {
          // 信頼する前に、バックエンドでトークンを検証します
          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;
};

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

バックエンド統合

認証ヘッダーの送信

// 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) {
    // トークンが期限切れ - リフレッシュを試みるか、強制的にログアウトします
    const refreshed = await attemptTokenRefresh();
    if (!refreshed) {
      await tokenStorage.clear();
      // 認証状態の更新をトリガーします (イベントを発行するか、コールバックを使用します)
    }
  }

  return response;
}

Google トークン検証 (FastAPI バックエンド)

# 参考: バックエンドは次のように Google トークンを検証する必要があります
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  # ここでは iOS ではなく WEB クライアント ID を使用します
    )
    return {
        "google_id": idinfo["sub"],
        "email": idinfo["email"],
        "name": idinfo.get("name"),
    }

認証問題のデバッグ

リダイレクト URI の構成を確認する

// 使用されているリダイレクト URI をログに記録します
console.log('Redirect URI:', AuthSession.makeRedirectUri());

これを次の場所で構成されているものと比較してください。

  • Google Cloud Console > 認証情報 > OAuth 2.0 クライアント ID
  • app.json スキームフィールド

一般的なエラーパターン

(原文がここで切り詰められています)

📜 原文 SKILL.md(Claudeが読む英語/中国語)を展開

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

Error Likely Cause
"redirect_uri_mismatch" Redirect URI in console doesn't match app
Auth popup opens but nothing happens Missing maybeCompleteAuthSession()
Works in Expo Go, fails in build Using Expo Go redirect URI in standalone config
Token validation fails on backend Using 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