😆

LIFFアプリにFirebase Authentication導入してみた

2022/01/30に公開

こんにちは。
LIFFアプリにFirebase Authentication導入したくないですか?

LIFFアプリって?

LINE上で動かせるやつです。Webでも普通に動きます。
LINEの認証情報が使えるのでユーザーのアカウント作成する手間が省けます。
詳しくはLINEの公式ページとか見てみると良いかもです。
https://lineapiusecase.com/ja/api/miniliff.html

※ 今回はLIFFアプリの作成手順とかは説明しません
※ フロントエンドにNextjs使います
※ LINEがLIFFアプリのスターターを公開しているので参考にしてみると良いかもです。https://github.com/line/line-liff-v2-starter

やっていきます。

方針

LINE上でLIFFアプリを起動させると自動的にLINEアカウントで認証された状態になります。
(直接Webで起動した場合はLINEのログイン画面をはさみます)
よって、今回はその認証情報を利用してFirebase Authenticationにも認証していきます。

Firebase Authenticationのログイン方法にsignInWithCustomTokenというものがあるのはご存知ですか?
詳しくは公式ドキュメントで↓
https://firebase.google.com/docs/auth/admin/create-custom-tokens?hl=ja

このsignInWithCustomTokenというメソッドを使えばLINEのUser IDを識別子としてFirebase認証することが可能になります。

カスタムトークンでログインするには、バックエンドでcreateCustomeToken('識別子')によってトークンを作成し、フロントエンドでsignInWithCustomToken(getAuth(), 'バックエンドで生成したトークン')してやります。

簡単ですよね?

注意点

1つ注意点があります。
それは、LINEのUser IDを直でバックエンドに渡してはいけないということです。
どういうことかというと、

手順1. (フロントエンド) LIFFアプリを起動してLINE認証する

手順2. (フロントエンド) LINEのUser IDを直でbodyに詰めてバックエンドに投げる

手順3. (バックエンド) bodyで受け取ったUser IDcreateCustomeTokenしてレスポンス

手順4. (フロントエンド) バックエンドから受け取ったカスタムトークンでsignInWithCustomTokenする

Firebase認証完了!!

こうしてしまうとセキュリティ的にやばいです。
バックエンドがフロントエンドから送られてきたUser IDという値を鵜呑みにしてしまうといくらでもなりすましが可能になってしまいますよね。

そこでフロントエンドから送られてきたものをバックエンドで検証できるようにします。
LIFFにはIDトークンというものが存在します。
https://developers.line.biz/ja/docs/liff/using-user-profile/#sending-id-token

IDトークンとは、LIFFアプリで生成することができます。そして、LINEが公開しているAPIでIDトークンを検証することができます。検証に成功するとUser IDやプロフィール名などの情報が取得できるみたいな感じになっています。
これを使えば安全に実装できそうですね。

実装していきます。

実装

処理の流れ

手順1. (フロントエンド) LIFFアプリを起動してLINE認証する

手順2. (フロントエンド) liff.getIDToken()で取得したIDトークンをbodyに詰めてバックエンドに投げる

手順3. (バックエンド) bodyで受け取ったIDトークンを検証する

手順3. (バックエンド) 検証して取得できたUser IDcreateCustomeTokenしてレスポンス

手順4. (フロントエンド) バックエンドから受け取ったカスタムトークンでsignInWithCustomTokenする

Firebase認証完了!!

先程の流れにIDトークンの検証を追加しただけです。

バックエンド

今回Firebaseを使っているということでバックエンドはFirebase Functionsを使用します。また、http関数にすると普通のAPIを叩くようにFunctionsを呼び出せるのですがCORSの設定とかだるいので今回は呼び出し可能関数というものを使用します。
https://firebase.google.com/docs/functions/callable?hl=ja

Functionsならではの書き方が混ざっていますがそこは各々書き換えてください。
(検証のAPI叩くときにnode-fetch使いたかったけどFunctionsでうまく動かなかったので泣く泣くrequest-promise使ってます;;)

backend.ts
import * as admin from 'firebase-admin';
import * as functions from 'firebase-functions';
import * as rq from 'request-promise';

admin.initializeApp();

export const verify = functions.https.onCall(async (data, context) => {
  // id tokenを取得
  const idToken = data.idToken;

  try {
    // id tokenの有効性を検証する
    const data = await rq({
      method: 'POST',
      uri: 'https://api.line.me/oauth2/v2.1/verify',
      form: {
        id_token: idToken,
        client_id: '1656847135',
      },
      json: true,
    });

    // LINE IDでfirebaseトークンを発行して返却
    const token = await admin.auth().createCustomToken(data.sub);
    return { token };
  } catch (err) {
    console.log(err);
    throw new functions.https.HttpsError('unknown', 'error');
  }
});

フロントエンド

LIFFの初期化はできているという前提で、今回はカスタムフックとして切り出したロジック部分だけを載せます。
Firebase Functionsを使用しているのでバックエンド呼び出しの部分は特殊ですが、fetchでAPI叩くようにしたりと各々書き換えてみてください。

hooks/useAuth.ts
import { Liff } from "@line/liff";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
  getAuth,
  onAuthStateChanged,
  signInWithCustomToken,
  signOut,
  User,
} from "firebase/auth";
import { getFunctions, httpsCallable } from "firebase/functions";

type Args = {
  liff: Liff | null;
};

export const useAuth = ({ liff }: Args) => {
  const [authUser, setAuthUser] = useState<User | null | undefined>(undefined);

  useEffect(() => {
    onAuthStateChanged(getAuth(), (user) => {
      setAuthUser(user);
    });
  }, []);

  useEffect(() => {
    if (!liff || authUser) return;
    const idToken = liff.getIDToken();
    if (!liff.isLoggedIn() || !idToken) {
      setAuthUser(null);
      return;
    }
    const verify = httpsCallable(getFunctions(), "verify");
    verify({ idToken })
      .then((result: any) => {
        signInWithCustomToken(getAuth(), result.data.token);
      })
      .catch((error) => {
        console.log(error);
      });
  }, [liff]);

  const isLoading = useMemo(() => {
    return authUser === undefined;
  }, [authUser]);

  const isLoggedIn = useMemo(() => {
    return !!authUser;
  }, [authUser]);

  const logout = useCallback(() => {
    if (!liff) return;
    liff.logout();
    signOut(getAuth());
  }, [liff]);

  return {
    authUser,
    isLoading,
    isLoggedIn,
    logout,
  };
};

まとめ

今回はLIFFアプリでFirebase認証する方法を紹介しました。
LIFFアプリもFirebaseも便利ですね。

Discussion