🦁

【Flutter】Firebase AuthとLINE SDKでLINEログインを実装する

2021/11/24に公開
2

まえがき

FlutterのアプリにFirebase AuthとLINE SDKを使って、LINEログインを実装してみた。
Firebase×LINEのユースケースが多くないのか、あまりまとまった情報がなく苦戦したので、参考程度に手順を書き留めておく。

概略

LINEログインは、Firebase Authが公式対応していないため、Firebaseのカスタム認証を使用する形で実装する。

カスタム認証にはカスタムトークンが必要だが、それはアプリ(Flutter)側では生成できないので、カスタムトークン取得用にCloud Functionsも使用する。

イメージ図

ログイン画面を作成する

諸々のログイン処理を実装する前に、まずは簡単にログイン画面を作成する。
firebase_authの詳細は割愛するが、すでにパッケージをインストールしている前提。
ちなみに、今回は状態管理にflutter_riverpodを使用して、Viewとロジックを分離している。

ファイル構成は以下の通り。

lib/
 ├ main.dart
 └ app/
    ├ sign_in/
    │  └ sign_in_page.dart (View)
    │
    └ auth_manager/
       └ auth_manager.dart (ロジック)

sign_in_page.dartのコードは以下の通り。
Sign Inボタンがあるだけのシンプルな画面で、ボタンタップでauth_manager.dart(内容は後述)のログイン処理を呼び出す。

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../auth_manager/auth_manager.dart';

class SignInPage extends ConsumerWidget {
  const SignInPage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      body: Center(
        child: ElevatedButton(
          onPressed: () => 
		  ref.read(authManagerProvider).signInWithLine(),
          child: const Text('Sign In with LINE'),
        ),
      ),
    );
  }

auth_manager.dartの内容は以下の通り。
今回のLINEログインとは直接関係ないが、ログイン状態を判定する処理も記述している。

import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

final authManagerProvider = ChangeNotifierProvider<AuthManager>(
  (ref) {
    return AuthManager();
  },
);

class AuthManager with ChangeNotifier {
  AuthManager() {
    _firebaseAuth.authStateChanges().listen((user) {
      isLoggedIn = user != null;
      notifyListeners();
    });
  }
  final _firebaseAuth = FirebaseAuth.instance;
  bool isLoggedIn = false;
   
  Future<void> signInWithLine() async {
    // 後ほどここにログイン処理を実装していく
  }
}

LINEログインの実装

LINE Developersのチャンネルを作成する

LINEログインを実装するにあたって、LINE Developersのチャンネルを作成しておく必要がある。
LINE Developerの登録はだいぶ前にやったきりで記憶が曖昧なので割愛。

チャンネル作成

LINE Developerの登録ができたら、こちらのページの今すぐはじめようをクリックして作成を進める。

以下のように、必要事項を入力する。
入力した情報は後から編集できるので、迷ったら適当でOK。


ちなみに、チャンネル名チャンネル説明は、以下のようにログイン時に表示される。
チャンネルアイコンもおそらくここでの表示使われる。
(画像はAndroid)

チャンネル作成完了

作成が完了すると、チャンネルの設定内容が表示される。
ここで確認できるチャンネルIDを後ほど使用するので、メモしておく。

LINEログイン設定

ここでLINEログイン設定を開き、iOS>iOS bundle ID, Android>パッケージ名もそれぞれ設定しておく。
(確認方法は下記の通り)

iOSのiOS bundle ID

以下コマンドでXCodeを開き、Runner>General>bundle Identifierで確認

$ open ios/Runner.xcworkspace/

Androidのパッケージ名

android/app/src/main/AndroidManifest.xmlの最初に書いてある

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="your_package_name">

LINE SDK for Flutterを導入する

ここまででLINE Developers上での設定は終わったので、アプリ側に戻る。

アプリからLINEログインするために、LINE SDKを導入する必要がある。
まずはパッケージを追加する。

iOSの設定

パッケージのReadmeを参考に、ios/Runner/Info.plistに以下の内容を追記。

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleTypeRole</key>
    <string>Editor</string>
    <key>CFBundleURLSchemes</key>
    <array>
      <!-- Specify URL scheme to use when returning from LINE to your app. -->
      <string>line3rdp.$(PRODUCT_BUNDLE_IDENTIFIER)</string>
    </array>
  </dict>
</array>
<key>LSApplicationQueriesSchemes</key>
<array>
  <!-- Specify URL scheme to use when launching LINE from your app. -->
  <string>lineauth2</string>
</array>

また、ios/Podfileに以下の内容を追記。(元からある場合は不要)

target 'Runner' do
  use_frameworks!               // 追記
  platform :ios, '10.0'   // 追記

Androidの設定

android/app/build.gradleminSdkVersionを21(以上)にする。(元からなっている場合はそのままでOK)

defaultConfig {    
    minSdkVersion 21  // 21以上にする

main.dartに追記

iOS/(Android)の設定ができたら、main.dartLineSDK.instance.setup("チャンネルID");を追記する。
チャンネルIDは先ほど作成したチャンネルのIDを入れる(String)。

今回の場合は、riverpod, firebaseを使用しているので、以下のような内容になっている。

import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter_line_sdk/flutter_line_sdk.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'app/auth_manager/auth_manager.dart';
import 'app/home_page/home_page.dart';
import 'app/sign_in/sign_in_page.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(); // firebaseの初期化
  // ↓に先ほど作成したチャンネルのチャンネルIDを入れる(String)
  await LineSDK.instance.setup("チャンネルID"); 
  runApp(
    // riverpod用の記述
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends ConsumerWidget {
  const MyApp({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    final _authManager = ref.watch(authManagerProvider);
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'タイトル',
      // ログイン中:ホーム画面、未ログイン:ログイン画面
      home: _authManager.isLoggedIn ? const HomePage() : const SignInPage(),
    );
  }
}

これでひとまず、アプリ側からLINEログインが可能になった。

Firebaseのカスタムトークンを取得する関数を作成

ここまでで、イメージ図①LINEログインして LINEのuser情報取得ができる状態になった。
次は②LINEから取得したuserIdを使用して Cloud FunctionsでFirebase用のカスタムトークンを作成を可能にするため、Cloud Functionsの関数を作成する。

Cloud Functionsの概要、使い方については割愛。

Firebase Admin SDKの準備

カスタムトークンの作成にはAdmin SDKが必要なので、npmでパッケージをインストール。

path/to/your_app/functions/
$ npm install firebase-admin

Admin SDKの利用にはサービスアカウントのキーが必要なので、Firebaseコンソールのプロジェクト設定から、キー(json)をダウンロードし、path/to/your_app/functions/に配置する。

関数作成→デプロイ

Admin SDKの準備ができたら、path/to/your_app/functions/src/index.tsに関数を書いていく。(今回はTypeScriptを使用)

Cloud Functionsのトリガーはいくつか種類があるが、今回はアプリ側から呼び出すためHTTPリクエストにした。

import * as functions from "firebase-functions";
import * as admin from "firebase-admin";

// 先ほど配置したサービスアカウントのキーのパスを指定する
const serviceAccount = require("../xxxxx.json");

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount)
});

exports.fetchCustomToken = functions
.region('asia-northeast1')
.https
.onRequest(async(request, response) => {
  const userId = request.body.data.userId;

  // userIdが不正の場合はエラーで終了
  if (typeof userId !== "string"){
    console.log("userId is not string");
    response.status(404).send({
      data: {"error": "userId is not string"},
    });
    return;
  }
  
  const customToken = await admin.auth().createCustomToken(userId);
  response.status(200).send({
    data: {"customToken": customToken},
  });
});

これをデプロイすればOK。

アプリ側のログイン処理を実装

いよいよ最後の手順。

cloud_functionsパッケージのインストール

アプリ側からCloud Functionsを呼び出すため、パッケージをインストールしておく。
パッケージはこちら

auth_manager.dartの最終形

一連の準備が整ったので、いよいよauth_manager.dartを完成させる。
コードは以下の通り。

import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter_line_sdk/flutter_line_sdk.dart';  // LINE SDK追加
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:cloud_functions/cloud_functions.dart'; // cloud_functions追加

final authManagerProvider = ChangeNotifierProvider<AuthManager>(
  (ref) {
    return AuthManager();
  },
);

class AuthManager with ChangeNotifier {
  AuthManager() {
    _firebaseAuth.authStateChanges().listen((user) {
      isLoggedIn = user != null;
      notifyListeners();
    });
  }
  final _firebaseAuth = FirebaseAuth.instance;
  final _lineSdk = LineSDK.instance;  // LINE SDKのインスタンス
  bool isLoggedIn = false;
   
  // ログイン処理
  Future<void> signInWithLine() async {
    // LINEログインし、LINEのuserIdを取得する
    final result = await _lineSdk.login();
    final lineUserId = result.userProfile?.userId;
    
    // LINEのuserIdを使って、Cloud Functionsからカスタムトークンを取得する
    final callable = FirebaseFunctions.instanceFor(region: 'asia-northeast1')
        .httpsCallable('fetchCustomToken');
    final response = await callable.call({
      'userId': lineUserId,
    });
    
    // functionsから取得したカスタムトークンを使用して、Firebaseログイン
    await _firebaseAuth.signInWithCustomToken(response.data['customToken']);
  }
  
  // ログアウト処理
  Future<void> signOut() async {
      // LINE, Firebase両方でログアウトする
    await _lineSdk.logout();
    await _firebaseAuth.signOut();
  }
}

冒頭でも説明した通り、まずはLINEログインして、その後カスタムトークン取得→Firebaseログインという流れ。

またログアウトについても、LINE, Firebase両方行う。

いざログイン!!

一通り実装が終わったので、まずはAndroidでビルドしてログインを試してみる。

私の場合は普段iPhoneを使っているので、動作確認のためだけに一度AndroidでLINEにログインした。

無事ログインに成功した!!

Firebaseのコンソールを見てみると、userが追加されているのがわかる。
ちなみにこのユーザー UIDは、LINEから取得したuserProfile.userIdになっている。

ログアウト→再度ログインしてみたところ、アカウントが重複することなく無事同じuserとしてログインできた。

後ほどiPhoneでも、同じLINEアカウントを使用してログインを試したところ、こちらも無事成功した。

さいごに

長かったが、Firebase AuthとLINE SDKを組み合わせて、LINEログインを実装することができた。
ただ、本アプリはリリース前のため、本番環境で動作するかはまだ検証していない。

もしかしたら他にも設定が必要かもしれないので、そちらは分かり次第追記予定。

Firebase Authのカスタム認証以外の(Clound Functionsを使わずに済む)方法も、改めて検討したい。

こんな記事もあり、LINEログインの必要性を再検討してみてもいいかも・・・。
https://qiita.com/pochi-sato/items/c1d51f310f5427253a3b

追記

Cloud Functionsを使わないやり方も書いてみた。
https://zenn.dev/yskuue/articles/7f0a93f9e17b50

参考記事

https://techblog.yahoo.co.jp/advent-calendar-2018/firebase-flutter-yid/
https://firebase.google.com/docs/auth/admin/create-custom-tokens?hl=ja
https://firebase.flutter.dev/docs/functions/usage
https://csiandal.medium.com/firebase-cloud-function-error-response-is-not-valid-json-object-401221c3cb89

Discussion

Kosuke SaigusaKosuke Saigusa

はじめまして。
Firebase Auth と LINE ログインの流れや、LINE Developer のダッシュボード上での作業の流れまでもが整理された素敵な記事だと思いました。

が、セキュリティ的に危険な実装がありますのでコメントさせていただきます。

具体的には、下記の部分で、LINE ログインによって Flutter クライアントで得られた LINE の user ID を直接 Cloud Functions で実装したサーバに送信している点が、

// ログイン処理
Future<void> signInWithLine() async {
  // LINEログインし、LINEのuserIdを取得する
  final result = await _lineSdk.login();
  final lineUserId = result.userProfile?.userId;
    
  // LINEのuserIdを使って、Cloud Functionsからカスタムトークンを取得する
  final callable = FirebaseFunctions.instanceFor(region: 'asia-northeast1')
      .httpsCallable('fetchCustomToken');
  final response = await callable.call({
    'userId': lineUserId,
  });
    
  // functionsから取得したカスタムトークンを使用して、Firebaseログイン
  await _firebaseAuth.signInWithCustomToken(response.data['customToken']);
}  

LINE ログインのセキュリティチェックリスト:

https://developers.line.biz/ja/docs/line-login/security-checklist/

の「バックエンドサーバーに、IDトークンやアクセストークンを送信して処理する際のチェックリスト」の

「クライアントからバックエンドサーバーに対して、ユーザーIDなどの情報ではなく、生のIDトークンやアクセストークンを送信しているか。」

の項目に反するためです。

たとえば、LINE の user ID 文字列が他人に漏れると、誰でもその人として Firebase Auth のカスタムトークン認証によるログインができてしまうような実装になっています。

アクセストークンを使用して新規ユーザーを登録する:

https://developers.line.biz/ja/docs/line-login/secure-login-process/#using-access-tokens

の「危険な方法」の図:

が示すように、user ID をクライアントアプリから直接送信し、使用してはいけません。

代わりに同ページの「安全な方法」の図:

が示すように、生のアクセストークンを送信して、Cloud Functions などのサーバ側でそのアクセストークンの有効性を検証した上で直接 LINE Platform から得られる user ID などを使用してカスタムトークン認証をしなければいけません。

YusukeYusuke

ご指摘ありがとうございます!

セキュリティ面は十分な考慮ができていなかったため、詳細ご説明いただき非常に参考になります。