LIFFアプリにFirebase Authentication導入してみた
こんにちは。
LIFFアプリにFirebase Authentication導入したくないですか?
LIFFアプリって?
LINE上で動かせるやつです。Webでも普通に動きます。
LINEの認証情報が使えるのでユーザーのアカウント作成する手間が省けます。
詳しくはLINEの公式ページとか見てみると良いかもです。
※ 今回はLIFFアプリの作成手順とかは説明しません
※ フロントエンドにNextjs使います
※ LINEがLIFFアプリのスターターを公開しているので参考にしてみると良いかもです。https://github.com/line/line-liff-v2-starter
やっていきます。
方針
LINE上でLIFFアプリを起動させると自動的にLINEアカウントで認証された状態になります。
(直接Webで起動した場合はLINEのログイン画面をはさみます)
よって、今回はその認証情報を利用してFirebase Authenticationにも認証していきます。
Firebase Authenticationのログイン方法にsignInWithCustomToken
というものがあるのはご存知ですか?
詳しくは公式ドキュメントで↓
このsignInWithCustomToken
というメソッドを使えばLINEのUser ID
を識別子としてFirebase認証することが可能になります。
カスタムトークンでログインするには、バックエンドでcreateCustomeToken('識別子')
によってトークンを作成し、フロントエンドでsignInWithCustomToken(getAuth(), 'バックエンドで生成したトークン')
してやります。
簡単ですよね?
注意点
1つ注意点があります。
それは、LINEのUser ID
を直でバックエンドに渡してはいけないということです。
どういうことかというと、
手順1. (フロントエンド) LIFFアプリを起動してLINE認証する
↓
手順2. (フロントエンド) LINEのUser ID
を直でbodyに詰めてバックエンドに投げる
↓
手順3. (バックエンド) bodyで受け取ったUser ID
でcreateCustomeToken
してレスポンス
↓
手順4. (フロントエンド) バックエンドから受け取ったカスタムトークンでsignInWithCustomToken
する
↓
Firebase認証完了!!
こうしてしまうとセキュリティ的にやばいです。
バックエンドがフロントエンドから送られてきたUser ID
という値を鵜呑みにしてしまうといくらでもなりすましが可能になってしまいますよね。
そこでフロントエンドから送られてきたものをバックエンドで検証できるようにします。
LIFFにはIDトークンというものが存在します。
IDトークンとは、LIFFアプリで生成することができます。そして、LINEが公開しているAPIでIDトークンを検証することができます。検証に成功するとUser ID
やプロフィール名などの情報が取得できるみたいな感じになっています。
これを使えば安全に実装できそうですね。
実装していきます。
実装
処理の流れ
手順1. (フロントエンド) LIFFアプリを起動してLINE認証する
↓
手順2. (フロントエンド) liff.getIDToken()
で取得したIDトークンをbodyに詰めてバックエンドに投げる
↓
手順3. (バックエンド) bodyで受け取ったIDトークンを検証する
↓
手順3. (バックエンド) 検証して取得できたUser ID
でcreateCustomeToken
してレスポンス
↓
手順4. (フロントエンド) バックエンドから受け取ったカスタムトークンでsignInWithCustomToken
する
↓
Firebase認証完了!!
先程の流れにIDトークンの検証を追加しただけです。
バックエンド
今回Firebaseを使っているということでバックエンドはFirebase Functionsを使用します。また、http関数にすると普通のAPIを叩くようにFunctionsを呼び出せるのですがCORSの設定とかだるいので今回は呼び出し可能関数というものを使用します。
Functionsならではの書き方が混ざっていますがそこは各々書き換えてください。
(検証のAPI叩くときにnode-fetch使いたかったけどFunctionsでうまく動かなかったので泣く泣くrequest-promise
使ってます;;)
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叩くようにしたりと各々書き換えてみてください。
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