SDK

React Native SDK

@authon/react-nativeReact Native와 Expo에서 쓸 수 있는 훅, 소셜 버튼, 보안 토큰 저장소, 저수준 OAuth 헬퍼를 제공합니다.

설치

토큰 영속화에는 `expo-secure-store`를 사용하세요. 권장 OAuth 플로우에는 `expo-web-browser`가 필수입니다.

bash
npm install @authon/react-native react-native-svg
npx expo install expo-secure-store expo-web-browser

Provider 설정

App.tsx
import { AuthonProvider } from "@authon/react-native";
import * as SecureStore from "expo-secure-store";

const storage = {
  getItem: (key: string) => SecureStore.getItemAsync(key),
  setItem: (key: string, value: string) => SecureStore.setItemAsync(key, value),
  removeItem: (key: string) => SecureStore.deleteItemAsync(key),
};

export default function App() {
  return (
    <AuthonProvider publishableKey="pk_live_..." storage={storage}>
      <Navigation />
    </AuthonProvider>
  );
}

이메일 / 비밀번호

screens/LoginScreen.tsx
import { useState } from "react";
import { View, TextInput, Button, Text, ActivityIndicator } from "react-native";
import { useAuthon, useUser } from "@authon/react-native";

export function LoginScreen() {
  const { isLoaded } = useUser();
  const { signIn, signOut, user, isSignedIn } = useAuthon();
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const handleSignIn = async () => {
    setLoading(true);
    setError(null);
    try {
      await signIn({ strategy: "email_password", email, password });
    } catch (err: any) {
      setError(err.message ?? "Sign-in failed");
    } finally {
      setLoading(false);
    }
  };

  if (!isLoaded) return <ActivityIndicator />;

  if (isSignedIn) {
    return (
      <View style={{ padding: 24, gap: 12 }}>
        <Text>Welcome, {user?.displayName ?? user?.email}</Text>
        <Button title="Sign out" onPress={signOut} />
      </View>
    );
  }

  return (
    <View style={{ padding: 24, gap: 12 }}>
      <TextInput placeholder="Email" value={email} onChangeText={setEmail} autoCapitalize="none" />
      <TextInput placeholder="Password" value={password} onChangeText={setPassword} secureTextEntry />
      {error ? <Text style={{ color: "red" }}>{error}</Text> : null}
      <Button title={loading ? "Signing in..." : "Sign in"} onPress={handleSignIn} disabled={loading} />
    </View>
  );
}

소셜 버튼

tsx
import { SocialButtons } from "@authon/react-native";

export function SocialLoginSection() {
  return (
    <SocialButtons
      onSuccess={() => console.log("Signed in")}
      onError={(error) => console.error(error)}
    />
  );
}

권장 Expo OAuth 플로우

권장 방식은 `flow=redirect`로 `expo-web-browser` 세션을 열고 HTTPS 브리지 페이지를 거쳐 앱으로 복귀하는 것입니다. 전체 체인이 HTTP 3xx 리다이렉트로 구성되어야 합니다. 브리지 페이지에서 JS 기반 리다이렉트에 의존하면 Android Custom Tab에서 브라우저가 안 닫히는 문제가 발생합니다.

GoogleButton.tsx
import * as WebBrowser from "expo-web-browser";
import { Alert, Button } from "react-native";
import { useAuthon } from "@authon/react-native";

const API_URL = "https:0
const PUBLISHABLE_KEY = "pk_live_...";
const APP_DEEP_LINK = "myapp://oauth-callback";

// HTTPS bridge page — Authon redirects here via HTTP 3xx,
// then this page redirects to your app deep link.
const RETURN_TO = "https:4

async function requestOAuthUrl(provider: string) {
  const params = new URLSearchParams({
    redirectUri: 11,
    flow: "redirect",
    returnTo: RETURN_TO,
  });

  const response = await fetch(
    12,
    { headers: { "x-api-key": PUBLISHABLE_KEY } },
  );

  if (!response.ok) {
    throw new Error(await response.text());
  }

  return response.json() as Promise<{ url: string; state: string }>;
}

export function GoogleButton() {
  const { completeOAuth, getToken, signOut } = useAuthon();

  const handlePress = async () => {
    try {
      const { url, state } = await requestOAuthUrl("google");

      5
      const pollPromise = completeOAuth(state);

      6
      await WebBrowser.openAuthSessionAsync(url, APP_DEEP_LINK);

      7
      await pollPromise;

      const authonAccessToken = getToken();
      8
    } catch (err: any) {
      9
      10
      Alert.alert("Login failed", err.message ?? "OAuth error");
    }
  };

  return <Button title="Continue with Google" onPress={handlePress} />;
}

HTTPS 브리지 페이지

브리지 페이지는 Authon 리다이렉트에서 `authon_oauth_state`를 받아 앱의 딥 링크로 전달합니다. JS `window.location.replace` 대신 서버 사이드 HTTP 302 리다이렉트를 사용하세요. iOS/Android 양쪽에서 브라우저 자동 종료 안정성에 가장 결정적인 요소입니다.

mobile-callback
<!-- Recommended: HTTP redirect (server-side) -->
<!-- Your server at /authon/mobile-callback should do: -->
<!-- 302 redirect to: myapp://oauth-callback?state=xxx -->
<!-- This is more reliable than JS-based redirect on Android Custom Tabs. -->

<!-- If you cannot do server-side redirect, use this HTML fallback: -->
<!doctype html>
<html>
  <body>
    <script>
      const params = new URLSearchParams(window.location.search);
      const state = params.get("authon_oauth_state");
      const error = params.get("authon_oauth_error");

      const target = new URL("myapp://oauth-callback");
      if (state) target.searchParams.set("state", state);
      if (error) target.searchParams.set("error", error);

      // Fallback: JS redirect. Less reliable on Android Custom Tabs.
      window.location.replace(target.toString());
    </script>
  </body>
</html>

로그아웃 (중요)

로그아웃 시 반드시 `signOut()`을 호출하세요. 로컬 토큰과 Authon 서버 세션을 모두 정리합니다. 여러 OAuth 프로바이더를 전환하며 사용하는 경우(예: 구글 → 카카오) Authon 세션을 정리하지 않으면 다음 로그인에서 stale session 간섭이 발생합니다.

LogoutButton.tsx
import { useAuthon } from "@authon/react-native";

function LogoutButton() {
  const { signOut } = useAuthon();

  const handleLogout = async () => {
    // signOut() calls DELETE /v1/auth/signout on Authon,
    // clearing the Authon session + local tokens.
    // This is critical when switching between providers
    // (e.g. Google → Kakao) to avoid stale session conflicts.
    await signOut();

    // If your app has its own backend session, clear it too:
    // await myBackend.post("/api/logout");
  };

  return <Button title="Sign out" onPress={handleLogout} />;
}

계정 삭제

Authon은 `DELETE /v1/auth/me`로 유저가 직접 계정을 삭제할 수 있습니다. SDK에서는 `deleteAccount()`로 사용할 수 있으며, Apple App Store와 Google Play 정책 준수에 필요합니다.

DeleteAccountButton.tsx
import { useAuthon } from "@authon/react-native";
import { Alert } from "react-native";

function DeleteAccountButton() {
  const { deleteAccount, signOut } = useAuthon();

  const handleDelete = () => {
    Alert.alert(
      "Delete Account",
      "This will permanently delete your account and all data. This cannot be undone.",
      [
        { text: "Cancel", style: "cancel" },
        {
          text: "Delete",
          style: "destructive",
          onPress: async () => {
            try {
              await deleteAccount();
              // User is now signed out and account is deleted.
            } catch (err: any) {
              Alert.alert("Error", err.message ?? "Failed to delete account");
            }
          },
        },
      ],
    );
  };

  return <Button title="Delete Account" onPress={handleDelete} color="red" />;
}
중요한 주의사항
  • OAuth 제공자에는 `myapp://...`를 직접 등록하지 마세요. 제공자 redirect URI는 항상 `{apiUrl}/v1/auth/oauth/redirect`여야 합니다.
  • `returnTo`에 앱 콜백 브리지 URL을 넣습니다. 이 값은 HTTPS URL이어야 하고, 해당 origin은 `ALLOWED_REDIRECT_ORIGINS`에 포함돼야 합니다.
  • 브리지 페이지에서는 JS 리다이렉트가 아닌 HTTP 3xx 서버 리다이렉트를 사용하세요. Android Custom Tab은 중간 페이지의 JS를 안정적으로 실행하지 못해 브라우저가 닫히지 않을 수 있습니다.
  • 리다이렉트 체인: Provider → api.authon.dev → HTTPS 브리지(302) → myapp://oauth-callback. 모든 전환이 HTTP 리다이렉트여야 합니다.
  • `completeOAuth()`는 poll 응답이 `status=error`이면 즉시 reject합니다. 반드시 try/catch로 감싸세요. 에러를 무시하면 로딩 상태가 무한 지속됩니다.
  • 로그아웃 시 반드시 `signOut()`을 호출하세요. 앱 로컬 상태만 지우고 Authon 세션을 남겨두면 프로바이더 전환 시 문제가 생깁니다.
  • 앱이 별도의 백엔드 세션을 가진 구조라면 `completeOAuth()` 직후 `getToken()`을 백엔드에 전달해 자체 세션을 발급받으세요.
  • 브리지/콜백 경로는 locale 독립적이어야 합니다(예: `/authon/mobile-callback`, `/en/authon/mobile-callback` 아님). locale 라우팅 문제를 방지합니다.
Authon — 범용 인증 플랫폼