【iOS】AppleサインインからFirebaseアカウントを作成する

#始めに
本ドキュメントは次の2点を目標とします。

- iOSアプリにAppleサインインを実装する
- Appleサインイン情報からFirebaseアカウントを作成する

執筆時の開発環境は次の通りです。

- Xcode 12.4
- Swift 5

Xcodeプロジェクトとそれに対応するFirebaseプロジェクトが作成済みであり、[iOS側のFirebase初期導入手順](https://firebase.google.com/docs/ios/setup?hl=ja)は実装済みである事が前提です。
また次のライブラリが必要です。

- FirebaseAuth

https://github.com/firebase/firebase-ios-sdk/blob/master/SwiftPackageManager.md

基本的には[Firebaseドキュメント](https://firebase.google.com/docs/auth/ios/apple?authuser=2)通りの内容ですが、次の2点が異なります。

- 必要最小限の情報のみ記載
- ビルドエラーを解決したコード

#実装
##Firebaseの設定
Authentication画面を表示します。
Sign-in methodタブを選択し、Appleを有効化して保存します。
その他の項目を編集する必要はありません。
![Firebase Authentication](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4077/e3e85b1c-6512-41d7-cacd-d116d0222e3d.png)

##iOSアプリの実装
次の流れを実装します。

1. Appleサインインし、Apple認証情報を取得する
1. 取得したApple認証情報をFirebaseに渡し、Firebaseアカウントを作成する

###Capabilities設定
XcodeのSetting & Capabilities画面を表示し、「+Capability」をクリックします。
表示された一覧の中からSign in with Appleを追加します。
![Capabilities設定](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4077/97465dbe-d274-2184-f4cd-417d3414a3d1.png)


###AccountManagerクラスの実装
新しくAccountManagerという名前のシングルトンクラスを実装します。
このクラスにサインインに必要な処理を実装していきます。

```AccountManager.swift
class AccountManager: NSObject {
    
    static var shared = AccountManager()
    
    private override init() {
        super.init()
    }
}
```

####Appleサインイン機能
#####ナンス生成関数
Appleサインインする際、得られたトークンの正当性を検証する為「ナンス」と呼ばれる文字列を使用します。

```AccountManager.swift
private extension AccountManager {
    func randomNonceString(length: Int = 32) -> String {
        precondition(length > 0)
        
        let charset: Array<Character> = Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._")
        var result = ""
        var remainingLength = length
        
        while remainingLength > 0 {
            var randoms = [UInt8]()
            for _ in 0 ..< 16 {
                var random: UInt8 = 0
                let errorCode = SecRandomCopyBytes(kSecRandomDefault, 1, &random)
                if errorCode != errSecSuccess {
                    fatalError("Unable to generate nonce. SecRandomCopyBytes failed with OSStatus \(errorCode)")
                }
                randoms.append(random)
            }
            
            randoms.forEach { random in
                if remainingLength == 0 { return }
                
                if random < charset.count {
                    result.append(charset[Int(random)])
                    remainingLength -= 1
                }
            }
        }
        
        return result
    }
}
```

#####文字列のSHA-256暗号化関数
ナンスはSHA-256という規格で暗号化し、Appleに渡します。
String型を拡張し、文字列の暗号化関数を実装します。

```String+Extension.swift
import CryptoKit

extension String {
    
    func sha256() -> String {
        let data = Data(self.utf8)
        let hashedData = SHA256.hash(data: data)
        let result = hashedData.compactMap {
            String(format: "%02x", $0)
        }.joined()
        
        return result
    }
}
```

#####Appleサインイン関数
実装するクラスに次のプロパティを追加します。

```AccountManager.swift
class AccountManager: NSObject {
    // 生成したナンスを保持し、検証に使用します
    var currentNonce = ""
}
```

Appleサインインを行う関数を実装します。

```AccountManager.swift
import AuthenticationServices

class AccountManager: NSObject {
    func requestSignInAppleFlow() {
        currentNonce = randomNonceString()
        
        let request = ASAuthorizationAppleIDProvider().createRequest()
        request.requestedScopes = [.fullName, .email]
        request.nonce = currentNonce.sha256()
        
        let controller = ASAuthorizationController(authorizationRequests: [request])
        controller.delegate = self
        controller.presentationContextProvider = self
        controller.performRequests()
    }
}
```

request.nonceにSHA-256で暗号化したナンスを設定しています。

######ASAuthorizationControllerPresentationContextProviding
Apple認証画面を表示するwindowを指定します。
ここでは最前面のwindowを返しています。

```AccountManager.swift
extension AccountManager: ASAuthorizationControllerPresentationContextProviding {
    func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
        return UIApplication.shared.windows.first { $0.isKeyWindow }!
    }
}
```

#####Firebaseサインイン処理
Appleサインインの結果はASAuthorizationControllerDelegateを通して通知されます。
Appleサインインが正しく終了した場合、その情報を使ってFirebaseのアカウントを作成します。

```AccountManager.swift
import Firebase

extension AccountManager: ASAuthorizationControllerDelegate {
    
    func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        debugPrint("Appleサインイン完了")
        
        if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
            guard let appleIDToken = appleIDCredential.identityToken else {
                debugPrint("有効なトークンが得られなかった為、処理を中断")
                return
            }
            
            guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
                debugPrint("トークンデータの文字列化に失敗: \(appleIDToken.debugDescription)")
                return
            }
            
            if currentNonce.count == 0 { fatalError("不正なNonce") }
            let credential = OAuthProvider.credential(withProviderID: "apple.com", idToken: idTokenString, rawNonce: currentNonce)
            
            Auth.auth().signIn(with: credential) { [unowned self] (result, error) in
                if let error = error {
                    debugPrint("Firebaseサインインに失敗: \(error.localizedDescription)")
                } else {
                    debugPrint("Firebaseサインイン完了")
                }
            }
        }
    }
    
    func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
        debugPrint("Appleサインイン失敗: \(error.localizedDescription)")
    }
}
```

`OAuthProvider.credential()`に保持しているナンスを渡し、認証情報を得ます。
`Auth.auth().signIn()`はFirebaseへのサインイン処理です。ここでAppleの認証情報を渡しています。

###サインイン画面の実装
ViewControllerにAppleサインインボタンを配置し、タップ時の処理を実装します。

```ViewController.swift
import UIKit
import AuthenticationServices

class ViewController: UIViewController {
    private let loginButton: ASAuthorizationAppleIDButton = {
        let button = ASAuthorizationAppleIDButton(authorizationButtonType: .default, authorizationButtonStyle: .whiteOutline)
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.addSubview(loginButton)
        loginButton.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
        loginButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        
        loginButton.addAction(.init(handler: { (_) in
            AccountManager.shared.requestSignInAppleFlow()
        }), for: .touchUpInside)
    }
}

```

#実行
iOSアプリを実行し、サインインボタンをタップします。
Firebaseサインインが行われたログが出力されれば問題無く実装できています。
Firebaseコンソールでアカウントが作られている事が確認できます。
![アカウント確認](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4077/a56d1cd8-7ba8-1aae-00a3-37ea4720e81e.png)

#蛇足
AccountManagerに次のプロパティを追加すると便利です。

```AccountManager.swift
import Firebase

class AccountManager: NSObject {
    // Firebaseのサインイン状況
    var isSignedIn: Bool { Auth.auth().currentUser != nil }

    // FirebaseのUserID
    var uid: String? { Auth.auth().currentUser?.uid }
}
```