📱

"react-native-google-signin" を使わないGoogle認証 [React Native/Expo]

に公開

先日自作アプリにLINEログインを追加したところ、Apple Sign-inがないことを理由にAppStoreでリジェクトされてしまいました。
本記事は、ExpoでApple Sign-inをはじめとするネイティブOSのサインイン機能を実装するため調べた知見をまとめたものです。


Expoを利用したReact Nativeアプリでネイティブ認証を実装する際、expo-apple-authentication@react-native-google-signin/google-signinといったライブラリが広く使われています。しかし、これらのライブラリには

  • 複数のライブラリ管理: AppleとGoogleで異なるライブラリを導入する必要があり、メンテナンス性が低下します。
  • 古いAPIへの依存: @react-native-google-signin/google-signinの無償版は、Androidで非推奨(deprecated)となったcom.google.android.gms:play-services-authに依存しています。これにより、Googleが推奨するモダンな認証UI(One Tapなど)が利用できず、ネイティブ実装に比べユーザーエクスペリエンスが低下します。

のような課題があります。
本記事で紹介するreact-native-credentials-managerは、単一のパッケージで両プラットフォームの認証APIを呼び出し、この課題を解決します。
(GitHubリポジトリ)

  • 統一のAPI: プラットフォームごとの違いを吸収し、一貫したAPIでAppleとGoogleのサインインを実装できます。
  • 優れたUX: Androidでは新しいCredential Manager APIを利用するため、非推奨ライブラリへの依存なく、優れたUI/UXを提供します。iOSでも同様のOS標準ライブラリをラップしています。
  • 高い拡張性: Google/Appleサインインに加え、パスキーを利用したサインインにも対応しており、モダンなサインインオプションを提供できます。

この記事では、react-native-credentials-managerとSupabaseを連携させ、シームレスでモダンなログインを実装する方法を解説します。

必要なパッケージのインストール

まず、プロジェクトに必要な依存関係をインストールします。

npm install react-native-credentials-manager uuid @supabase/supabase-js
npm install --save-dev @types/uuid

(Yarnの場合)

yarn add react-native-credentials-manager uuid @supabase/supabase-js
yarn add --dev @types/uuid

バックエンド(Supabase)の設定

今回は、認証処理のバックエンドとしてSupabaseを利用する場合について解説します。

1. Supabaseプロジェクトの準備(省略)

Supabaseプロジェクトを作成し、プロジェクトに組み込みます。

2. 認証プロバイダの有効化

Supabaseのダッシュボードで、[Authentication] > [Providers] に移動し、AppleGoogleを有効にします。各プロバイダに必要なOAuthクライアントIDやシークレット情報を設定してください。
Googleプロバイダには、後述する「ウェブアプリケーション」タイプのクライアントIDとクライアントシークレットを設定します。

プラットフォーム固有の設定

次に、iOSとAndroidそれぞれのプラットフォームでネイティブ認証を有効にするための設定を行います。

ドメイン認証

ライブラリのドキュメント にある通り、AppleとGoogleに対しドメイン認証を行います。

iOS (Appleサインイン)

  1. Apple DeveloperのIdentifiersページから自分のアプリのidを選択し、"Sign In with Apple" を有効化します。
  2. app.config.ts (または expo.json) に、Appleサインインを有効化する設定を追加します。バンドルIDや関連ドメインはご自身の環境に合わせてください。
    app.config.ts
    // app.config.ts または expo.json
    export default {
      ios: {
        usesAppleSignIn: true,
        bundleIdentifier: "your.app.bundle.id",
        infoPlist: {
          // 必要に応じて追加のplist設定を追加
        },
        associatedDomains: [
          "applinks:yourdomain.com",
          "webcredentials:yourdomain.com",
        ]
      }
    };
    

Android (Googleサインイン)

Google Cloud Consoleでの認証情報設定は、Supabaseとネイティブ認証を連携させる上で間違いやすいポイントです。(私もここで詰まりました)

まず、「Androidアプリ」と「ウェブアプリケーション」の2種類のOAuthクライアントIDを作成する必要があります。

  1. Androidアプリ:

    • Google Cloud Consoleの「APIとサービス」 > 「認証情報」で「認証情報を作成」 > 「OAuthクライアントID」を選択します。
    • アプリケーションの種類で「Android」を選択します。
    • アプリのパッケージ名SHA-1証明書フィンガープリントを必ず登録してください。これにより、GoogleはあなたのAndroidアプリからのリクエストを正しく識別できます。
  2. ウェブアプリケーション:

    • 同様に「認証情報を作成」 > 「OAuthクライアントID」を選択します。
    • アプリケーションの種類で「ウェブアプリケーション」を選択します。
    • このクライアントIDとクライアントシークレットを、SupabaseのGoogleプロバイダ設定と、後述するアプリの実装コードで使用します。

※注意: Supabaseとアプリコード (serverClientId) の両方で使用するのは、ウェブアプリケーションのクライアントIDです。AndroidアプリのクライアントIDは、コードに直接記述することはありません。

app.config.ts
export default {
  android: {
    package: "your.app.package.name",
    permissions: [
      "android.permission.ACCESS_NETWORK_STATE",
      // その他の権限...
    ]
  }
};

実装

ネイティブ認証ロジックの作成

プラットフォームを判別し、それぞれに対応するサインアップ処理を呼び出す関数をcredentialManager.tsとして実装します。YOUR_GOOGLE_CLIENT_IDには、前述の通りGoogle Cloud Consoleで発行した「ウェブアプリケーション」タイプのクライアントIDを設定してください。

credentialManager.ts
import { Platform } from "react-native";
import { signUpWithApple, signUpWithGoogle } from "react-native-credentials-manager";
import { v4 as uuidv4 } from 'uuid';

// 一貫したデータ構造のためのユーザーインターフェースを定義
interface PlatformCredentialUser {
  name?: string;
  givenName?: string;
  familyName?: string;
  photo?: string;
  email?: string;
}

// レスポンスインターフェースを定義
interface PlatformCredentialResponse {
  type: "google" | "apple";
  token: string;
  id: string;
  user: PlatformCredentialUser;
}

async function platformSpecificSignUp(): Promise<PlatformCredentialResponse> {
  try {
    if (Platform.OS === "android") {
      // Android: Googleサインイン
      const googleCredential = await signUpWithGoogle({
        serverClientId: "YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com", // ⚠️ ウェブアプリケーションのクライアントID
        autoSelectEnabled: true,
      });

      return {
        type: "google",
        token: googleCredential.idToken,
        id: googleCredential.id,
        user: {
          name: googleCredential.displayName,
          givenName: googleCredential.givenName,
          familyName: googleCredential.familyName,
          photo: googleCredential.profilePicture,
        },
      };
    } else {
      // iOS: Appleサインイン
      const appleCredential = await signUpWithApple({
        nonce: uuidv4(),
        requestedScopes: ["fullName", "email"],
      });

      return {
        type: "apple",
        token: appleCredential.idToken,
        id: appleCredential.id,
        user: {
          name: appleCredential.displayName,
          givenName: appleCredential.givenName,
          familyName: appleCredential.familyName,
          email: appleCredential.email,
        },
      };
    }
  } catch (error) {
    console.error("サインイン処理に失敗: ", error);
    throw error;
  }
}

export { platformSpecificSignUp, type PlatformCredentialResponse };

Reactコンポーネントへの組み込み

作成した認証ロジックをReactコンポーネントから呼び出し、SupabaseのsignInWithIdTokenメソッドに連携させます。

App.tsx
import React, { useState } from 'react';
import { View, Button, Alert, Text, Platform } from 'react-native';
import { platformSpecificSignUp } from './credentialManager';
import { supabase } from './supabase'; // Supabaseクライアント

const LoginScreen: React.FC = () => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);

  const handleNativeLogin = async () => {
    try {
      setLoading(true);
      
      // クレデンシャルを取得
      const result = await platformSpecificSignUp();

      // ネイティブトークンを使用してSupabaseでサインイン
      const {error, data: {user}} = await supabase.auth.signInWithIdToken({
        provider: result.type, // "apple" か "google"
        token: result.token
      });
      
      if (error) {
        throw error;
      }
      
      setUser(user);
      console.log('ログイン成功:', user);
      
    } catch (error) {
      console.error('ログインに失敗しました:', error);
      Alert.alert('ログイン失敗', error.message || 'しばらくしてから再試行してください。');
    } finally {
      setLoading(false);
    }
  };

  const handleSignOut = async () => {
    await supabase.auth.signOut();
    setUser(null);
  };

  if (user) {
    return (
      <View style={{ padding: 20 }}>
        <Text>ようこそ、{user.user_metadata?.name || user.email || 'ユーザー'}さん!</Text>
        <Text>ユーザーID: {user.id}</Text>
        <Button 
          title="サインアウト" 
          onPress={handleSignOut} 
        />
      </View>
    );
  }

  return (
    <View style={{ padding: 20 }}>
      {Platform.select({
        ios: (
          <Button
            title={loading ? "サインイン中..." : "Appleで続ける"}
            onPress={handleNativeLogin}
            disabled={loading}
          />
        ),
        android: (
          <Button
            title={loading ? "サインイン中..." : "Googleで続ける"}
            onPress={handleNativeLogin}
            disabled={loading}
          />
        )
      })}
    </View>
  );
};

export default LoginScreen;

結論

react-native-credentials-managerを利用することで、手軽にネイティブアプリ同様のログインUXを実現できます。
本記事で紹介したライブラリ react-native-credentials-manager を開発・公開してくださったokwasniewski氏に感謝します。
本記事が、あなたのアプリ開発の一助となれば幸いです。

Discussion