【SwiftUI】FirebaseAuthをつかってAppleIDログインを実装する

# はじめに
今回は、SwiftUIを使ってFirebaseAuthと連携するAppleIDログイン処理を実装します。
AppleIDログインとはよくみる「Sign in with Apple」です。

実装についてはFirebaseの公式サイトに載っているコードのコピペがほとんどですが、
SwiftUIで実装する記事があまりなかったため解説していきたいと思います。

# 実装の流れ
　① Firebase導入(プロジェクト作成・SDK導入）
　② UI実装
　③ ログイン処理実装


## ① Firebase導入(プロジェクト作成・SDK導入）
まずはFirebaseの初期設定とアプリにSDKを導入します。
すでにご存知の方は読み飛ばしていただいて構いません。

#### Xcodeプロジェクト作成
Xcodeから新規SwiftUIプロジェクトを作成してください。
今回はサンプルプロジェクトとして「AppleVerify」としました。

#### Firebaseプロジェクト作成
Firebaseの[コンソール](https://firebase.google.com/)上でプロジェクトを作成します。
SwiftのApp名と同様「AppleVerify」としました。


特に何も考えず次へを押していき作成を完了させます。

![001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233342/79587ac6-0a69-a3b2-21c9-82a8d17116f7.png)

#### FirebaseAuthでAppleID認証を有効にする
Firebaseのメニュ画面にてAuthenticationを選択し、
Sign-in methodタブのAppleを有効化して保存する。

![008.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233342/0c70b232-faa7-80a1-aab5-4a4fdbfe8cd6.png)


#### Firebaseにアプリを登録する
作成したプロジェクトのトップページからiosを選択しアプリを登録します。

![002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233342/3faae448-fc14-48f4-47ab-82c1a6d204ae.png)


アプリのバンドルIDのみ入力して次に進みます。

 ![003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233342/51ea1d2b-8367-710d-2aed-d8cb632f852e.png)

#### 設定ファイルのインポート
その後設定ファイルをダウンロードしてXcodeプロジェクトにドラックアンドドロップで追加します。

#### SDKの追加
FirebaseSDKの追加はXcodeのpackageRepositoryを使って導入しました。
XcodeのFile＞Swift Packages＞Add Package Dependencyをクリックして下記のリポジトリを入力。

https://github.com/firebase/firebase-ios-sdk.git

そのまま次へ進んでいき下記画面でFirebaseAuthを選択。

![004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233342/b8663f14-2b74-0c3f-a95d-87d934fa8ae4.png)


#### 初期化コード追加
最後に初期化コードを実装します。AppleVerifyApp.swiftを下記のようにしてください。
※SwiftUIでの実装のため公式のコードと異なっています。

```swift:AppleVerifyApp.swift
import SwiftUI
import Firebase

@main
struct AppleVerifyApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        FirebaseApp.configure()
        return true
    }
}
```

#### capabilityの設定
AppleIDでのサインインを許可する設定をXcode上で行います。

AppleVeryfy＞TARGETS＞Signing &Capabilitiesタブを選択。「＋Capability」から「Sign in with Apple」を追加する
![005.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233342/f6e12a06-b12b-25d4-485b-645cd0f628b1.png)


以上で事前準備は完了となります。
続いて実装に移っていきましょう。


## ② UI実装
ログインボタンを作成します。
ボタンのUIはAuthenticationServicesパッケージに含まれているASAuthorizationAppleIDButtonを使うことでよく見る共通のデザインで実装することが出来ます。

しかしこちらはUIKitのモジュールになりますのでSWiftUIで使用するためにはUIViewRepresentableでラップする必要があります。
新規SwiftファイルとしてSignInWithAppleButton.swiftを追加してください。

```swift:SignInWithAppleButton.swift
import SwiftUI
import AuthenticationServices

struct SignInWithAppleButton: UIViewRepresentable {
    typealias UIViewType = ASAuthorizationAppleIDButton
    
    func makeUIView(context: Context) -> ASAuthorizationAppleIDButton {
        return ASAuthorizationAppleIDButton(type: .signIn, style: .black)
    }
    
    func updateUIView(_ uiView: ASAuthorizationAppleIDButton, context: Context) {}
}
```

ContentView.swiftに反映して確認します。

```swift:ContentView.swift
struct ContentView: View {
    var body: some View {
        
        VStack {
            Button(action: {
                print("ここにログイン処理を書く")
            }, label: {
                SignInWithAppleButton()
                    .frame(height: 50)
                    .cornerRadius(16)
            })
            .padding()
        }
    }
}
```

![006.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233342/8fca082a-b2fb-3f70-9649-754ef6c94045.png)



ボタンが表示されました！UI実装はここまでです。
最後はいよいよログイン処理を実装していきます。


## ③ ログイン処理実装

#### 認証の流れ

まずはどのような流れでログイン認証がなされるのかを図で見ていきます。
![図1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233342/0eeeda7a-f8b3-aea5-5bdf-27002481da1d.png)


こちらの図の通り、まずユーザはAppleにcredentialをリクエストします。
その際、ノンス（nonce)を暗号化したものを付与しておき、検証時に改ざんされていないかを確認します。
ノンスとは暗号通信で用いられる、使い捨てのランダムな値のことです。

credentialを貰ったらそれを使ってFirebaseに認証をします。


#### 実装
実装ですが、認証の部分については[Firebase](https://firebase.google.com/docs/auth/ios/apple?hl=ja)のコードを参考。
ランダムなnonceの生成は[Auth0](https://auth0.com/docs/authorization/mitigate-replay-attacks-when-using-the-implicit-flow#generate-a-cryptographically-random-nonce)のドキュメントを参考にして実装しました。

新規Swiftファイルを作成し、下記をコピペしてください。

sginInWithApple()でappleへのrequestを生成しています

randomNonceString()とsha256()はランダム文字列と暗号化関数です。

authorizationController()ではAppleから受信したcredentialを検証し、
問題がなければFirebaseへログイン認証リクエストを生成しています。

```swift:SignInWithAppleObject.swift
import AuthenticationServices
import CryptoKit
import FirebaseAuth

public class SignInWithAppleObject: NSObject {
    private var currentNonce: String?
    
    public func signInWithApple() {
        let request = ASAuthorizationAppleIDProvider().createRequest()
        request.requestedScopes = [.email, .fullName]
        let nonce = randomNonceString()
        currentNonce = nonce
        request.nonce = sha256(nonce)
        
        let authorizationController = ASAuthorizationController(authorizationRequests: [request])
        authorizationController.delegate = self
        authorizationController.performRequests()
    }
    
    //  https://auth0.com/docs/api-auth/tutorials/nonce#generate-a-cryptographically-random-nonce
    private func randomNonceString(length: Int = 32) -> String {
        precondition(length > 0)
        let charset: Array<Character> =
            Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._")
        var result = ""
        var remainingLength = length
        
        while remainingLength > 0 {
            let randoms: [UInt8] = (0 ..< 16).map { _ in
                var random: UInt8 = 0
                let errorCode = SecRandomCopyBytes(kSecRandomDefault, 1, &random)
                if errorCode != errSecSuccess {
                    fatalError("Unable to generate nonce. SecRandomCopyBytes failed with OSStatus \(errorCode)")
                }
                return random
            }
            
            randoms.forEach { random in
                if remainingLength == 0 {
                    return
                }
                
                if random < charset.count {
                    result.append(charset[Int(random)])
                    remainingLength -= 1
                }
            }
        }
        
        return result
    }
    
    private func sha256(_ input: String) -> String {
        let inputData = Data(input.utf8)
        let hashedData = SHA256.hash(data: inputData)
        let hashString = hashedData.compactMap {
            return String(format: "%02x", $0)
        }.joined()
        
        return hashString
    }
    
}

extension SignInWithAppleObject: ASAuthorizationControllerDelegate {
    
    public func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        // Sign in With Firebase app
        if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
            guard let nonce = currentNonce else {
                print("Invalid state: A login callback was received, but no login request was sent.")
                return
            }
            
            guard let appleIDToken = appleIDCredential.identityToken else {
                print("Unable to fetch identity token")
                return
            }
            
            guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
                print("Unable to serialize token string from data")
                return
            }
            
            let credential = OAuthProvider.credential(withProviderID: "apple.com", idToken: idTokenString, rawNonce: nonce)
            Auth.auth().signIn(with: credential) { result, error in
                guard error == nil else {
                    print(error!.localizedDescription)
                    return
                }
            }
        }
    }
}
```

ログイン処理を実装しましたのでそれをContentViewのボタンに組み込みます。
SignInWithAppleObjectのインスタンスを定義し、
ボタンのaction内でsignInWithApple()を呼び出しています

```swift:ContentView.swift
struct ContentView: View {
    @State private var email: String = ""
    @State private var password: String = ""
    
    @State private var signInWithAppleObject = SignInWithAppleObject()
    
    var body: some View {
        
        VStack {
            TextField("E-mail", text: self.$email)
            TextField("Password", text: self.$password)
            
            Button(action: {
                signInWithAppleObject.signInWithApple()
            }, label: {
                SignInWithAppleButton()
                    .frame(height: 50)
                    .cornerRadius(16)
            })
            .padding()
        }
    }
}
```

以上で実装は終了となります。

# 動作確認
最後に動作確認を行います。
動作確認はシミュレータではうまく動作しないことがありますので実機で確認を行ってください。

実行してボタンをタップすると下記のように認証がされるかと思います。
![009.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233342/5c3b3b48-77c2-76a0-d991-abf8739c9f84.png)

FirebaseのAuthenticationコンソールを確認するとユーザが追加されました！！

![010.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233342/1f29fdae-ba91-43db-a184-92733f58f212.png)



# おわりに
本日はSwiftUIを用いてFirebaseのAppleID認証を実装しましたが、いかがでしたでしょうか。
私自身完全に理解しているわけではございませんので間違っている点がありましたらコメントいただけると嬉しいです。
以上です！