プログラム関係の備忘録。技術系記事1000記事以上を目指すブログ

react-nativeでFirebase Authenticationを使った認証を実装

まず、react-nativeにはreact-native-cliとexpo-cliの2通りがありますが、今回は前者のreact-native-cliでのアプリです。まずはAndroid用です。

着地点

自身のアプリケーションにFirebase Authenticationを組み込み、ユーザー情報の認証機能を作るまで。

Firebaseで認証機能の考え方とそのメリット

通常、アプリケーションでAPIを用いたユーザー認証を使う場合は次のような手順になるかと思います。

①クライアントサイドからログイン情報をPOST(ユーザー名・パスワード)
②自前のサーバーがデータを受け取り、ユーザー情報を格納しているDBに問い合わせ
③ユーザー情報があれば、一意のID等をjwtなどでトークン可してクライアントサイドに返却
④クライアントサイドはトークンを保持し、以降のAPIアクセスはheaderにトークンを付与してリクエスト

Firebaseの認証機能を使えば、2~3をサーバー側で実装するのを省略できるといった感じでしょうか。

ログイン、ログアウト、ログイン状態の取得をFirebaseのAPIに投げるだけで済み、パスワード再設定や、アカウントの停止、削除もFirebaseの管理画面から行うことができます。

ユーザーに関するその他の情報は、Firebaseが発行する一意のID(ユーザー UID)と紐づけて管理するようにすれば、DBではユーザー情報を管理する必要ががないためセキュリティ面でも強化できます。

ユーザー情報をアプリでも保持して、認証機能のみFirebaseを使いたいという場合は、サーバーサイドにFirebase Adminという機能を入れることで、受け取ったUIDからメールアドレス等を取得することができるので、それを元にアプリ側のDBとデータをやりとりすることができるようです。

設定

Firebase自体を利用できるようにアプリとFirebaseの接続は済んでいる体で(google-services.jsonの設置など)割愛します。
https://firebase.google.com/docs/android/setup?hl=ja

まずはFirebaseの管理画面から、メール/パスワードによる認証を有効にします

その後、パッケージをアプリ側に入れます。
すでにreact-native-firebaseを入れている場合は必要ありません。

@react-native-firebase/auth

AndroidディレクトリのApp配下のbuild.gradleにfirebase-authの依存関係を追加します。

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    //noinspection GradleDynamicVersion
    implementation "com.facebook.react:react-native:+"  // From node_modules
 ・
 ・
 ・
    implementation 'com.google.android.gms:play-services-base:17.2.1'
    implementation 'com.google.firebase:firebase-core:17.0.0'
    implementation "com.google.firebase:firebase-messaging:20.0.0"
+  implementation "com.google.firebase:firebase-auth:18.0.0"
    implementation 'me.leolin:ShortcutBadger:1.1.21@aar'

Androidのbuildディレクトリをクリーンして、buildが通ることを確認

cd andoroid && ./gradlew clean

アプリ側の実装

認証用の関数を格納するコンポーネントを作ります。
ここではAuthentication.jsとします。

各メソッド用のコンポーネント

import firebase from 'react-native-firebase';

// サインアップ
export const signup = (email, password) => {
    return firebase.auth().createUserWithEmailAndPassword(email, password)
    .then(user => {
        if (user) {
            console.log("Success to Signup",user)
            return user
        }
    })
    .catch(error => {
        console.log("error",error.message);
        return error.message
    })
}

// サインイン
export const signIn = (email, password) => {
    return firebase.auth().signInWithEmailAndPassword(email, password)
    .then(response => {
        console.log("signIn Success!",response);
        return response
    })
    .catch(error => {
        console.log("error",error.message);
        return error.message
    });
}

// サインアウト
export const signOut = () => {
    return firebase.auth().signOut()
    .then(response => {
        console.log("SignOut");
        return response
    })
    .catch(error => {
        console.log("error",error.message);
        return error.message
    });
}

サインアップページ

サインアップのページで先ほどのメソッドを呼び出すようにします。

import React, {Component} from 'react';
import { Content,Text,Button} from 'native-base';

import { signup } from '../lib/Authentication';

class SignUpPage extends Component {
  constructor(props) {
    super(props)
    this.state = {
      email: 'test@gmail.com', // 暫定で決め打ちの値
      password: 'test1234',  // 暫定で決め打ちの値 
   };
  }

  // ユーザー登録のメソッド
  signUp = () => {
    const { email, password } = this.state;
    signup(email, password)
    .then(user => {
      console.log("result: ", user )
    })
  }

  render() {
    return (
  ・
  ・
  ・
        <Content>
          <Text>ログイン</Text>
          <Button
            title="送信"
            onPress={() => this.signUp()}
          />
        </Content>

これでサインアップの処理は完了

実際に試してみると、Firebase側はこのように表示されました。

アプリ側のコンソールでも結果が取得できているのが確認できます。

サインアウトページ

続いてサインアウトのページを作ります

import { signOut } from '../lib/Authentication';

class SignOutPage extends Component {

  signOut = () => {
    signOut()
    .then(() => {
      console.log("signOut")
    })
  }

  render() {
    return (
     ・
     ・
     ・
          <Button
            title="送信"
            onPress={() => this.signOut()} // ユーザー登録メソッドを実行
          />
     ・
     ・
     ・

先ほどサインアップした状態でしたので、サインアウトもきちんと行われることが確認できます。

サインインページ

次にサインインのページを作ります

import React, {Component} from 'react';
import { signIn } from '../lib/Authentication';

class SignInPage extends Component {
  constructor(props) {
    super(props)
    this.state = {
      email: 'test@gmail.com',
      password: 'test1234',
   };
  }

  signIn = () => {
    const { email, password } = this.state;
    signIn(email, password)
    .then(user => {
      console.log("result: ", user )
    })
  }

  render() {
    return (
    ・
    ・
    ・
          <Button
            title="送信"
            onPress={() => this.signIn()} // ユーザー登録メソッドを実行
          />
    ・
    ・
    ・
    );
  }

ここまでで、サインアップ、サインアウト、サインインの処理が作ることができました。

あとは、いまの認証状態を取得する処理を各コンポーネントのwrapperに入れることで、任意のページに割り当てるようにしました。

この記事ではreact-native-router-fluxで画面遷移をしています。

App.js

  componentDidMount(){
    firebase.auth().onAuthStateChanged( user => {
      if (user) {
        return this.setState({authState: user.uid})
      } else {
        return this.setState({authState: false})
      }
    });
  }

  render() {
    return (
      <Wrapper
        authState={this.state.authState}
      />
    );
  }

こんな感じでwrapper(react-native-router-fluxでルーティングしている画面)にstateを渡すようにして、wrapperではその有無によって初期画面をinitialで分けるようにしました。

正直どこでやるのが正解かというと理解が乏しいのですが、アプリ全体に認証が必要であればこういうやり方でもいいのかなと思った次第です。

ページによって認証の有無が必要なアプリの場合はまた別の方法で書くことになりそうです。