【Firebase】SMS認証でアカウント乗っ取りを防げ！

※この記事は、筑波大学情報科学類とのコラボプロジェクト筑波ミライラボ Advent Calendar 2023の12月14日の記事です。

こんにちは、1年の青柳世成です。
今回はFlutterを使ってSMS認証を作っていきたいと思います。
*今回はAndroid版しか作りません。

# この記事のターゲット
・Flutterはある程度分かるけどFirebaseについての知識がない！
・Firebaseを何から始めればいいのかわからない🧐
という方向けにちょいムズイレベルのSMS認証から入っていただければと思い作りました！

# SMS認証とは
SMS認証とは、スマホやガラケーなどの携帯電話宛に送信されたSMSに記載のある確認コードを入力してログインする仕組みです。

SMS認証を導入することで、セキュリティ対策を大幅に強化できます。基本的に本人以外がデバイスを所持していることは少ないため、悪意のある不正アクセスを防ぎやすい傾向にあります。
また、SMS認証は電話番号にメッセージを送信する方法であるため、第三者にパスワードが漏れるといった心配もありません。
端末取られたらどうしようもなくね？って思ってる人は🤫

# 用意するもの
[Android Studio](https://developer.android.com/studio?hl=ja), [FlutterSDK](https://docs.flutter.dev/get-started/install), [Googleアカウント](https://www.google.com/intl/ja/account/about/), [Firebaseプロジェクト](https://firebase.google.com/?hl=ja)
(多分これで全部だと思うけど多すぎて忘れてるかも...)

# Firebaseプロジェクトの作成
リンクを開いたら「コンソールへ移動」ボタンを押してください。
![コンソールへ移動](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3510989/f580527d-703d-70f8-d8e2-fd6bc1f995aa.png)
[このような画面](https://console.firebase.google.com/?hl=ja)に移るはずです。
「プロジェクトを追加」を押して、手順に沿ってプロジェクトを作成してください。
![Firebase コンソール](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3510989/ad3ae777-4fe5-92e9-887c-f322bee80559.png)
Flutterを選択してください。
![Flutter](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3510989/d508c071-b50b-e6ce-21c5-4e313017ccf1.png)
必要なものを入れていきます。
次のセクションへGO!!
※Flutterを押したらその画面のままタブを置いといてください、この後使います。

# 前準備
Node.jsがインストールされている前提で話を進めていきます。
まだインストールしていない方は[こちら](https://nodejs.org/en)から道なりにインストールしてください。

## Firebase CLI
Terminalを開き、次のコマンドを実行してください。
```shell
npm install -g firebase-tools
```
このコマンドはFirebase CLIをインストールするコマンドです。
Googleアカウントにログインする必要があるので、次のコマンドでログインします。
```shell
firebase login
```
ログインできたら、Firebaseプロジェクトを一覧表示し、CLIが正しくインストールされていてアカウントにアクセスしていることをテストします。
```shell
firebase projects:list
```
先ほど作成したFirebaseプロジェクトが表示されていたらOKです！

# FlutterFire
※この手順の前にAndroid Studioでいつも通りプロジェクトを作ってください。
Android Studioプロジェクトからターミナルを開き、以下のコマンドを入力してください。
```shell
dart pub global activate flutterfire_cli
```
実行後
```shell
Activated flutterfire_cli 0.2.7.
```
と表示されたら以下のボタンを押して二個目のコマンドをコピーします。
![FlutterFire CLI](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3510989/272c76f3-3207-9930-40a0-dfe368dcd235.png)
コピーしたコマンドをターミナルに貼り付けます。
```
Which platforms should your configuration support (use arrow keys & space to select)?
```
という表示が出たら、どのプラットフォームで作りたいか？ということなので、Androidだけを選びます。

Android以外のチェックを外します。矢印キーで上下、スペースキーでチェックを切り替えることができます。
Androidのみにしたら、Enterキーで確定します。

次に
```shell
The files android/build.gradle & android/app/build.gradle will be updated to apply Firebase configuration and gradle build plugins. Do you want to continue?
```
と表示されるので、「y」キーを押してEnterで次に進んでください。
```shell
Learn more about using this file and next steps from the documentation:
 > https://firebase.google.com/docs/flutter/setup
```
と表示されたら完了です！

## Android用のデバッグキー
Android Studioでいつも通りプロジェクトを作成し、プロジェクトを開いてターミナルで下記のコマンドを実行してください。
```shell
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
```
※このコマンドがJavaのエラーで使えない方は以下の記事を参考にしてください。
https://zenn.dev/satokazur222/articles/66568417b291d8

実行すると、SHA1キーが生成されます。
出力の以下の部分をコピーします。
```shell
Certificate fingerprints:
         SHA1: ここ！
```
コピーできたら、Firebaseプロジェクトの設定から以下の部分にキーを追加します。
![SHA 証明書フィンガープリント](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3510989/d1373bf8-1843-09a6-4265-905fb3dd97bd.png)

これで、準備が完了しました。
依存関係のインストールをしていきます！

## 依存関係のインストール
Android Studioでプロジェクトを作成し、以下のコマンドをターミナルに打ち、依存関係をインストールします。
```shell
- flutter pub add firebase_core
- flutter pub add firebase_auth
```

## Authencation設定
Firebaseプロジェクト内メニューからAuthencationを探し、「始める」を押すことで認証方法の選択画面に飛びますので、電話番号を選択してください。

全ての準備が完了しました！
やっとコード書ける...

# 実装
### main.dart
```dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(MyApp());
}
```
このコード全体の流れは、Flutterフレームワークを初期化し、Firebaseを初期化し、最終的にアプリケーションを開始するというものです。

>``void main() async {``

エントリーポイントであるmain関数が定義されています。この関数はプログラムの開始点です。

>``WidgetsFlutterBinding.ensureInitialized();``

ensureInitializedは、Flutterフレームワークが初期化されたことを確認します。

>``await Firebase.initializeApp();``

Firebase.initializeApp()は、Firebaseの初期化を行います。この呼び出しは非同期処理であり、awaitが使用されています。Firebaseを使用する前に、Firebaseを初期化する必要があります。

>``runApp(MyApp());``

runApp関数は、Flutterアプリを開始します。引数にはMyApp()というウィジェットが渡されています。MyAppはアプリケーションのメインウィジェットです。

※ここから下は全て
```dart
final FirebaseAuth _auth = FirebaseAuth.instance;
```
を定義してください。

_authはFirebase Authenticationを使用するためのFirebaseAuthのインスタンスです。これは、Firebaseアプリ全体で共有されるため、finalキーワードで宣言しています。
### アカウント登録
```dart
Future<User?> signUpWithPhoneNumber(String phoneNumber) async {
    try {
      await _auth.verifyPhoneNumber(
        phoneNumber: phoneNumber,
        verificationCompleted: (PhoneAuthCredential credential) async {
          // 認証が完了した場合
          UserCredential authResult = await _auth.signInWithCredential(credential);
          return authResult.user;
        },
        verificationFailed: (FirebaseAuthException e) {
          // 認証が失敗した場合
          print("認証失敗: ${e.message}");
        },
        codeSent: (String verificationId, int? resendToken) {
          // SMSコードが送信された場合
          // ここで認証コードの入力UIを表示するなど...
          print("コード送信済み。認証ID: $verificationId");
        },
        codeAutoRetrievalTimeout: (String verificationId) {
          // 認証がタイムアウトした場合
          print("コード自動取得のタイムアウト。認証ID: $verificationId");
        },
      );
    } catch (e) {
      print("電話番号認証中のエラー: $e");
      return null;
    }
  }
```
> ``Future<User?> signUpWithPhoneNumber(String phoneNumber) async {``

signUpWithPhoneNumber関数は、電話番号を使用したユーザー登録を行います。戻り値の型はFuture<User?>であり、非同期関数であることを示すasyncが付いています。

> ``try {``

関数の実際の処理が始まります。エラーが発生する可能性がある処理をtry-catchで囲みます。

> ``await _auth.verifyPhoneNumber(``

_authのverifyPhoneNumberを呼び出して、電話番号を認証します。これにより、ユーザーにSMSコードが送信され、電話番号の確認が行われます。

> ``verificationCompleted: (PhoneAuthCredential credential) async {``

認証が完了した場合の関数。認証が完了すると、渡されたcredentialを使用してユーザーをサインインします。

> ``verificationFailed: (FirebaseAuthException e) {``

認証が失敗した場合の関数。エラーメッセージがコンソールに表示されます。

> ``codeSent: (String verificationId, int? resendToken) {``

SMSコードが送信された場合の関数。認証コードの送信が成功したことをログに表示します。

> ``codeAutoRetrievalTimeout: (String verificationId) {``

認証が自動的にタイムアウトした場合の関数。タイムアウトが発生したことをログに表示します。

> ``} catch (e) {``

try内で発生した例外をキャッチし、エラーメッセージをログに表示します。

> ``return null;``

電話番号認証中にエラーが発生した場合、nullを返します。

### ログイン
```dart
Future<User?> signInWithPhoneNumber(String verificationId, String smsCode) async {
    try {
      PhoneAuthCredential credential = PhoneAuthProvider.credential(
        verificationId: verificationId,
        smsCode: smsCode,
      );
      UserCredential authResult = await _auth.signInWithCredential(credential);
      return authResult.user;
    } catch (e) {
      print("電話番号サインイン中のエラー: $e");
      return null;
    }
  }
```
> ``Future<User?> signInWithPhoneNumber(String verificationId, String smsCode) async {``

signInWithPhoneNumber関数は、Firebase Authenticationを使用して電話番号でのサインインを行います。引数には認証ID(verificationId)とSMSコード(smsCode)が渡されます。戻り値の型はFuture<User?>であり、非同期関数であることを示すasyncが付いています。

> ``try {``

関数の処理が始まります。エラーが発生する可能性がある処理をtry-catchで囲みます。

> ``PhoneAuthCredential credential = PhoneAuthProvider.credential(``

PhoneAuthProvider.credentialメソッドを使用して、電話番号認証のための情報を生成します。これにはverificationId（認証ID）とsmsCode（SMSコード）が必要です。

> ``UserCredential authResult = await _auth.signInWithCredential(credential);``

生成された情報を使用して、_authを介してサインインを試行します。サインインが成功すると、UserCredentialオブジェクトが得られます。

> ``return authResult.user;``

サインインが成功した場合、UserCredentialからUserを取得し、それを戻り値として返します。

> ``} catch (e) {``

try内で発生した例外をキャッチし、エラーメッセージをログに表示します。

> ``print("電話番号サインイン中のエラー: $e");``

キャッチしたエラーをログに表示します。

> ``return null;``

サインイン中にエラーが発生した場合、nullを返します。

### ログアウト
```dart
Future<void> signOut() async {
    await _auth.signOut();
  }
```
> ``Future<void> signOut() async {``

signOut関数は、非同期関数であり、引数も戻り値もないことを示すFuture<void>型です。サインアウト処理を行います。

> ``await _auth.signOut();``

_authのsignOutを呼び出して、現在のユーザーをサインアウトします。この処理は非同期であるため、awaitを使用して待機します。

# あとがき
今回の記事では、Flutterを使用してSMS認証を実装する手順を紹介しました。Firebaseを利用して、セキュリティを強化した認証機能を実装することができます。

初めてFirebaseを使用する方や、SMS認証に興味がある方にとって、ちょうどいい難易度の内容だったでしょうか。

また、記事内で使用したリンクやコマンドがうまく動作しない場合は、最新の情報を確認してください。技術は常に進化していますので、変更があるかもしれません。

FlutterやFirebaseを使った開発は、ユーザーエクスペリエンス向上に貢献する強力なツールです。今後もさまざまな機能やアプリケーションを開発していく中で、新たな発見や工夫をしていただければ嬉しいです。

お疲れ様でした！どうぞ楽しいFlutter開発をお過ごしください。