Sign in with Apple with Firebase Authentication

> ※Sign in with Appleはまだbeta版の為、仕様等に変更が入る可能性があります

WWDC 2019でSign in with Appleの機能が発表されました。
この機能はApple IDを利用して様々なアプリやサービスにログインが可能となる物です。
但し、ログインが必要なアプリやサービスはアプリ単体で完結する物は少なく、基本的にはサーバー上のデータと連携する必要があるかと思います。
ここではFirebase Authenticationを利用してアプリとサーバーのユーザー情報を連携させてみようと思います。

## 事前準備

Apple Developerページの`Identifiers`で`Sign in with Apple`を有効にします。

<img width="960" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2846/58e0e855-5684-18e9-64b2-28da6721234d.png">

Xcode上でもプロジェクトの`Signing & Capabilities` に `Sign in with Apple`を追加します。

## ログインボタンの設置

基本的にはOSが用意したボタンを用意するのが良いかと思いますが、カスタマイズしたい場合は[HIG](https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/)を確認してガイドラインに沿うか確認した方が良いでしょう。

```swift
import AuthenticationServices

let appleIdButton = ASAuthorizationAppleIDButton(authorizationButtonType: .signUp, authorizationButtonStyle: .whiteOutline)
appleIdButton.addTarget(self, action: #selector(handleTap(_:)), for: .touchUpInside)
view.addSubview(appleIdButton)

```

## ログインボタンのイベントハンドリング

```swift
@objc
func handleTap(_ sender: ASAuthorizationAppleIDButton) {
    let request = ASAuthorizationAppleIDProvider().createRequest()
    request.requestedScopes = [.fullName, .email]
    let controller = ASAuthorizationController(authorizationRequests: [request])
    controller.delegate = self
    controller.presentationContextProvider = self
    controller.performRequests()
}
```

## ログインのdelegateを実装

```swift
extension ViewController: ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
    func authorizationController(controller _: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        switch authorization.credential {
        case let appleIdCredential as ASAuthorizationAppleIDCredential:
            // TODO: use appleIdCredential.user, email: appleIdCredential.email
        default:
            break
        }
    }

    func authorizationController(controller _: ASAuthorizationController, didCompleteWithError error: Error) {
        DispatchQueue.main.async { [weak self] in
           // TODO: Error handling
        }
    }

    func presentationAnchor(for _: ASAuthorizationController) -> ASPresentationAnchor {
        return view.window!
    }
}

```

# Firebase

環境はNode 8系を利用します。
Firebase toolsがインストールされている前提です。

## Firebase Cloud functionsの作成

`firebase init functions` でCloud functionsを初期化します。
JavaScriptかTypeScriptを聞かれるのでモダンなTyprScriptを選択しておきましょう。
`functions/src/index.ts` などが生成されているかと思いますので下記の様に修正します。

```typescript
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';

admin.initializeApp();

type AuthRequest = {
    email: string | null,
    userIdentifier: string
};

function isAuthRequest(data: any): data is AuthRequest {
    return typeof data.userIdentifier === 'string';
}

export const signin_with_apple = functions.https.onCall(async (data, context)  => {
    if (!isAuthRequest(data)) {
        throw new Error('invalid request');
    }
    // この書き方なんとかしたい
    // 追記: 良い書き方をコメントで教えてもらったので気になる方はコメントを参照ください
    const request = data as any as AuthRequest;
    const uid = request.userIdentifier;
    const email = request.email;

    const customToken = await admin.auth().createCustomToken(uid);
    if (!email) {
        return {
            custom_token: customToken
        };
    }
    await admin.auth().updateUser(uid, {
        email: email
    });
    return {
        custom_token: customToken
    };
});

```

開発が完了したら `firebase deploy --only functions:signin_with_apple` などでデプロイをしておきます。

## アプリからCloud functionsの呼び出し

```swift
import FirebaseFunctions

Functions.functions().httpsCallable("signin_with_apple").call(["email": appleIdCredential.email, "userIdentifier": appleIdCredential.user]) { result, error in
    if let error = error {
        // TODO: Error handling
        return
    }

    if let customToken = (result?.data as? [String: Any])?["custom_token"] as? String {
        // Success!
        return
    } else {
        // TODO: Error handling
    }
}

```

上記で受け取った`customToken`を`FirebaseAuth`に渡します。

## レスポンスを連携

```swift
import FirebaseAuth

Auth.auth().signIn(withCustomToken: customToken) { [weak self] _, error in
    if error != nil {
        // TODO: Error handling
        return
    }
    // Success!
}

```

# Apple IDのバリデーション

Apple IDは設定アプリやサイトから簡単に無効化出来るので、必要に応じてApple IDのハンドリングしましょう。

```swift
let provider = ASAuthorizationAppleIDProvider()
provider.getCredentialState(forUserID: appleId) { credentialState, _ in
    DispatchQueue.main.async {
        switch credentialState {
        case .authorized:
            // nothing todo
            break
        case .notFound, .revoked:
            // TODO: error handling
            break
        @unknown default:
            fatalError("unknown credentialState")
        }
    }
}
```

# まとめ

Apple IDでのログイン機能をFirebase Authと連携する事が出来ました。
Appleとしては既存アプリが外部ログイン機能を提供している場合、Apple IDでのログイン機能が必要だと言っているので、なるべく早く対応しておきたいですね。