LINEログイン

最終更新: 2022年01月31日

概要

Firebaseカスタム認証を使えばLINEでFirebaseアプリにログインすることができます。

  1. LINEのユーザーID取得する
  2. LINEのユーザーIDを使ってカスタム認証を行う

フローは上記の通りです。シンプルですが、LINEのユーザーIDを取得するまでに次のアクションが必要です。

  1. LINEチャネルを作成
  2. LINE連携用のURLを作成
  3. 上記URLより連携を行う(認可コードが生成される)
  4. 認可コードを使ってアクセストークンを取得
  5. アクセストークンを使ってユーザー情報を取得(ユーザーIDつき)

準備

  • LINEチャネルの作成
  • IAM>メンバー編集>ロール編集

末尾が@appspot.gserviceaccount.comのメンバーを編集し、ロールに「サービスアカウントトークン作成者」を追加します。これがないとカスタムトークンを作成できません。

サービスアカウントトークン作成者を追加

LINE連携用のURLを作成

チャネルのクライアントIDやリダイレクトURIを使ってAngularでURLを生成する。リダイレクトURIはアクセストークン取得用Webhookのエンドポイントにする。Webhookとは外部からのアクセスを許可するURLを指します。チャネルに設定したリダイレクトURIと必ず一致させる必要があります。

pages/login.tsx
const [lineLoginURL, setLineLoginURL] = useState<string>(); const getLineLoginURL = async () => { // stateを生成&取得 const callable = httpsCallable(fns, 'createState'); const state = await callable({}); const url = new URL('https://access.line.me/oauth2/v2.1/authorize'); url.search = new URLSearchParams({ response_type: 'code', // 固定でcodeとする client_id: NEXT_PUBLIC_LINE_CLIENT_ID, // チャネルのクライアントID state, // stateを設定 scope: 'profile openid email', // LINEから取得する情報 bot_prompt: 'aggressive', // ログイン時にBOTと連携させたい場合 redirect_uri: NEXT_PUBLIC_LINE_REDIRECT_URL, }).toString(); setLineLoginURL(url.href); }
<a href={lineLoginURL} target="_blank" rel="noopenner">LINEでログイン</a>

stateの返却

stateを作成し、返却するための関数を作成します。生成したランダム文字列をFirestoreに保存しておき、LINE認証後に返却されるstateと照合します。合言葉(state)を決めた上でLINEにお使いに出して、戻ってきた際に合言葉で本人確認を行うイメージです。前述のコードでは以下の関数を呼んでいます。

functions/state.ts
export const createState = functions .region('asia-northeast1') .https.onCall(async (data, context) => { // ランダム文字列を生成 const state: string = admin.firestore().collection('_').doc().id; await admin.firestore().doc(`states/${state}`).set({ state }); return state; });

生成したURLをクリックするとLINEログイン画面に移動します。LINEログインを行うとLINEから認可コードと先ほど指定したstateを持たされた状態でリダイレクトURIに返ってきます。リダイレクトURIはFunctionsとなります。以下の関数ではstate(合言葉)で本人確認を行った上で、LINEから受け取った認可コードをクエリーパラメータに添えた状態でクライアントに飛ばしています。

functions/line-code-webhook.ts
export const getLineCodeWebhook = functions .region('asia-northeast1') .https.onRequest(async (req, res) => { const code = req.query.code; const state = req.query.state; const isValidState = (await admin.firestore().doc(`states/${state}`).get()) .exists; if (!isValidState) { return; } if (code) { res.redirect(`http://localhost:3000/login?code=${code}`); } else { res.redirect(`http://localhost:3000`); } });

カスタムトークンの作成

上記の関数により表示されたページで認可コードを受け取り、それを使って

  • 認可コードを使ってLINEアクセストークンを取得
  • LINEアクセストークンを使ってLINEユーザー情報を取得
  • LINEユーザー情報を使ってFirebaseカスタムトークンを作成
  • Firebaseカスタムトークンを使ってFirebaseカスタム認証

という処理を一気に行います。

pages/login.tsx
useEffect(() => { const code = router.query.code; if (!code) { return; } // Firebaseログイン用のカスタムトークン取得 const callable = httpsCallable(fns, 'getCustomToken'); const customToken = await callable({ code }); // 認証(Firebaseログイン) signInWithCustomToken(auth, customToken); }, [router.query.code]);

カスタムトークンは以下の関数で取得しています。

functions/custom-token.ts
/** * 認可コードをLINEアクセストークンを取得 * @param code 認可コード */ const getAccessToken = async (code: string) => { return fetch('https://api.line.me/oauth2/v2.1/token', { method: 'post', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ code, grant_type: 'authorization_code', client_id: functions.config().line.client_id, client_secret: functions.config().line.secret, redirect_uri: 'https://asia-northeast1-training-4e164.cloudfunctions.net/getLineCodeWebhook', }), }).then((r) => r.json()); }; /** * アクセスコードを使ってLINEトークン&ユーザー情報を取得 */ export const getCustomToken = functions .region('asia-northeast1') .https.onCall(async (data, context) => { if (!data) { return; } // 認可コードを使ってアクセストークン&ユーザーを取得 const lineUser = await fetch('https://api.line.me/oauth2/v2.1/verify', { method: 'post', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ id_token: (await getAccessToken(data.code)).id_token, client_id: functions.config().line.client_id, }), }).then((r) => r.json()); // Firebaseログインに用いるUIDを管理 let uid: string = context.auth?.uid as string; // LINE連携済みのユーザーを取得 const connectedUser = ( await admin .firestore() .collection('users') .where('lineId', '==', lineUser.sub) .get() ).docs[0]; if (uid && !connectedUser.exists) { // ログイン中のユーザーにLINEを連携 await admin.firestore().doc(`users/${uid}`).set( { lineId: lineUser.sub, }, { merge: true } ); } else if (!uid && connectedUser.exists) { // LINE連携済み既存ユーザーID uid = connectedUser.id; } else if (!uid && !connectedUser.exists) { // 未ログインかつ連携済みユーザーがいなければユーザー新規作成 uid = lineUser.sub; await admin.firestore().doc(`users/${uid}`).set( { lineId: lineUser.sub, name: lineUser.name, photoURL: lineUser.picture, email: lineUser.email, createdAt: new Date(), }, { merge: true } ); } return await admin.auth().createCustomToken(uid); });

他SNSとの共存

上記関数は他SNS認証との共存が可能な設計になっています。既に他SNSでログインしていた場合、LINEアカウントが既存アカウントに統合される形になります。逆にLINEで認証しているアカウントに他SNSを統合したい場合、以下のような実装を行います。

loginWithGoogle() { if (this.user) { // ユーザーが存在すれば既存ユーザーにGoogle認証を統合 linkWithPopup(auth.currentUser!, new GoogleAuthProvider()) } else { // そうでなければ新たにGoogle認証ユーザーを作成(ログイン)する return signInWithPopup(auth, new auth.GoogleAuthProvider()); } }

以上です。従来のSNSログイン同様認証ユーザーが作成され、従来の認証と同様に扱うことができます。

Cloud Functionsのuser().onCreate()トリガーはカスタム認証(つまりLINEログイン)では起動しない点に注意しましょう。今回の実装ではLINEによる初回認証のタイミングでユーザーデータを作成しています。