Firebaseでメールリンク認証を実装してみる

f:id:daichi510:20210218201639p:plainコンテンツ開発事業部の馬場です。

自分の携わっているプロジェクトでFirebase Authenticationを触る機会がありました。
Firebase Authenticationは簡単にログイン機能を実装することができるライブラリです。

今回の記事ではFirebase Authenticationを使用したメールリンク認証の実装と自分がハマったポイントをご紹介しようと思います。

実装環境

実装言語:TypeScript

準備

Firebaseプロジェクトを作成し、アプリを登録する

まず、Firebaseコンソールでプロジェクトを作成します。
次にアプリの登録を行います。今回作成するTestアプリはウェブ上で動作するものを実装するので『ウェブ』を選択して登録していきます。

f:id:daichi510:20210218201639p:plainf:id:daichi510:20210218201817p:plain
Firebaseコンソール

機能を有効化する

Firebaseコンソールで『Authenticaiton』を有効にします。
今回はメールリンク認証を使用したいので、『メール/パスワード』を有効にし、
『メールリンク』も有効にします。

f:id:daichi510:20210218201642p:plain
Authentication有効化

本来は『承認済みドメイン』も追加する必要があると思いますが、
今回はlocalhostで実行確認したので追加しませんでした。

ハマったポイント

『Realtime Database』も有効にしておきます。
これを有効にしないとあとで出てくるconfigで「databaseURL」が出てきませんでした。

Firebase SDKを追加する

今回はnpmを使用して追加します。次のコマンドを実行して追加します。

npm install firebase@7.5.2

ハマったポイント

2021/2/17現在、npmで最新バージョンをインストールするとimportができず実装時にエラーが発生しました。
『7.5.2』のバージョンを指定してインストールすることでエラーが起きないことを確認しています。

実装

Firebaseの初期化処理

初期化はfirebase.initializeAppを呼ぶだけです。
configはFirebaseコンソールで『プロジェクトの設定』に表示されているのでそのままコピペします。

f:id:daichi510:20210309110047p:plain
Configの表示

    // 初期化処理
    init_Firebase() {
        cc.log("INIT");
           
        // firebaseコンソールに表示されたものまんま
        const config = {
            apiKey: "XXXXXXXXXX",
            authDomain: "XXXXXXXXXX",
            databaseURL: "XXXXXXXXXX",
            projectId: "XXXXXXXXXX",
            storageBucket: "XXXXXXXXXX",
            messagingSenderId: "XXXXXXXXXX",
            appId: "XXXXXXXXXX",
        }

        if (!this.initFlag) {
            firebase.initializeApp(config);
            this.initFlag = true;
        }
    }
登録状況確認処理

firebase.auth().fetchSignInMethodsForEmailでメールアドレスが登録済みかどうかを確認できます。
ただ、新規登録の場合と登録済みの場合、どちらも認証とログイン処理は後述の同じ処理でできます。

    // 登録状況確認処理
    async fetchAsync_Firebase(email:string):Promise<any> {
        cc.log("FETCH");
        
        return new Promise((resolve) => {
            firebase.auth().fetchSignInMethodsForEmail(email).then((result) => {
                if (0 < result.length) {
                    // 登録済み
                    resolve(result);
                } else {
                    // 未登録
                    resolve(null);
                }
            }).catch((error) => {
                cc.log("FirebasePlugin::fetch() errorCode: " + error.code);
                cc.log("FirebasePlugin::fetch() errorMessage: " + error.message);
                resolve(null);
            });
        });     
    }
認証メールの送信

firebase.auth().sendSignInLinkToEmailでメールアドレスに認証メールを送ります。
actionCodeSettingsのurlには認証メールからの戻り先のURLを設定します。

ログイン完了時に使用するため、メールアドレスはローカルストレージに保存します。

    // 認証メールの送信処理
    async sendAsyc_Firebase(email:string):Promise<boolean> {
        cc.log("SEND MAIL");
        
        // 認証メール送信後かわかるようにパラメータ追加しておく
        const callbackUrl = "http://localhost:7456/?login=1";
        const actionCodeSettings = {
            url: callbackUrl,
            handleCodeInApp: true
        };
        // ローカルストレージにメールアドレスを保存
        window.localStorage.setItem("EmailForLogin", email);
        return new Promise((resolve) => {
            firebase.auth().sendSignInLinkToEmail(email, actionCodeSettings).then(() => {
                // 認証メール送信完了
                resolve(true);
            }).catch((error) => {
                cc.log("FirebasePlugin::send() errorCode: " + error.code);
                cc.log("FirebasePlugin::send() errorMessage: " + error.message);
                resolve(false);
            });
        }); 
    }
ログイン完了処理

認証メールのリンクから戻ってきたらfirebase.auth().signInWithEmailLinkでログインを完了させます。
このとき、メールアドレスはローカルストレージに保存したメールアドレスを使用します。

    // ログイン完了処理
    async loginAsync_Firebase():Promise<any> {
        cc.log("LOGIN");
        
        // ローカルストレージに保存してあるメールアドレスを取得
        const email = window.localStorage.getItem("EmailForLogin");
        return new Promise((resolve) => {
            if(firebase.auth().isSignInWithEmailLink(location.href)) {
                firebase.auth().signInWithEmailLink(email, location.href).then((result) => {
                    // ログイン完了
                    // ローカルに保存したメールアドレスを削除
                    window.localStorage.removeItem("EmailForLogin");
                    resolve(result);
                }).catch((error) => {
                    cc.log("FirebasePlugin::login() errorCode: " + error.code);
                    cc.log("FirebasePlugin::login() errorMessage: " + error.message);
                    // ローカルに保存したメールアドレスを削除
                    window.localStorage.removeItem("EmailForLogin");
                    resolve(null);
                });
            } else {
                // ローカルに保存したメールアドレスを削除
                window.localStorage.removeItem("EmailForLogin");
                resolve(null);
            }
        });    
    }

ハマったポイント

ローカルストレージに保存してあるメールアドレスを使用するため、
認証メールを送るときのブラウザと認証メールのリンクから開くブラウザは同一ブラウザである必要があります。

iPhoneなど携帯端末でログインを行う場合にGmailなどのメールアプリからリンクを開くとアプリ内ブラウザで開いてしまいログインできないといったことが起きました。 アプリ内ブラウザは例えばSafariを指定して開いたとしても厳密にはSafariではないためログインが完了できないで、 「本当の」ブラウザでリンクを開くことが必要となります。

ちなみに、別ブラウザで開いた場合でも、正しいメールアドレスを再入力させることでログインを完了させることもできます。

ユーザー情報の取得処理

ログイン済みのユーザー情報の取得方法を2つご紹介します。

1つ目はfirebase.auth().currentUserで取得する方法

    // ログイン済みユーザー情報の取得1
    getUser1_Firebase():any {
        cc.log("GET USER1");

        return firebase.auth().currentUser;
    }

2つ目はfirebase.auth().onAuthStateChangedで取得する方法です。

    // ログイン済みユーザー情報の取得2
    async getUser2Async_Firebase():Promise<any> {
        cc.log("GET USER2");
        

        return new Promise((resolve) => {
            firebase.auth().onAuthStateChanged((_user:any) => {
                if(_user != null) {
                    // ユーザー情報取得成功
                    resolve(_user);
                } else {
                    resolve(null);
                }
            });
        });  
    }

ハマったポイント

上記のユーザー情報の取得だが、ログイン直後でなくても
使用ブラウザで過去にログインしたことがあれば、基本的に取得できます。

ただし、上で紹介した1番目の方法は注意が必要となります。
firebase.auth().currentUserで情報取得が成功した場合は問題ないです。

失敗した場合が問題で、仕様上、ユーザー情報がない場合はもちろん失敗するが、
初期化が終了していなかった場合も失敗してしまいます。 また、これを見分ける手段はないです。
そのため、1番目の方法だと初期化直後の1回目はだいたいの場合、取得に失敗してしまいます。

2番目の方法は初期化が終わるのを待ってから返してくれるのでその心配はないです。

なので、ログイン直後は1番目の方法で、それ以外の場合は2番目の方法で、と使い分けるとよいと考えています。

最後に

この記事を書くにあたり改めてテストアプリを作成してみましたが、ログイン機能の実装は簡単にできるなと改めて感じました。
もしFirebaseでログイン機能を実装することがあった場合にこの記事に参考になる点があれば嬉しく思います。

ここまでご覧いただきありがとうございました。

参考:JavaScriptでメールリンクを使用してFirebase認証を行う

「Firebase」はGoogle LLCの商標または登録商標です。 tecotec.co.jp