FlutterでFirebaseを使わずにSign in with Appleを実装する

この記事は[Flutter #2 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/flutter-2)の6日目の記事です。

アドベントカレンダーの参加は今年が初めてです。
いつもは[個人ブログ](https://tamappe.com/)で記事を投稿していますので、Qiitaへの投稿は久しぶりです。

Flutterはクロスプラットフォーム開発ができる便利なツールですが、まだまだ実務で導入するには見えていない部分があり会社で導入するためにはハードルがあったりします。

例えば、どれだけOS依存の課題に対応できるか、そしてどれだけサーバー連携が絡んだ時に柔軟にカスタムできるかだったりですね。

今回はその企業でも導入する際に検討事項に入りそうな「アカウント周りの情報」にういて深堀りしてみます。

企業としては「ただユーザーがSNSアカウントでログインできたら良いや」というのは絶対ありえなく、ログインしたらユーザー情報を社内のデータベースに保存してセキュアに取り扱いたいというのが本音です。

また絶対にユーザー情報が外部に漏れても駄目ですね。
そんなアカウント周りの情報ですが、Flutterでのログイン機構だとまだまだ見えていない部分の方が多いです。

そこで、今回はAppleIDを使って認証するシステムである`Sign in with Apple`における振る舞いについて見ていきます。

# iOSエンジニアがFlutterでSign in with Apple

Flutterを導入できるプロジェクトの場合はだいたいは相性がいいFirebaseも導入してそこら辺はFirebaseが担ってくれる場面が多いのですが、プロジェクトによっては`Firebase Authentication`の機能が使えず`Firebase Authentication`で連携できない場面もあるかもしれません。

[Firebase Authentication](https://firebase.google.com/docs/auth/ios/apple?hl=ja)

その場合にはFlutterでSign in with Appleでの認証が可能かどうかやっぱり気になります。
なので、今回は`Firebase Authentication`が使えない場合を想定してみました。

今回はFlutterでのSign in with Apple認証をするために使うパッケージに`sign_in_with_apple`をチョイスしてみます。非常にpopularなパッケージになっています。

[sign_in_with_apple](https://pub.dev/packages/sign_in_with_apple)
URL: https://pub.dev/packages/sign_in_with_apple

このパッケージを使ってFlutterでSign in with Appleを実装してみます。

## 開発環境

```sh
Flutter 1.22.4 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 1aafb3a8b9 (2 weeks ago) • 2020-11-13 09:59:28 -0800
Engine • revision 2c956a31c0
Tools • Dart 2.10.4
```

- Xcode 12.1
- Android Studio 4.0
- iOS 14.3 (Sign in with Apple は実機で開発した方がスムーズのため)

## 発生したエラー

### Xcodeのバージョンが古い

実機のiOSのバージョンに対してXcodeのバージョンが足りなかった時に発生したエラー。

```
═══════════════════════════════════════════════════════════════════════════════════
Error launching app. Try launching from within Xcode via:
    open ios/Runner.xcworkspace

Your Xcode version may be too old for your iOS version.
═══════════════════════════════════════════════════════════════════════════════════
2020-11-29 18:23:16.711 ios-deploy[2686:17758351] [ !! ] Error 0xe8000022: The service is invalid. AMDeviceSecureStartService(device, serviceName, NULL, &dbgServiceConnection)
Could not run build/ios/iphoneos/Runner.app on 00008030-000508D21A83802E.
Try launching Xcode and selecting "Product > Run" to fix the problem:
  open ios/Runner.xcworkspace

Error launching application on XXXXX.

```

おそらくXcodeのバージョンを上げたらいいのかと思いバージョンを上げてみる。

Xcode 12.2 でアプリが起動してくれました🎉

## Flutter で Sign in with Apple の実装まで

それではFlutterでの本実装の解説に入ります。

### Xcode側で「Capability」の設定を行う

Xcode側でSign in with Appleの設定を活性化させておきます。

![sign_in_with_apple_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44392/1eb7236d-2849-51e4-7d64-be70a05f019b.png)

これを設定していないと、Apple認証が正しく動作しません。


### pubspec.yamlのソースコード

sign_in_with_appleをインストールするためpubspec.yamlファイルを編集します。

```yaml:pubspec.yaml
dependencies:
  flutter:
    sdk: flutter


  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.0
  sign_in_with_apple: ^2.5.4 # 追加する
```
 
これで`Pub get`してインポートします。

### ソースコードについて

今回は味気ないですが、単純に初期コードにSign in with Appleのボタンを追加するだけにします。


```dart:main.dart
import 'package:flutter/material.dart';
import 'package:sign_in_with_apple/sign_in_with_apple.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
        SignInWithAppleButton(
          onPressed: () async {
            final credential = await SignInWithApple.getAppleIDCredential(
              scopes: [
                AppleIDAuthorizationScopes.email,
                AppleIDAuthorizationScopes.fullName,
              ],
            );

            print(credential);
          },
        )
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), 
    );
  }
}

```

Sampleにある通りにSign in with Appleのウィジェットは`SignInWithAppleButton`だそうです。
これを使って実装しました。

これでソースコードをビルドすると次のような画面が表示されます。

![IMG_0467.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44392/ec606641-a100-9ec9-8695-30447b4e8717.png)

個人的にボタンのデザインをカスタムにできたら嬉しいなと思っています。ま、多分カスタマイズできると思っています。黒色の「Sign in with Apple」をタップするとApple認証のやつが下から表示されます。

Apple認証はAppleのサーバーにリクエストしてレスポンスとしてUser情報を受け取りますので、非同期処理のために`async/await`で対応します。

`getAppleIDCredential`を叩いた時に`scopes`引数があるのはUserのAppleIdに紐付いている

- 姓名
- メールアドレス

は任意でリクエストを送らないと取得できないようになっているからです。
ですので、名前とメールアドレスの取得が必要であればこの`scopes`に`AppleIDAuthorizationScopes.email`と`AppleIDAuthorizationScopes.fullName`をセットしないといけません。

### 受け取れるユーザー情報

で、ここから本題になりますが、この受け取ったUser情報がどれくらいネイティブアプリと比べて取得できるのかを調べます。

今回のソースコードでは、

```dart
print(credential);
```

この部分の調査になります。まず、credentialは`getAppleIDCredential`を叩いたときに返ってくるものです。このメソッドをググると、

```dart
  static Future<AuthorizationCredentialAppleID> getAppleIDCredential({
    @required List<AppleIDAuthorizationScopes> scopes,

    /// Optional parameters for web-based authentication flows on non-Apple platforms
    ///
    /// This parameter is required on Android.
    WebAuthenticationOptions webAuthenticationOptions,

    /// Optional string which, if set, will be be embedded in the resulting `identityToken` field on the [AuthorizationCredentialAppleID].
    ///
    /// This can be used to mitigate replay attacks by using a unique argument per sign-in attempt.
    ///
    /// Can be `null`, in which case no nonce will be passed to the request.
    String nonce,

    /// Data that’s returned to you unmodified in the corresponding [AuthorizationCredentialAppleID.state] after a successful authentication.
    ///
    /// Can be `null`, in which case no state will be passed to the request.
    String state,
  }) async {
```

というふうに`Future<AuthorizationCredentialAppleID>`が返ります。`AuthorizationCredentialAppleID`はSign in with Appleを実装したiOSエンジニアならご存知ですがこれがApple認証が成功した時に受け取れるUser情報になります。

以下が`credential`の情報になります。

![sign_in_with_apple.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44392/41f1303e-8442-fdab-7ce1-b2ccb3b0c72e.png)

| プロパティ | 役割 |
|:----------:|:------------|
| userIdentifier       | 一番重要なApple認証後のUser情報        |
| givenName     | 名      |
| familyName       | 性        |
| email         | メールアドレス          |
| authorizationCode       | 実はよくわかりません       |
| identityToken    |  JSON Web Token (JWTです、後述します) |
| state    | 状態 (よくわかりません)     |

```dart
print(credential.userIdentifier);
print(credential.givenName);
print(credential.familyName);
print(credential.email);
print(credential.authorizationCode);
print(credential.identityToken);
print(credential.state);
```


ちなみに2回目以降のApple認証で取得できるUser情報はこちらになります。

![スクリーンショット 2020-11-29 21.51.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44392/0429ce62-8357-2687-5f20-5b1ec64e6acb.png)

givenName、familyName、emailが2回目以降取得できないのが再現されています。(それはそう。)
これら3つの情報を何度も取得したい場合は端末のApple認証ステータスをログアウトする必要があります。

「設定アプリ」から「パスワードとセキュリティ」「Apple IDを使用中のApp」の項目へ進んで「Apple IDの使用を停止する」を選択すればAppleIDの使用が停止され再度上記3つのデータを取得できるようになっているはずです。

### identityToken の説明

そして、ここからはいつもの`Sign in with Apple`の使い方ですが、`identityToken`というのはJWTというもので、これは暗号化された文字列になっています。

この情報を解析するためには、

https://jwt.io/

へアクセスして、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44392/03fdfc35-7525-67a3-6612-4217ffff7bf3.png)

の「Encoded」の部分に`identityToken`をそのままコピーペーストすればデコードされた情報が確認できます。
そして、デコードされた情報の中に`credential.userIdentifier`と同じ情報が含まれています。

そのため、例えば、独自のAPIリクエストを使って社内のデータベースと認証して同じユーザーかどうかを確認する場合はこの`userIdentifier`を使えば良さそうです。

そんな感じで[Swift/iOSでアレだけ面倒だった](https://developer.apple.com/documentation/authenticationservices/implementing_user_authentication_with_sign_in_with_apple)Sign in with Appleがなんと

```dart
SignInWithAppleButton(
          onPressed: () async {
            final credential = await SignInWithApple.getAppleIDCredential(
              scopes: [
                AppleIDAuthorizationScopes.email,
                AppleIDAuthorizationScopes.fullName,
              ],
            );

            print(credential);

          },
        )
```

とこれだけでSign in with Appleの実装ができるのですね。

ここの部分をSwiftで書くとしたら下のようになります。

```swift
@objc
func handleAuthorizationAppleIDButtonPress() {
    let appleIDProvider = ASAuthorizationAppleIDProvider()
    let request = appleIDProvider.createRequest()
    request.requestedScopes = [.fullName, .email]
    
    let authorizationController = ASAuthorizationController(authorizationRequests: [request])
    authorizationController.delegate = self
    authorizationController.presentationContextProvider = self
    authorizationController.performRequests()
}
```

しかも動作や受け取れるユーザー情報もネイティブのときと同じです。

ただ、ちょっと気になるのがXcode 12.2じゃないとビルドできなかった点ぐらいでしょうか。
iOSが最新バージョンだった影響もあるかもしれません。

(なので、既存のプロジェクトでSign in with Appleを導入する場合は、OSのバージョンに注意したほうが良いかもしれません。)

### ネイティブ実装

それではおまけ程度にネイティブでSign in with Appleを実装する方法を解説します。
とはいうもののそんなたいそうな話ではなく既にAppleがサンプルのアプリを用意してくれています。

[Implementing User Authentication with Sign in with Apple](https://developer.apple.com/documentation/authenticationservices/implementing_user_authentication_with_sign_in_with_apple)

ここの「Download」からサンプルプロジェクトをダウンロードできます。
それを見て実装方法を調査すればできます。

ネイティブの場合はリクエストを送信するとDelegateでコールバックでUser情報が返ってきます。
細かいハマリポイントは会社のテックブログでまとめましたので良かったらこちらのページから確認してください。

[iOS 版レアジョブアプリが Sign in with Apple に対応した話](https://rarejob-tech-dept.hatenablog.com/entry/2020/08/21/190000)

こちらの記事では、本記事で取り上げなかった

- メールアドレスの取り扱い(メールを非公開、にした場合に得られるApple側のアドレスの内容)
- メール送信機能がある場合の対応方法
- iOS 13未満のOSに対する取り扱い

について解説しています。

ということで僕からは以上になります。