FlutterとCloud Functions、Firebase Authを使ってSign In with Appleを実装してみた

どうやら Sign in with Appleはサードパーティログインを実装している場合は必須で実装しないといけないみたいですね。

https://qiita.com/_asa08_/items/4f6e383fc6c9d38046ed

自分のアプリではログインの処理はFlutterとFirebaseで完結させたかったので実装してみました。


完成系はこんな感じです。(あくまで参考に)

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Sign In with Appleはこんな感じ。<br>ユーザー体験が最高だから基本は導入したいけど、別プラットフォームで同じアカウントでログインしたい時どうしたらいいんだろう🤔 <a href="https://t.co/cZXTQbOrGx">pic.twitter.com/cZXTQbOrGx</a></p>&mdash; shogo.yamada (@yshogo87) <a href="https://twitter.com/yshogo87/status/1178284800995512326?ref_src=twsrc%5Etfw">September 29, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## まずは準備

まずはXcodeの設定をしないといけないので、こちらの素晴らしい記事を参考に実装してください。

https://qiita.com/_asa08_/items/4f6e383fc6c9d38046ed

この記事では割愛します。


## Flutter側の準備

次にFlutter側に Sign In with Appleのプラグインがあったので、こちらを導入します。

https://pub.dev/packages/apple_sign_in

pubspec.ymlに下記を追加(バージョンについては最新のものをお使いください。)

```yml
apple_sign_in: ^0.0.3
```


## ボタンを出す


それではログインボタンを実装します。
実装方法は下記


```dart

import 'dart:io';

import 'package:apple_sign_in/apple_sign_in.dart';

class MyPage extends StatefulWidget {
  @override
  _MyPageState createState() => _MyPageState();
}

class _MyPageState extends State<MyPage> with RouteAware {
  final Future<bool> _isAvailableFuture = AppleSignIn.isAvailable();

  @override
  void initState() {
    super.initState();
    AppleSignIn.onCredentialRevoked.listen((_) {
      print("Credentials revoked");
    });
  }

  @override
  Widget build(BuildContext context) {
    return _login();
  }

  Widget _login() {
    return Scaffold(
      backgroundColor: Colors.black,
      appBar: AppBar(),
      body: Container(
        width: MediaQuery.of(context).size.width,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Platform.isIOS
                ? FutureBuilder(
                    future: _isAvailableFuture,
                    builder: (_, isAvailableSnapshot) {
                      if (!isAvailableSnapshot.hasData) return SizedBox();

                      return Container(
                        margin: const EdgeInsets.only(
                          left: 50,
                          right: 50,
                        ),
                        child: isAvailableSnapshot.data
                            ? AppleSignInButton(
                                onPressed: () {
                                   // ログイン処理を実装する
                                }
                            : SizedBox(),
                      );
                    },
                  )
                : const Text("This Platform is the iOS")
          ],
        ),
      ),
    );
  }
}

```

これでボタンが表示されます。下がボタンが表示された例です。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">お、FlutterでSign in with Apple実装できそうだ😳 <a href="https://t.co/Wj06Dmc8h7">pic.twitter.com/Wj06Dmc8h7</a></p>&mdash; shogo.yamada (@yshogo87) <a href="https://twitter.com/yshogo87/status/1177573116131737600?ref_src=twsrc%5Etfw">September 27, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


もしかしたらこのレイアウトだと、Appleの審査に弾かれてしまう可能性があるのでガイドラインを読んで適宜修正してください。
https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/



## リクエスト処理

次にリクエスト処理を実装します。

```dart
  Future doAppleLogin() async {
    final AuthorizationResult result = await AppleSignIn.performRequests([
      AppleIdRequest(requestedScopes: [Scope.email, Scope.fullName])
    ]);

    switch (result.status) {
      case AuthorizationStatus.authorized:
        print("success");
        print(result.credential.user);
        // ログイン成功

        break;

      case AuthorizationStatus.error:
        print("Sign in failed: ${result.error.localizedDescription}");

        throw Exception(result.error.localizedDescription);
        break;

      case AuthorizationStatus.cancelled:
        print('User cancelled');
        break;
    }
  }

```


## Cloud Functionsの実装


次にカスタム認証するためのトークンの取得をCloud Functionsで行います。
こちらのコードは下記の記事を参考に実装しました。(下の記事のコードの方がとても綺麗で安全なコードですので、そっちを参考にしたほうがいいと思います)

https://qiita.com/fromkk/items/573ed96bc6a367bbf167

```javascript
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
admin.initializeApp();

export const onSignInWithApple = functions.https.onCall(
  async (data, context) => {
    const uid = data.userIdentifier;
    const email = data.email;
    const customToken = await admin.auth().createCustomToken("Apple" + uid);
    if (!email) {
      return {
        custom_token: customToken
      };
    }
    await admin.auth().updateUser(uid, {
      email: email
    });
    return {
      custom_token: customToken
    };
  }
);

```

書いたらデプロイします。



## Flutter側から呼び出し

次にこのエンドポイントをFlutterから呼び出します。
エンドポイントですので、普通にAPIを叩くようにしてもいいのですが、Cloud Functionsを呼び出すプラグインがあるので、せっかくなのでこちらを使いましょう

https://pub.dev/packages/cloud_functions

pubspec.ymlに下記を追加(バージョンについては最新のものをお使いください)

```yml
cloud_functions: ^0.4.1+1
```


ログインが成功したらCloud Functionsを呼び出します。


```dart
  Future doAppleLogin() async {
    final AuthorizationResult result = await AppleSignIn.performRequests([
      AppleIdRequest(requestedScopes: [Scope.email, Scope.fullName])
    ]);

    switch (result.status) {
      case AuthorizationStatus.authorized:
        print("success");
        print(result.credential.user);
        CloudFunctions functions = CloudFunctions.instance;
        HttpsCallableResult httpCallResult = await functions
            .getHttpsCallable(functionName: "onSignInWithApple")
            .call({
          "email": result.credential.email,
          "userIdentifier": result.credential.user,
        });

        FirebaseUser user = await _signInToFirebaseFromApple(
            httpCallResult.data["custom_token"]);

        // ログイン後の処理

        break;

      case AuthorizationStatus.error:
        print("Sign in failed: ${result.error.localizedDescription}");

        throw Exception(result.error.localizedDescription);
        break;

      case AuthorizationStatus.cancelled:
        print('User cancelled');
        break;
    }
  }

  Future<FirebaseUser> _signInToFirebaseFromApple(String token) async {
    FirebaseAuth auth = FirebaseAuth.instance;
    final result = await auth.signInWithCustomToken(token: token);
    return result.user;
  }


```


これで実装完了です。

#### 追記

もしここでCLoud Functionsを呼び出した時にエラーが発生した場合はIAM関連で必要な権限が設定されていない可能性があります。
下記のツイートで解決しますので、詳しくはこちらをどうぞ

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Google App Engine Node.jsでGoogle Cloud StorageのSigned URLを取得する by <a href="https://twitter.com/ogawa0071?ref_src=twsrc%5Etfw">@ogawa0071</a> <a href="https://t.co/EGPkuqMVsb">https://t.co/EGPkuqMVsb</a><br><br>この方法で解決しました！<br>こんな権限いじらないといけないんか、、、<br>めっちゃハマった、、、 <a href="https://t.co/eOopZtOvpZ">https://t.co/eOopZtOvpZ</a></p>&mdash; shogo.yamada (@yshogo87) <a href="https://twitter.com/yshogo87/status/1178180342642921472?ref_src=twsrc%5Etfw">September 29, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


## 今不明なこと


<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Firebase にカスタム認証したときにSign In with Appleでメールアドレスを公開しない設定にしてるときにサインアップするとFirebaseのコンソールでの表示がこういう風になってしまうんだけど、本当にこれでいいのかな。 <a href="https://t.co/uc9m5A96xx">pic.twitter.com/uc9m5A96xx</a></p>&mdash; shogo.yamada (@yshogo87) <a href="https://twitter.com/yshogo87/status/1178295603475828737?ref_src=twsrc%5Etfw">September 29, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

このツイートについてもし知ってる方いらっしゃれば、コメント欄にてご連絡ください。