본문 바로가기

Capacitor 앱에서 Better Auth 사용법 (2026 가이드)

Capacitor 8 앱에서 Better Auth 사용하기: 신뢰할 수 있는 origin, 쿠키 대신 bearer token 사용, 이메일 및 비밀번호 로그인, 네이티브 Google 및 Apple 로그인, 그리고 각종 버그 수정.

Better Auth 기반 Capacitor 앱의 회원가입 화면

Capacitor 앱에서 Better Auth를 사용하려면, better-auth 웹 code에 client를 설치하고, Better Auth 서버를 가리키도록 설정한 뒤, Capacitor origin을 추가하세요.capacitor://localhost iOS에서는, https://localhost Android에서는) 하여 trustedOrigins, and switch from cookies to bearer tokens with the bearer plugin을 통해 이메일과 비밀번호 방식은 SDK에서 그대로 작동합니다. Google 및 Apple의 경우, 다음 native plugin을 사용하여 ID token을 가져온 뒤 @capgo/capacitor-social-login 에 전달합니다. authClient.signIn.social({ idToken }).

이 가이드에서는 서버 및 클라이언트 설정, token 저장, 소셜 로그인, enterprise provider, 그리고 Better Auth 웹 앱을 Capacitor로 전환할 때 발생하는 오류들을 다룹니다.

Capacitor 내에서 Better Auth를 실행할 때 달라지는 점

Capacitor 앱은 native WebView 내부의 local origin에서 서빙되는 web app입니다. 일반적인 웹사이트와 비교했을 때 세 가지 차이점이 있습니다:

주제 웹사이트의 경우 Capacitor 앱의 경우
Origin https://app.example.com capacitor://localhost (iOS), https://localhost (Android)
인증 서버 동일 사이트인 경우 쿠키가 정상 작동함 도메인이 다르면 WKWebView에서 서드파티 쿠키가 차단됨
OAuth 리다이렉트로 거부합니다 전체 페이지 리다이렉트는 문제없음 리다이렉트 시 번들 앱을 벗어나게 되며, Google은 WebView를 차단함

해결 방법은 다음과 같습니다: Capacitor origin을 신뢰하고, bearer token을 사용하며, 소셜 로그인을 위해 native plugin을 사용하는 것입니다.

1단계: Better Auth 서버 설정

이 예제는 Hono를 사용하지만, Better Auth 설정은 Express, Next.js route handler 또는 다른 런타임에서도 동일합니다.

bun add better-auth hono
// auth.ts
import { betterAuth } from 'better-auth';
import { bearer } from 'better-auth/plugins';
import { db } from './db'; // your Drizzle, Prisma, Kysely or pg instance

export const auth = betterAuth({
  baseURL: process.env.BETTER_AUTH_URL, // https://auth.example.com
  database: db,
  emailAndPassword: {
    enabled: true,
  },
  socialProviders: {
    google: {
      clientId: process.env.GOOGLE_WEB_CLIENT_ID as string,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
    },
    apple: {
      clientId: process.env.APPLE_SERVICE_ID as string,
      clientSecret: process.env.APPLE_CLIENT_SECRET as string,
      appBundleIdentifier: process.env.APPLE_BUNDLE_ID as string, // com.example.app
    },
  },
  trustedOrigins: [
    'capacitor://localhost', // iOS WebView
    'https://localhost', // Android WebView (androidScheme https, the default)
    'https://app.example.com', // your web build
    'https://appleid.apple.com',
  ],
  plugins: [bearer()],
});

appBundleIdentifier Apple에서는 이 부분이 중요합니다. native iOS 시트의 token은 bundle ID를 audience로 포함하는 반면, web 및 Android token은 Services ID를 포함합니다. Better Auth는 이 필드를 사용하여 두 방식 모두 수용합니다.

핸들러를 마운트하고 앱 오리진에 대해 CORS를 허용하세요. 다음을 set-auth-token 헤더에 노출해야 클라이언트가 bearer token을 확인할 수 있습니다:

// server.ts
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { auth } from './auth';

const app = new Hono();

app.use(
  '/api/auth/*',
  cors({
    origin: ['capacitor://localhost', 'https://localhost', 'https://app.example.com'],
    allowHeaders: ['Content-Type', 'Authorization'],
    allowMethods: ['GET', 'POST', 'OPTIONS'],
    exposeHeaders: ['set-auth-token'],
    credentials: true,
  }),
);

app.on(['GET', 'POST'], '/api/auth/*', (c) => auth.handler(c.req.raw));

export default app;

실행 bunx @better-auth/cli migrate (또는 generate ORM용)을 사용하여 테이블을 생성합니다.

2단계: 앱에서 인증 클라이언트 생성

bun add better-auth @capgo/capacitor-native-biometric
bunx cap sync

세션 token을 Keychain 또는 Keystore에 저장하고, 클라이언트가 동기적으로 읽을 수 있도록 메모리에 복사본을 유지하세요:

// auth-client.ts
import { createAuthClient } from 'better-auth/client';
import { NativeBiometric } from '@capgo/capacitor-native-biometric';

const TOKEN_KEY = 'session.token';
let sessionToken = '';

export async function loadSessionToken() {
  try {
    const { value } = await NativeBiometric.getData({ key: TOKEN_KEY });
    sessionToken = value;
  } catch {
    sessionToken = '';
  }
}

async function saveSessionToken(token: string) {
  sessionToken = token;
  if (token) {
    await NativeBiometric.setData({ key: TOKEN_KEY, value: token });
  } else {
    await NativeBiometric.deleteData({ key: TOKEN_KEY }).catch(() => {});
  }
}

export const authClient = createAuthClient({
  baseURL: 'https://auth.example.com',
  fetchOptions: {
    auth: {
      type: 'Bearer',
      token: () => sessionToken,
    },
    onSuccess: async (ctx) => {
      const token = ctx.response.headers.get('set-auth-token');
      if (token) await saveSessionToken(token);
    },
  },
});

export async function clearSession() {
  await saveSessionToken('');
}

호출하세요 await loadSessionToken() 앱이 렌더링되기 전에, authClient.getSession() 사용자 정보를 복구하려면:

import { authClient, loadSessionToken } from './auth-client';

await loadSessionToken();
const { data: session } = await authClient.getSession();
if (session) {
  console.log('Signed in as', session.user.email);
}

React, Vue, Svelte 또는 Solid를 사용하는 경우, createAuthClient 해당 서브패스에서 import 하세요 (better-auth/react, better-auth/vue, …)를 통해 useSession hook을 가져오세요. fetchOptions 동일하게 유지됩니다. setData 및 getData 메서드는 네이티브 전용입니다. 웹에서는 쿠키나 sessionStorage 사용 Capacitor.isNativePlatform().

3단계: 이메일과 비밀번호

플러그인이 필요 없습니다. 일반 HTTP 호출입니다.

import { authClient, clearSession } from './auth-client';

export async function signUp(email: string, password: string, name: string) {
  const { data, error } = await authClient.signUp.email({ email, password, name });
  if (error) throw new Error(error.message);
  return data;
}

export async function signIn(email: string, password: string) {
  const { data, error } = await authClient.signIn.email({ email, password });
  if (error) throw new Error(error.message);
  return data;
}

export async function signOut() {
  await authClient.signOut();
  await clearSession();
}

이메일 인증 및 비밀번호 재설정의 경우, Better Auth는 URL을 여는 링크를 보냅니다. 링크를 callbackURL iOS Universal Link 및 Android App Link로도 설정된 도메인 페이지로 지정하세요. 그러면 링크가 휴대폰에서는 앱을, 그 외에서는 웹사이트를 엽니다.

4단계: 네이티브 Google 로그인

다음에 설명된 대로 Google을 구성하세요 Capacitor를 사용하여 Google로 로그인하는 방법, 그런 다음:

import { SocialLogin } from '@capgo/capacitor-social-login';
import { authClient } from './auth-client';

await SocialLogin.initialize({
  google: {
    webClientId: 'WEB_CLIENT_ID.apps.googleusercontent.com',
    iOSClientId: 'IOS_CLIENT_ID.apps.googleusercontent.com',
    mode: 'online', // Better Auth needs the idToken, offline mode only returns a code
  },
});

export async function signInWithGoogle() {
  const { result } = await SocialLogin.login({ provider: 'google', options: {} });

  if (result.responseType !== 'online' || !result.idToken) {
    throw new Error('Google did not return an ID token');
  }

  const { data, error } = await authClient.signIn.social({
    provider: 'google',
    idToken: {
      token: result.idToken,
      accessToken: result.accessToken?.token,
    },
  });
  if (error) throw new Error(error.message);
  return data;
}

Better Auth가 Google 키와 설정된 client ID를 통해 토큰을 검증하고, 사용자를 생성 또는 연결한 뒤 세션을 반환합니다. Bearer 토큰은 set-auth-token header에 포함되어 전달되며, onSuccess hook에 저장됩니다.

5단계: Native Apple 로그인

Apple은 다음 문서를 참고해 설정하세요: Capacitor를 사용해 Apple로 로그인하는 방법. nonce를 생성해 Apple에 전달하고, 같은 값을 Better Auth로 보내세요:

import { SocialLogin } from '@capgo/capacitor-social-login';
import { authClient } from './auth-client';

export async function signInWithApple() {
  const nonce = crypto.randomUUID();

  const { result } = await SocialLogin.login({
    provider: 'apple',
    options: { scopes: ['email', 'name'], nonce },
  });

  if (!result.idToken) throw new Error('Apple did not return an ID token');

  const { data, error } = await authClient.signIn.social({
    provider: 'apple',
    idToken: {
      token: result.idToken,
      nonce,
      accessToken: result.accessToken?.token,
    },
  });
  if (error) throw new Error(error.message);
  return data;
}

Apple은 첫 인증 시에만 사용자 이름을 공유합니다. 사용자 레코드에 이름을 저장하려면 첫 로그인 직후에 다음을 사용해 프로필을 업데이트하세요. result.profile.givenName 및 familyName.

Facebook도 같은 방식을 따릅니다. Limited Login을 사용하는 iOS에서는 idToken, 다른 곳에서는 액세스 토큰을 전달합니다. Better Auth 연동 페이지 에서 Facebook 전체 예제를 확인할 수 있습니다.

6단계: Okta, Auth0, Entra ID 및 기타 OIDC 공급자

Better Auth의 genericOAuth 플러그인은 Auth0, Okta, Keycloak, Microsoft Entra ID용 헬퍼를 제공하며, 모든 OIDC 서버에 대한 수동 설정도 지원합니다. 웹 빌드에서는, authClient.signIn.oauth2({ providerId }) 일반 사이트와 동일하게 작동합니다.

iOS 및 Android에서는 공급자 로그인 페이지가 WebView 안에서 로드되면 안 되므로 시스템 브라우저가 필요합니다. 작동하는 패턴은 다음과 같습니다:

  1. 앱은 시스템 브라우저에서 Better Auth 로그인 URL을 엽니다 (예: SocialLogin.openSecureWindow 또는 인앱 브라우저 플러그인 사용).
  2. Better Auth가 공급자와 OAuth 흐름을 진행하고 서버에 세션을 생성합니다.
  3. 콜백 페이지에서 해당 세션을 수명이 짧은 일회용 토큰으로 변환합니다(Better Auth에는 oneTimeToken ) 플러그인을 사용하고 다음과 같은 딥 링크로 리다이렉트합니다. com.example.app://auth?token=....
  4. 앱이 auth 클라이언트로 일회용 토큰을 검증하고 일반 Bearer 세션을 받습니다.

ID 토큰 핸드오프보다 신경 쓸 부분이 더 많습니다. Better Auth의 사용자 데이터베이스는 필요 없고 엔터프라이즈 SSO만 필요하다면 제공자에 직접 로그인하는 편이 더 간단합니다. 다음을 참고하세요: Okta, Auth0) 및 Entra ID 가이드.

7단계: 세션으로 API 보호

Better Auth 서버 API를 사용하면 자체 API에서 Bearer 토큰으로 세션을 읽을 수 있습니다:

import { Hono } from 'hono';
import { auth } from './auth';

const api = new Hono();

api.get('/me/orders', async (c) => {
  const session = await auth.api.getSession({ headers: c.req.raw.headers });
  if (!session) return c.json({ error: 'unauthorized' }, 401);
  return c.json(await listOrders(session.user.id));
});

Bearer 플러그인을 사용하면 getSession 허용 Authorization: Bearer <token> 쿠키를 허용하는 것과 동일한 방식으로.

문제 해결

문제 원인 해결 방법
Invalid origin 또는 로그인 시 403 오류 Capacitor origin을 신뢰할 수 없음 추가 capacitor://localhost 및 https://localhost 대상 trustedOrigins
WebView 콘솔의 CORS 에러 서버 CORS에 앱 origin이 나열되지 않음 이들을 CORS 미들웨어에 추가하고 허용하세요 Authorization
로그인은 성공하지만 getSession 반환합니다 null 도메인 간 쿠키에 의존 bearer 플러그인을 사용하여 토큰을 전송하세요
set-auth-token 항상 null 헤더가 노출되지 않음 추가 exposeHeaders: ['set-auth-token'] CORS에
Google: disallowed_useragent WebView에서 OAuth 페이지 로드됨 네이티브 sign-in을 사용하고 idToken 핸드오프
Google ID 토큰이 거부됨 Audience가 설정된 client ID와 일치하지 않음 토큰 디코딩: SocialLogin.decodeIdToken 그리고 비교 aud 포함 clientId
iOS에서 Apple ID 토큰이 거부됨 Audience는 bundle ID입니다 설정 appBundleIdentifier Apple provider에서
Apple nonce 오류 Apple과 Better Auth에 서로 다른 nonce가 전송됨 한 번 생성하여 동일한 값 재사용
앱 재시작 시 세션 유실 토큰이 메모리에만 유지됨 다음 방식으로 영구 저장: setData 저장 후 첫 요청 전에 로드

모바일 쪽 배포하기

인증 변경은 네이티브 영역이 아닌 경우가 많습니다. 신뢰할 수 있는 Origin과 Provider는 서버 설정이며, 클라이언트 code는 TypeScript이므로 대부분의 수정 사항은 스토어 출시 대신 Capgo Live Update 사용하기 배포할 수 있습니다. 네이티브 플러그인을 추가하거나 URL scheme을 변경할 때만 새로운 바이너리가 필요합니다.

Capacitor 앱용 Live updates

웹 레이어 버그가 라이브 상태라면, 앱 스토어 승인을 며칠씩 기다리지 말고 Capgo를 통해 수정 사항을 배포하세요. 네이티브 변경 사항은 정상 리뷰 절차를 그대로 따르는 반면, 사용자는 백그라운드에서 업데이트를 받습니다.

블로그 최신 글

Capgo는 정말 프로페셔널한 모바일 앱을 만드는 데 필요한 최고의 인사이트를 제공합니다.