Capacitor アプリで Better Auth を使用するには、 better-auth クライアントを Web code にインストールし、Better Auth サーバーを指定して、Capacitor のオリジン(capacitor://localhost iOSでは適用され、 https://localhost Android の場合)を追加します trustedOrigins、そして、 bearer プラグインを使用します。メールとパスワード認証は SDK をそのまま使用できます。Google や Apple の場合、 @capgo/capacitor-social-login などのネイティブプラグインで ID トークンを取得し、 authClient.signIn.social({ idToken }).
このガイドでは、サーバーとクライアントのセットアップ、トークンの保存、ソーシャルサインイン、エンタープライズプロバイダー、および Better Auth の Web アプリを Capacitor 8 に移行する際に遭遇するエラーについて説明します。
Better Auth が Capacitor 内で実行される場合の変更点
Capacitor アプリは、ネイティブ WebView 内のローカルオリジンから配信される Web アプリです。通常の Web サイトと比較して、3 つの点が異なります。
| 項目 | Web サイトの場合 | Capacitor アプリの場合 |
|---|---|---|
| オリジン | https://app.example.com |
capacitor://localhost (iOS)、 https://localhost (Android) |
| 認証サーバー | 多くの場合、同一サイトなのでクッキーはそのまま動きます | ドメインが異なる場合、WKWebView によりサードパーティークッキーがブロックされます |
| OAuth リダイレクト | フルページのリダイレクトは問題ありません | リダイレクトでバンドル済みアプリの外に出ると、Google が WebView をブロックします |
解決策は、Capacitor のオリジンを信頼し、ベアラートークンを使い、ソーシャルログインにはネイティブプラグインを使うことです。
ステップ 1: Better Auth サーバーを設定する
この例では Hono を使用していますが、Better Auth の設定は Express、Next.js のルートハンドラー、その他のランタイムでも同じです。
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 にとって重要です。ネイティブ iOS のシートから発行されるトークンは audience としてバンドル ID を持ち、Web と Android のトークンは 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 対応するサブパスからインポートします(better-auth/react, better-auth/vue, …) を呼び出して useSession hook を取得します。 fetchOptions は変わりません。 setData と getData メソッドはネイティブ専用です。Webでは、cookie または 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 のキーと設定済みのクライアント ID と照合し、ユーザーを作成またはリンクしてセッションを返します。ベアラートークンは set-auth-token ヘッダーに到達し、あなたの onSuccess フックがそれを保存します。
ステップ 5: ネイティブ 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 も同じパターンです。iOS の Limited Login では idTokenを受け取り、それ以外の環境ではアクセストークンを渡します。 Better Auth 統合ページ には Facebook の完全なサンプルがあります。
ステップ 6: Okta、Auth0、Entra ID およびその他の OIDC プロバイダー
Better Auth の genericOAuth プラグインには、Auth0、Okta、Keycloak、Microsoft Entra ID 向けのヘルパーが含まれており、任意の OIDC サーバーには手動設定が必要です。Web ビルドでは、 authClient.signIn.oauth2({ providerId }) が通常のサイトと同様に動作します。
iOS と Android では、プロバイダーのログインページを WebView 内で読み込ませるべきではないため、システムブラウザでのフローが必要です。動作するパターンは以下の通りです:
- アプリはシステムブラウザで Better Auth のサインイン URL を開きます(例えば
SocialLogin.openSecureWindowまたはインアプリブラウザプラグインを使用)。 - Better Auth がプロバイダーとの OAuth フローを実行し、サーバー上でセッションを作成します。
- コールバックページがそのセッションを短命のワンタイムトークンに変換します(Better Auth にはこれ用の
oneTimeTokenプラグインがあります)し、ディープリンク(例:com.example.app://auth?token=.... - アプリは auth クライアントでワンタイムトークンを検証し、通常のベアラセッションを受信します。
ID トークンの引き継ぎよりも複雑な構成です。エンタープライズ SSO だけで Better Auth のユーザーデータベースが不要な場合、プロバイダーに直接サインインする方がシンプルです。 Okta, Auth0 と Entra ID ガイドを参照してください。
ステップ 7: セッションで API を保護する
独自の API は、Better Auth のサーバー API を使用して、ベアラートークンからセッションを読み取れます:
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));
});
ベアラープラグインにより getSession 受け付けます Authorization: Bearer <token> Cookie を受け入れるのと同じ方法で
トラブルシューティング
| 問題点 | 原因 | 解決策 |
|---|---|---|
Invalid origin またはサインイン時に403エラー |
Capacitor のオリジンが信頼されていません | 追加 capacitor://localhost と https://localhost まで trustedOrigins |
| WebView コンソールで CORS エラーが発生 | サーバーの CORS にアプリのオリジンがリストされていない | CORS ミドルウェアに追加し、許可してください Authorization |
サインインは成功しますが getSession を返します null |
ドメイン間でCookieに依存する | bearerプラグインを使用し、tokenを送信する |
set-auth-token 常に null |
ヘッダーが公開されていない | 追加 exposeHeaders: ['set-auth-token'] CORS へ |
Google: disallowed_useragent |
WebView に読み込まれた OAuth ページ | ネイティブサインインと idToken 引き継ぎ |
| Google ID トークンが拒否されました | Audience が設定されたクライアント ID と一致しません | トークンをデコードするには SocialLogin.decodeIdToken を比較します aud を使用して clientId |
| iOS で Apple ID トークンが拒否されました | Audience は bundle ID です | 設定 appBundleIdentifier Apple プロバイダーで |
| Apple nonce エラー | Apple と Better Auth に異なる nonce が送信される | 一度生成し、同じ値を再利用する |
| アプリ再起動後にセッションが失われる | メモリ内にのみ保持されるトークン | 永続化するには setData を使用し、最初のリクエスト前に読み込みます |
モバイル側のデプロイ
認証の変更はネイティブ側であることは稀です。信頼できるオリジンやプロバイダーはサーバー設定であり、クライアントの code は TypeScript で書かれているため、ほとんどの修正は Capgo Live Updates で配信でき、ストアへのリリースは不要です。ネイティブプラグインの追加や URL スキームの変更を行った場合のみ、新しいバイナリが必要です。
関連ガイド
- Capacitorアプリに認証を追加する方法 Better Auth を Firebase、Supabase、Neon Auth(マネージド Better Auth)、ホスト型 ID プロバイダーと比較しています。
- Capacitor アプリでの生体認証 Face IDや指紋認証で、保存されたセッションのロックを解除するために使用します。
- Social Login プラグイン Google、Apple、Facebookのネイティブフロー用です。