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.
下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。
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
$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. 下の青いボタンを押して
rn-auth.zipをダウンロード - 2. ZIPファイルをダブルクリックで解凍 →
rn-authフォルダができる - 3. そのフォルダを
C:\Users\あなたの名前\.claude\skills\(Win)または~/.claude/skills/(Mac)へ移動 - 4. Claude Code を再起動
⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。
🎯 このSkillでできること
下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。
📦 インストール方法 (3ステップ)
- 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
- 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
- 3. 展開してできたフォルダを、ホームフォルダの
.claude/skills/に置く- · macOS / Linux:
~/.claude/skills/ - · Windows:
%USERPROFILE%\.claude\skills\
- · macOS / Linux:
Claude Code を再起動すれば完了。「このSkillを使って…」と話しかけなくても、関連する依頼で自動的に呼び出されます。
詳しい使い方ガイドを見る →- 最終更新
- 2026-05-18
- 取得日時
- 2026-05-18
- 同梱ファイル
- 1
📖 Skill本文(日本語訳)
※ 原文(英語/中国語)を Gemini で日本語化したものです。Claude 自身は原文を読みます。誤訳がある場合は原文をご確認ください。
React Native 認証 (Expo)
コアパターン
OAuth のための Expo AuthSession
OAuth フローには、expo-auth-session を expo-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'],
});
よくある落とし穴
maybeCompleteAuthSession()の欠落 - これがモジュールレベルにないと、認証リダイレクトは何も表示されずに失敗します- 間違ったクライアント ID - iOS は iOS クライアント ID が必要ですが、バックエンド検証には Web クライアント ID が必要です
- スキームの不一致 -
app.jsonのスキームは、Google Cloud Console のリダイレクト URI と一致する必要があります - 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
- Missing
maybeCompleteAuthSession()- Auth redirects fail silently without this at module level - Wrong client ID - iOS needs the iOS client ID, but backend verification needs the web client ID
- Scheme mismatch -
app.jsonscheme must match Google Cloud Console redirect URI - 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.jsonscheme 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
- Clear all tokens:
await tokenStorage.clear() - Force kill app
- Reopen and verify redirect to login
- Complete sign-in flow
- Force kill and reopen - should stay logged in