FirebaseのFCMを利用した認証手続き(Linphone Android + Flexisip API)

Push Notification

相手方呼び出し時、相手方の端末がスリープモード等により呼出しリクエストに応答しないケースで、Firebaseからのプッシュ通知を利用して接続を確立。Flexisipサーバ側の設定が必要です。

Push notification configration


Push Gateway

600px-Pushgateway_incoming_call

ContactRouterInserter

FlexisipがSBCゲートウェイとして使用される場合、このモジュールは登録情報の経路を調整し、正しい送り先に着信呼び出しをルーティングできるようにします。

SBC (Session Border Controller) (セッション・ボーダー・コントローラ)の略で、通信ネットワークにおいてセッション(通話やデータ転送などの通信セッション)の境界で制御を行う装置やソフトウェア

アカウントマネージャのAPIでFirebaseによるプッシュ通知を利用する場合、アカウントマネージャを稼働しているphp-fpmコンテナ経由でflexisipサーバコンテナへコマンド送信する必要があるため、php-fpmコンテナにSSH接続などによる機能追加・PHPコードの変更などが必要になります。LaravelのフレームワークにSSHライブラリを追加することで対応できますがデバックなどに時間を要します。

このため、flexisipイメージにphp-fpmの機能を追加した新たなイメージを作成し、このイメージからflexisipサーバとphp-fpmを同時に立ち上げることにします。

PHPとその機能拡張、Composerのインストールを追加

flex-from-ubuntu-apt-repo

FROM ubuntu:22.04
MAINTAINER  Takanobu Fuse <[email protected]>

ARG DEBIAN_FRONTEND=noninteractive

# Prepare dependencies
RUN apt-get update
RUN apt-get install -y nano xsdcxx gdb libmariadb3 snmp-mibs-downloader snmp snmpd iproute2 iptables wget gnupg2 \ 
    php8.1-fpm libpng-dev libfreetype-dev libjpeg-turbo8-dev libxml2-dev curl zip unzip \
    php8.1-cli php8.1-common php8.1-mysql php8.1-zip php8.1-gd php8.1-mbstring php8.1-curl php8.1-xml php8.1-xmlrpc \
    php8.1-bcmath php8.1-redis
    
RUN echo 'deb [arch=amd64] http://linphone.org/snapshots/ubuntu jammy stable' > /etc/apt/sources.list.d/belledonne.list
RUN wget https://www.linphone.org/snapshots/ubuntu/pubkey.gpg -O - | apt-key add -
RUN apt-get update && apt-get install -y bc-flexisip

# Set working directory
WORKDIR /var/www/html

# Installing composer
COPY composer_installer.sh ./
RUN chmod +x composer_installer.sh
RUN echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf \
    && ./composer_installer.sh && mv composer.phar /usr/local/bin/composer
    
# Installing Laravel
RUN chown -R www-data:www-data /var/www/html
RUN composer global require laravel/installer \
    && ln -s /root/.config/composer/vendor/laravel/installer/bin/laravel /usr/local/bin/laravel
###### for fleisip-account-manager until the above line

# Add it to the default path
ENV PATH=$PATH:/opt/belledonne-communications/bin

WORKDIR /opt/belledonne-communications

# Generate a default configuration
RUN flexisip --dump-default all > /etc/flexisip/flexisip.conf

VOLUME /etc/opt/belledonne-communications/flexisip
VOLUME /var/opt/belledonne-communications/log/flexisip
COPY flexisip-entrypoint.sh /
COPY backtrace.gdb /
RUN chmod a+x /flexisip-entrypoint.sh

# Script to wait db before launch flexisip [Licence Apache2]
ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.2.1/wait /wait
RUN chmod +x /wait

ENTRYPOINT ["/flexisip-entrypoint.sh"]
CMD flexisip

起動スクリプトにphp-fpmの起動コマンド"/etc/init.d/php8.1-fpm start"を追加

flexisip-entrypoint.sh

#!/bin/bash
ulimit -c unlimited
set -- flexisip "$@"

# Wait for needed container startup
/wait || exit $?

echo "Flexisip docker params : $*"

if [[ $@ == *"--server"* ]]; then
  echo "--server param found, starting only one Flexisip instance"
  "$@" &
else
  echo "Server param not found, starting 3 Flexisip instances (proxy, presence, conference)"
  ( "$@" --server proxy 2>&1| tee /var/opt/belledonne-communications/log/flexisip/flexisip_proxy_stdout.log ) &
  ( "$@" --server presence 2>&1| tee /var/opt/belledonne-communications/log/flexisip/flexisip_presence_stdout.log ) &
  ( "$@" --server conference 2>&1| tee /var/opt/belledonne-communications/log/flexisip/flexisip_conference_stdout.log ) &
fi

/etc/init.d/php8.1-fpm start

wait -n

echo "At least one server crashed, stopping every sub process that is still alive"
pkill -P $$

# coredump management, used in unit tests
# we execute gdb on each coredump, with the options given in backtrace.gdb file
# we search in /root because it's the workdir set in Dockerfile and because by default our coredumps are generated by default in the working directory
if [ -n "$(find /root -type f -name "core*")" ]; then
	find /root -type f -name "core*" | xargs -L1 gdb flexisip -x /backtrace.gdb;
fi

Nginxのfastcgi_passを変更

Dockerネットワークのゲートウェイアドレス:172.17.0.1などに変更

default.conf

    location = /index.php {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_keep_conn on;
        fastcgi_param  HTTPS 'on';
        fastcgi_pass   xx.xx.xx.xx:9000;
        fastcgi_index  index.php;
        include        fastcgi_params;
        fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

PHP-FPM設定ファイル:接続方法の変更

ListenがデフォルトでUnixソケットなので、ポートに変更します。このファイルをコンテナ起動時に読み込ませるため、下記ディレクトリから抜き出してDocker Composeファイルで読み込むように設定して下さい。

/etc/php/8.1/fpm/pool.d/www.conf

.....
.....

; The address on which to accept FastCGI requests.
; Valid syntaxes are:
;   'ip.add.re.ss:port'    - to listen on a TCP socket to a specific IPv4 address on
;                            a specific port;
;   '[ip:6:addr:ess]:port' - to listen on a TCP socket to a specific IPv6 address on
;                            a specific port;
;   'port'                 - to listen on a TCP socket to all addresses
;                            (IPv6 and IPv4-mapped) on a specific port;
;   '/path/to/unix/socket' - to listen on a unix socket.
; Note: This value is mandatory.
listen = 9000
.....
.....

アカウントマネージャ.envの変更

php-fpmコンテナのネットワークをブリッジからホストへ変更したため、アカウントマネージャの.envファイル内で指定したサービス名によるホスト指定をIPアドレスによるものに変更します。

flexiapi/.env

.....
.....
DB_CONNECTION=mysql
DB_HOST=172.xx.xx.xx
.....
.....
REDIS_CLIENT=phpredis # Use phpredis-sentinel and uncomment the REDIS_SENTINEL variable bellow
REDIS_HOST=172.xx.xx.xx
.....
.....

注)ポート9000へのアクセスがufwによって制限されていると504エラーが発生します。
ブリッジネットワークからのアクセスを許可します。

$ sudo ufw allow proto tcp from 172.xx.0.0/12 

PostmanによるFlexisipアカウントマネージャAPIテスト

Postmanでは各種APIの動作を検証できるウェブサービスが提供されています。

https://www.postman.com/

専用デスクトップアプリのダウンロード

Flexisip Account Manager API

1) Account Creation Request Tokens:アカウント作成リクエストトークン

このトークンはアカウント作成トークンを取得するために利用されます。
JSONフォーマットにより、アカウント作成リクエストトークンと認証URLが返されます。

2) Account Creation Tokens:アカウント作成トークン

アカウント作成トークンを取得するAPIは以下二通り用意されています。

  • 上記 1) で取得したアカウントリクエストトークンを利用するAPI
  • プッシュトークンを利用したAPI

Linphone AndroidのFCM認証について

サーバーから Firebase サービスにアクセスする場合、Firebase Authentication を使用する必要はありません。代わりに、Admin SDK を使用します。

FirebaseのFCMサービスを利用して、Android端末上でLinphoneの電話番号による登録手続きとログインが出来るようにします。FCMサービスを利用する際のポイントは、Androidアプリとウェブアプリの両者を如何に連携させるかです。

必要となる大きな作業は以下の3点です(開発用のGoogleアカウントは必須)。

  1. Firebaseでプロジェクトを登録
    登録したプロジェクト内でAndroidアプリであるLinphoneとウェブアプリであるFlexisip Account Managerをアプリとして追加します。ややこしいキーフレーズが満載ですが、Google純正のAIチャットBardの助けを借りながら理解して下さい。Firebaseと各アプリ間で安全に通信するための認証ファイル(キーファイル)、各通信で必要となるトークン(一時的なユーザID)を理解することがポイントです。

  2. ウェブアプリのコーディング
    Flexisip Account ManagerはLaravelのPHPフレームワークで構成されているため、PHPに対応したFirebaseの機能拡張ライブラリを追加する必要があります。この機能拡張クラスを使用してFirebaseのFCMをクライアントからのAPIリクエストに応じて送信します。

  3. Android Linphoneアプリのコーディング
    電話番号を利用したユーザ登録やログイン時、認証コードが記述されたFCMを受信・表示させるためのコードを追加します。

The following three major tasks are required (a Google account for development is required).

  1. Register a project in Firebase
    Add the Android app Linphone and the web app Flexisip Account Manager as apps in the project you registered. There are a lot of confusing key phrases, but please understand them with the help of Google’s AI chat Bard. The key points are to understand the authentication file (key file) for secure communication between Firebase and each app, and the token (temporary user ID) required for each communication.

  2. Coding for web app
    Since Flexisip Account Manager is composed of the Laravel PHP framework, you need to add the Firebase feature extension library that supports PHP. Use this feature extension class to send Firebase FCM in response to API requests from clients.

  3. Coding for Android Linphone app
    Add code to receive and display FCM containing the authentication code when registering or logging in users using a phone number.

追加・変更ファイル一覧 : Additional or Modified Files

  1. Firebase
    google-service.json —> linphone-android/app/google-services.json

    SHA Finger Print(SHA-1, SHA-256) —> For Using Google Play Services : API, Self-signing Your Application(SHA-1), Play Integrity API(SHA-256)

    $ keytool -list -v -alias androiddebugkey -keystore ~/.android/debug.keystore
    

    Firebase Cloud Messaging API(V1) —> Service Account(Tab) —> Firebase

    Admin SDK Secret Key File —> flexiapi/.env (kreait/firebase-php:FIREBASE_CREDENTIALS)

    Cloud Messaging API Server Key (Legacy) —> flexiapi/.env (APP_FLEXISIP_PUSHER_FIREBASE_KEY), flexisip.conf (firebase-projects-api-keys)

  2. Web App
    2-1. Flexisip Account Manager(PHP 8.1.2)
    flexiapi/.env

    APP_FLEXISIP_PUSHER_PATH=/opt/belledonne-communications/bin/flexisip_pusher
    APP_FLEXISIP_PUSHER_FIREBASE_KEY=xxxxxxxxxxxxxxxxxxxxxxx
    
    # https://console.firebase.google.com/project/_/database
    FIREBASE_DATABASE_URL=https://flexisip-xxxxx-default-rtdb.asia- southeast1.firebasedatabase.app
    # kreait/laravel-firebase: firebase service account
    FIREBASE_CREDENTIALS=/var/www/html/flexiapi/app/flexisip-xxxx-firebase-adminsdk-xxxxxx.json
    

    flexiapi/app/Services/AccountService.php

    use App\Libraries\FirebaseAuth;
    use Illuminate\Support\Facades\DB;
    
    public function requestPhoneChange(Request $request)
    {
        // Modified Codes
    }    
    
    public function recoverByPhone(Account $account): Account
    {
        // Modified Codes
    }
    

    flexiapi/app/Http/Controllers/Api/Account/AccountController.php

    use App\AccountCreationToken;
    use App\Libraries\FirebaseAuth;
    
    // if (!config('app.dangerous_endpoints')) return abort(404);
    
    public function storePublic(Request $request)
    {
        // Modified Codes
    }
    
    public function recoverByPhone(Request $request)
    {
        // Modified Codes
    }
    
    

    flexiapi/app/Libraries/FirebaseAuth.php (New : composer require “kreait/firebase-php:^7.0” )

    namespace App\Libraries;
    
    use Illuminate\Support\Facades\Log;
    use Kreait\Firebase\Contract\Auth;
    use Kreait\Firebase\Contract\Messaging;
    use Kreait\Firebase\Exception\Messaging\InvalidMessage;
    use Kreait\Firebase\Messaging\CloudMessage;
    use Kreait\Firebase\Messaging\AndroidConfig;
    use Kreait\Firebase\Messaging\RawMessageFromArray;
    use Kreait\Firebase\Exception\MessagingException;
    use Kreait\Firebase\Contract\Firestore;
    
    class FirebaseAuth
    {
        private $auth;
        private $messaging;
    
        // With Dependency Injection
        // https://firebase-php.readthedocs.io/en/stable/authentication.html  
        public function __construct(Auth $auth, Messaging $messaging)
        {
            $this->auth = $auth;
            $this->messaging = $messaging;
        }
        // Additional New Methods
    }
    

    2-2. Flexisip
    /etc/flexisip.conf

    firebase-projects-api-keys = [Firebase Project Number]:[Firebase Cloud Messaging
    # API (Legacy) Server Key]
    
  3. Android Linphone
    build.gradle

    dependencies {
         classpath 'com.android.tools.build:gradle:8.1.4'
         classpath 'com.google.gms:google-services:4.4.0'
         classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.21'
         classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.9'
     }
    

    app/build.gradle

    plugins {
    
     id 'com.google.gms.google-services'
    
     }
    
    
    def packageName = "com.ficusonline"
    
    dependencies {
    
         implementation 'androidx.work:work-runtime-ktx:2.9.0'
     
         implementation platform('com.google.firebase:firebase-bom:32.5.0')
    
         if (firebaseAvailable) {
             implementation 'com.google.firebase:firebase-messaging'
             implementation 'com.google.firebase:firebase-analytics'
             // Add the dependency for the Firebase Authentication library
             // When using the BoM, you don't specify versions in Firebase library dependencies
             implementation 'com.google.firebase:firebase-auth'
             // Also add the dependency for the Google Play services library and specify its version
             implementation 'com.google.android.gms:play-services-auth:20.7.0'
             //implementation 'java.util.concurrent:time:1.0'
             implementation "androidx.activity:activity-ktx:1.7.2"
             implementation "com.google.firebase:firebase-auth-ktx"
     }
    
    

    app/src/main/AndroidManifest.xml

         <service android:name="org.linphone.core.tools.firebase.FirebaseMessaging"
             android:enabled="true"
             android:exported="false">
             <intent-filter>
                 <action android:name="com.google.firebase.MESSAGING_EVENT" />
             </intent-filter>
         </service>
    

    app/src/main/java/org/linphone/core/tools/firebase/FirebaseMessaging.java (New)

    private void onPushReceived(RemoteMessage remoteMessage) {
    // Additional codes
    }
    private void buildDisplayNotification(String messageBody) {
    // Additional codes
    {
    

    app/src/main/assets/assistant_default_values

       <entry name="push_notification_allowed" overwrite="true">1</entry>
    

    app/src/main/assets/assistant_linphone_default_values

        <entry name="quality_reporting_collector" overwrite="true">sip:[email protected];transport=tls</entry>
    
        <entry name="reg_identity" overwrite="true">sip:[email protected]</entry>
        <entry name="reg_proxy" overwrite="true">&lt;sip:sip.ficusonline.com;transport=tls&gt;</entry>
        <entry name="reg_route" overwrite="true">&lt;sip:sip.ficusonline.com;transport=tls&gt;</entry>
    
        <entry name="realm" overwrite="true">sip.ficusonline.com</entry>
        <entry name="conference_factory_uri" overwrite="true">sip:[email protected]</entry>
        <entry name="audio_video_conference_factory_uri" overwrite="true">sip:[email protected]</entry>
    
        <entry name="lime_server_url" overwrite="true">https://sip.ficusonline.com/lime-server/lime-server.php</entry>
    
      <section name="nat_policy_default_values">
        <entry name="stun_server" overwrite="true">xxx.ficusonline.com:xxxx</entry>
    
     </section>   
     <section name="assistant">
       <entry name="domain" overwrite="true">sip.ficusonline.com</entry>
    
     </section>
    

    app/src/main/assets/linphonerc_factory

    [account_creator]
    backend=1
    # 1 means FlexiAPI, 0 is XMLRPC
    url=https://sip.ficusonline.com/api/
    

Firebase Admin SDK for PHP — Firebase Admin SDK for PHP Documentation

認証情報

FCM メッセージについて  |  Firebase Cloud Messaging

実装する FCM 機能によっては、Firebase プロジェクトから次の認証情報の取得が必要になる場合があります。

プロジェクト ID FCM v1 HTTP エンドポイントへのリクエストで使用される Firebase プロジェクトの一意の識別子。この値は、Firebase コンソールの [設定] ペインで確認できます。
登録トークン 各クライアント アプリ インスタンスを識別する一意のトークン文字列。単一のデバイスとデバイス グループへのメッセージングに必要になります。登録トークンは非公開にしなければならないことに注意してください。
送信者 ID Firebase プロジェクトの作成時に作成される一意の数値。Firebase コンソールの [設定] ペインにある [Cloud Messaging] タブで確認できます。送信者 ID は、クライアント アプリにメッセージを送信できる各送信者を識別するために使用されます。
アクセス トークン HTTP v1 API へのリクエストを承認する短命な OAuth 2.0 トークン。このトークンは、Firebase プロジェクトに属するサービス アカウントと関連付けられています。アクセス トークンの作成およびローテーションを行うには、送信リクエストを承認するに記されている手順に沿って操作します。
サーバーキー(レガシー プロトコル用) アプリサーバーによる Google サービスへのアクセス(以前の Firebase Cloud Messaging プロトコル経由でのメッセージの送信など)を承認するサーバーキー。サーバーキーは Firebase プロジェクトの作成時に取得します。サーバーキーを確認するには、Firebase コンソールの [設定] ペインにある [クラウド メッセージング] タブに移動します。重要: クライアント コードにはサーバーキーを含めないでください。また、アプリサーバーの承認には、必ずサーバーキーのみを使用してください。Android、Apple プラットフォーム、ブラウザのキーは、FCM で拒否されます。

メールアドレスまたは電話番号による認証画面切替えクラス:メソッド

電話番号を取得していないデバイスでは、メール認証のページへ誘導。

app/src/main/java/org/linphone/activities/assistant/fragments/WelcomeFragment.kt

class WelcomeFragment : GenericFragment<AssistantWelcomeFragmentBinding>() {
    private lateinit var viewModel: WelcomeViewModel

    override fun getLayoutId(): Int = R.layout.assistant_welcome_fragment

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding.lifecycleOwner = viewLifecycleOwner

        viewModel = ViewModelProvider(this)[WelcomeViewModel::class.java]
        binding.viewModel = viewModel

        binding.setCreateAccountClickListener {
            if (LinphoneUtils.isPushNotificationAvailable()) {
                Log.i("[Assistant] Core says push notifications are available")
                val deviceHasTelephonyFeature = coreContext.context.packageManager.hasSystemFeature(
                    PackageManager.FEATURE_TELEPHONY
                )
                if (!deviceHasTelephonyFeature) {
                    Log.i(
                        "[Assistant] Device doesn't have TELEPHONY feature, showing email based account creation"
                    )
                    navigateToEmailAccountCreation()
                } else {
                    Log.i(
                        "[Assistant] Device has TELEPHONY feature, showing phone based account creation"
                    )
                    navigateToPhoneAccountCreation()
                }
            } else {
                Log.w(
                    "[Assistant] Failed to get push notification info, showing warning instead of phone based account creation"
                )
                navigateToNoPushWarning()
            }
        }
.....
.....

Android-Studio デバイスシミュレータによるFCM送受信確認

1つのデバイスで1つのアカウントを付与することはできるが、任意の電話番号または他人の電話番号でアカウントが取得可能のため使用はNG。

YouTube : Create Account by phone number
https://youtube.com/shorts/nq6F1L5qK18?si=_qVY-F_FN5cJkLwb

YouTube : Login by phone number
https://youtube.com/shorts/_X_QQHG7kr4?si=7jXSabxvcG6XAjVL

Click Assistant Menu

Click CREATE ACCOUNT

Input Phone Number, Then Click CREATE ACCOUNT

Waiting for a few second until receiving the message

Received the message from Firebase

Pull down the top indicator, and check the confirmation code (FCM Message)

Input the confirmation code, then click FINISH CONFIGURATION

Finished the Account Registration

参考:Firebase + OTP に関するブログ検索(Medium)

以下のMediumサイトからFirebaseのOTPに関するブログを検索。

Medium

Laravelによるウェブアプリ作成の際は以下参照。


KotlinによるAndroidアプリ作成の際は以下参照。

OTP Login using Firebase

MVVM Architecture