見出し画像

【Swift5】Appleログイン(Sign In With Apple)・新規登録を実装する

Appleログイン(Sign In With Apple)について


※Apple Developer Programに登録してある前提で進めていきます。

GoogleやTwitter,Facebookなどのサードパーティ(外部のシステム)のログイン機能を使用してログインを実装する場合、Appleログインを必ず実装しなければリリースができなくなりました。
また現在リリース済みのアプリについても、サードパーティのログインを実装している場合は2020年4月までにアップデートが必要になります。
筆者もこれには頭を悩ませました。。
そして、Appleログインを実装している記事はあったりするのですが、Firebaseと連携したAppleログインの記事はかなり少ないです。
そのため、今回実装させていただきます。
一点実装するに当たって注意点があります。

Appleログインには開発者の有料アカウント(developer program)が必要です!

そのため練習で実装してみようという感じではできないのもしんどいです。
もしまだdeveloper programに登録していない人は、この章は飛ばしてしまって問題ないです。(アプリ自体は完成できます!)

次の章よりいよいよ実装していきます。少し難しいですが、がんばって実装してみましょう!

1.Firebaseの設定

Firebase consoleを開きAuthenticationのログイン方法に移動します。

画像1

画面下の方に「Apple」がありますのでそちらをクリックします。

画像2

画面右上に有効にするスイッチがあるのでクリックして有効にします。
スイッチが反転して有効になっていることを確認したら画面右下の「保存」をクリックします。
こちらでFirebaseの設定は完了になります。

2.Apple Developer Programの設定


※今回の章では、証明書(Certificate)の設定等は割愛させていただきます。
こちらの記事にかなり分かりやすくまとまっているので、証明書の登録が終わっていない方は記事を参考にして「2.Certificateの作成」まで進めた後この章に戻ってきてください。

参考記事:【2019年版】iOSアプリをApp Storeに公開するための全手順まとめ

まずはApple Developerに移動します。

画像3

画面右上部の「Account」をクリックします。
ここで初めてログインする場合はログインを求められると思うので、ログインをしてください。

画像4

ログインが完了すると、こちらの画面にいくので、
左の「Certificates, Identifiers & Profiles」をクリックしてください。

画像5

画面左のメニューより「Identifiers」をクリックします。
こちらの画面でIdentifiersの隣にある「青い+ボタン」をクリックします。

画像6

この画面で、「App IDs」が選択されている状態で画面右上の「Continue」をクリックします。

画像7

この画面で入力する内容は2つ、加えてAppleログインを使用する設定をします!

Description
→自分がアプリを認識できる説明(一般的にはアプリ名)
Bundle ID
→Xcodeで設定してあるbundle identifier

Appleログインを使用する設定は、画面を下の方にスクロールしていきます。

画像8

画像の真ん中らへんに「Sign In With Apple」とあるのでこちらにチェックをつけます。
チェックと入力が完了したら画面右上部の「Continue」ボタンをクリックします。

画像9

こちらの画面に変わったら画面右の「Register」をクリックします。

画像10

登録が完了して「Identifiers」に自分が登録したアプリが追加されていれば完了です。今回私はFirebaseAppSampleとしました。

これでDeveloper Programの設定は完了です。

3.Appleログインの実装

Appleログインは設定がいろいろ必要でめんどくさいが、もう少し頑張りましょう。(書くのも疲れる。。)

3.1 Xcodeの設定
まずはXcodeの設定です。

左のメニューからプロジェクト名の部分を選択して、「Signing & Capabilities」を開きます。

画像11

+ Capabilitity」をクリックすると画像のようなWindowが開きます。
その中から「Sign In With Apple」を探してSigningの下あたりにドラッグアンドドロップします。

画像12

Sign In With AppleがSigningの下に追加されて、特にエラーが出ていなければXcodeの設定は完了です。
※ここで「Signing Certificate」にエラーが出る場合は、一つ前の章のdeveloper programの設定でSign In With Appleにチェックがついていなかった可能性があります。確認してみてください。

3.2 AppleLoginViewControllerの実装
Appleログインのボタンはstoryboardでは設定せず、コードにて実装を行います。(いくつか試したのですができなかったため)
今回は他のログインと比べてかなりコード量があるので、大変ですが少しづつ解説していきます。

今回追記するコードを大きく分けると6箇所です。
まずは全体のコードを載せます。

import UIKit
// ①importする
import AuthenticationServices
import CryptoKit
import Firebase
import PKHUD  // 必要に応じて
import FirebaseAuth

class AppleLoginViewController: UIViewController {
   
   fileprivate var currentNonce: String?
   
   override func viewDidLoad() {
       super.viewDidLoad()
       // ②レイアウトを作成する
       // IOS13以降のみ使えるので、そのように制限
       if #available(iOS 13.0, *) {
           // ここでインスタンス(ボタン)を生成
           let appleLoginButton = ASAuthorizationAppleIDButton(
               authorizationButtonType: .default,
               authorizationButtonStyle: .whiteOutline
           )
           // ボタン押した時にhandleTappedAppleLoginButtonの関数を呼ぶようにセット
           appleLoginButton.addTarget(
               self,
               action: #selector(handleTappedAppleLoginButton(_:)),
               for: .touchUpInside
           )
           // ↓はレイアウトの設定
           // これを入れないと下の方で設定したAutoLayoutが崩れる
           appleLoginButton.translatesAutoresizingMaskIntoConstraints = false
           // Viewに追加
           view.addSubview(appleLoginButton)
           
           // ↓はAutoLayoutの設定
           // appleLoginButtonの中心を画面の中心にセットする
           appleLoginButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
           appleLoginButton.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
           // appleLoginButtonの幅は、親ビューの幅の0.7倍
           appleLoginButton.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 0.7).isActive = true
           // appleLoginButtonの高さは40
           appleLoginButton.heightAnchor.constraint(equalToConstant: 40.0).isActive = true
       }
   }
   
   
   // ③appleLoginButtonを押した時の挙動を設定
   @available(iOS 13.0, *)
   @objc func handleTappedAppleLoginButton(_ sender: ASAuthorizationAppleIDButton) {
       // ランダムの文字列を生成
       let nonce = randomNonceString()
       // delegateで使用するため代入
       currentNonce = nonce
       // requestを作成
       let request = ASAuthorizationAppleIDProvider().createRequest()
       // sha256で変換したnonceをrequestのnonceにセット
       request.nonce = sha256(nonce)
       // controllerをインスタンス化する(delegateで使用するcontroller)
       let controller = ASAuthorizationController(authorizationRequests: [request])
       controller.delegate = self
       controller.presentationContextProvider = self
       controller.performRequests()
   }
   
   // ④randomで文字列を生成する関数を作成
   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 length == 0 {
                   return
               }
               
               if random < charset.count {
                   result.append(charset[Int(random)])
                   remainingLength -= 1
               }
           }
       }
       return result
   }
   
   // ⑤SHA256を使用してハッシュ変換する関数を用意
   @available(iOS 13, *)
   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
   }
   
   
   @IBAction func handleBackViewButton(_ sender: Any) {
       self.dismiss(animated: true, completion: nil)
   }
   
   
   
}


// ⑥extensionでdelegate関数に追記していく
extension AppleLoginViewController: ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
   // 認証が成功した時に呼ばれる関数
   func authorizationController(controller _: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
       // credentialが存在するかチェック
       guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential else {
           return
       }
       // nonceがセットされているかチェック
       guard let nonce = currentNonce else {
         fatalError("Invalid state: A login callback was received, but no login request was sent.")
       }
       // credentialからtokenが取得できるかチェック
       guard let appleIDToken = appleIDCredential.identityToken else {
           print("Unable to fetch identity token")
           return
       }
       // tokenのエンコードを失敗
       guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
            print("Unable to serialize token string from data: \(appleIDToken.debugDescription)")
           return
       }
       // 認証に必要なcredentialをセット
       let credential = OAuthProvider.credential(
           withProviderID: "apple.com",
           idToken: idTokenString,
           rawNonce: nonce
       )
       // Firebaseへのログインを実行
       Auth.auth().signIn(with: credential) { (authResult, error) in
          if let error = error {
              print(error)
              // 必要に応じて
              HUD.flash(.labeledError(title: "予期せぬエラー", subtitle: "再度お試しください。"), delay: 1)
              return
          }
          if let authResult = authResult {
              print(authResult)
              // 必要に応じて
              HUD.flash(.labeledSuccess(title: "ログイン完了", subtitle: nil), onView: self.view, delay: 1) { _ in
               // 画面遷移など行う
              }
          }
       }
   }

   // delegateのプロトコルに設定されているため、書いておく
   func presentationAnchor(for _: ASAuthorizationController) -> ASPresentationAnchor {
       return view.window!
   }
   
   // Appleのログイン側でエラーがあった時に呼ばれる
   func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
     // Handle error.
     print("Sign in with Apple errored: \(error)")
   }
   
   
}

かなり長いですが、6つのブロックに分けてご説明していきます。
簡単に6つのブロックを説明しておきます。

実装の6つのブロック
①必要なライブラリをimportと変数の宣言
②レイアウトを作成する
③appleLoginButtonを押した時の挙動を設定
④randomで文字列を生成する関数を作成(③の関数内で使用)
⑤SHA256を使用してハッシュ変換する関数を用意(③の関数内で使用)
⑥extensionでdelegate関数に追記する

順に説明していきます。

①必要なライブラリをimportと変数を宣言

// ①importする
import AuthenticationServices
import CryptoKit
import Firebase
import PKHUD // 必要に応じて
import FirebaseAuth

class AppleLoginViewController: UIViewController {
   
   fileprivate var currentNonce: String?

AuthenticationServicesはAppleログインに使用するライブラリです。
CryptoKitはSHA256のハッシュを生成する時に使用するライブラリです。
後ほど使用するcurrentNonceと言う変数を宣言します。

②レイアウトを作成する

 override func viewDidLoad() {
       super.viewDidLoad()
       // ②レイアウトを作成する
       // IOS13以降のみ使えるので、そのように制限
       if #available(iOS 13.0, *) {
           // ここでインスタンス(ボタン)を生成
           let appleLoginButton = ASAuthorizationAppleIDButton(
               authorizationButtonType: .default,
               authorizationButtonStyle: .whiteOutline
           )
           // ボタン押した時にhandleTappedAppleLoginButtonの関数を呼ぶようにセット
           appleLoginButton.addTarget(
               self,
               action: #selector(handleTappedAppleLoginButton(_:)),
               for: .touchUpInside
           )
           // ↓はレイアウトの設定
           // これを入れないと下の方で設定したAutoLayoutが崩れる
           appleLoginButton.translatesAutoresizingMaskIntoConstraints = false
           // Viewに追加
           view.addSubview(appleLoginButton)
           
           // ↓はAutoLayoutの設定
           // appleLoginButtonの中心を画面の中心にセットする
           appleLoginButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
           appleLoginButton.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
           // appleLoginButtonの幅は、親ビューの幅の0.7倍
           appleLoginButton.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 0.7).isActive = true
           // appleLoginButtonの高さは40
           appleLoginButton.heightAnchor.constraint(equalToConstant: 40.0).isActive = true
       }
   }

コードが少し長いので細かく分けてみていきます。

// ②レイアウトを作成する
// IOS13以降のみ使えるので、そのように制限
   if #available(iOS 13.0, *) {
      // ここでインスタンス(ボタン)を生成
      let appleLoginButton = ASAuthorizationAppleIDButton(
      authorizationButtonType: .default,
      authorizationButtonStyle: .whiteOutline
   )

ASAuthorizationAppleIDButtonと言うのがAppleログインのボタンになります。
buttonTypeとbuttonStyleは変えると少々変わるので、自由に変えてみてください。

// ボタン押した時にhandleTappedAppleLoginButtonの関数を呼ぶようにセット
appleLoginButton.addTarget(
  self,
  action: #selector(handleTappedAppleLoginButton(_:)),
  for: .touchUpInside
)

ここでAppleログインのボタンをタップした時のアクションを設定します。(storyboardでボタンを設定していない時のアクションの設定の仕方です。)
今回ボタンをタップすると「handleTappedAppleLoginButton」という関数が実行されるようになります。
handleTappedAppleLoginButton」は③で別途定義しています。

// ↓はレイアウトの設定
// これを入れないと下の方で設定したAutoLayoutが崩れる
appleLoginButton.translatesAutoresizingMaskIntoConstraints = false
// Viewに追加
view.addSubview(appleLoginButton)
           
// ↓はAutoLayoutの設定
// appleLoginButtonの中心を画面の中心にセットする
appleLoginButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
appleLoginButton.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
// appleLoginButtonの幅は、親ビューの幅の0.7倍
appleLoginButton.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 0.7).isActive = true
// appleLoginButtonの高さは40
appleLoginButton.heightAnchor.constraint(equalToConstant: 40.0).isActive = true

ここでは生成したボタンをViewに追加するのと、AutoLayoutを設定しています。コードでAutoLayoutを設定する時は、
appleLoginButton.translatesAutoresizingMaskIntoConstraints = false
こちらを設定しないとレイアウトが崩れるので注意が必要です。
それ以下はAutoLayoutの設定になります。詳しくはこちらの記事を参考にしてみてださい。

参考記事:Auto Layoutをコードで書いてみた

③appleLoginButtonを押した時の挙動を設定

// ③appleLoginButtonを押した時の挙動を設定
   @available(iOS 13.0, *)
   @objc func handleTappedAppleLoginButton(_ sender: ASAuthorizationAppleIDButton) {
       // ランダムの文字列を生成
       let nonce = randomNonceString()
       // delegateで使用するため代入
       currentNonce = nonce
       // requestを作成
       let request = ASAuthorizationAppleIDProvider().createRequest()
       // sha256で変換したnonceをrequestのnonceにセット
       request.nonce = sha256(nonce)
       // controllerをインスタンス化する(delegateで使用するcontroller)
       let controller = ASAuthorizationController(authorizationRequests: [request])
       controller.delegate = self
       controller.presentationContextProvider = self
       controller.performRequests()
   }

②で設定した「handleTappedAppleLoginButton」の設定になります。

// ランダムの文字列を生成
let nonce = randomNonceString()
// delegateで使用するため代入
currentNonce = nonce

まずnonceというランダムの文字列を生成します。
nonceというのは暗号化通信で使用される使い捨ての文字列のことです。
詳しくはこちらを参照ください。
randomNonceString()とありますが、こちらは④で定義しています。
currentNonce = nonceで生成したnonceを代入します。(こちらはdelegateメソッドで使用します。)

// requestを作成
let request = ASAuthorizationAppleIDProvider().createRequest()
// sha256で変換したnonceをrequestのnonceにセット
request.nonce = sha256(nonce)

appleログインで使用するリクエストを生成します。
リクエストのnonceにSHA256で変換したnonceをセットします。

// controllerをインスタンス化する(delegateで使用するcontroller)
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = self
controller.presentationContextProvider = self
controller.performRequests()

delegateで使用するcontrollerに先ほど生成したrequestをセットします。

④randomで文字列を生成する関数を作成

// ④randomで文字列を生成する関数を作成
   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 length == 0 {
                   return
               }
               
               if random < charset.count {
                   result.append(charset[Int(random)])
                   remainingLength -= 1
               }
           }
       }
       return result
   }

④はランダムで文字列を生成する関数です。
そのままコピーしてお使いください。

⑤SHA256を使用してハッシュ変換する関数を用意

// ⑤SHA256を使用してハッシュ変換する関数を用意
   @available(iOS 13, *)
   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
   }
   
   
   @IBAction func handleBackViewButton(_ sender: Any) {
       self.dismiss(animated: true, completion: nil)
   }

こちらもそのまま使えるのでコピーしてください。

⑥extensionでdelegate関数に追記していく

// ⑥extensionでdelegate関数に追記していく
extension AppleLoginViewController: ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
   // 認証が成功した時に呼ばれる関数
   func authorizationController(controller _: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
       // credentialが存在するかチェック
       guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential else {
           return
       }
       // nonceがセットされているかチェック
       guard let nonce = currentNonce else {
         fatalError("Invalid state: A login callback was received, but no login request was sent.")
       }
       // credentialからtokenが取得できるかチェック
       guard let appleIDToken = appleIDCredential.identityToken else {
           print("Unable to fetch identity token")
           return
       }
       // tokenのエンコードを失敗
       guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
            print("Unable to serialize token string from data: \(appleIDToken.debugDescription)")
           return
       }
       // 認証に必要なcredentialをセット
       let credential = OAuthProvider.credential(
           withProviderID: "apple.com",
           idToken: idTokenString,
           rawNonce: nonce
       )
       // Firebaseへのログインを実行
       Auth.auth().signIn(with: credential) { (authResult, error) in
          if let error = error {
              print(error)
         // 必要に応じて
              HUD.flash(.labeledError(title: "予期せぬエラー", subtitle: "再度お試しください。"), delay: 1)
              return
          }
          if let authResult = authResult {
              print(authResult)
         // 必要に応じて
              HUD.flash(.labeledSuccess(title: "ログイン完了", subtitle: nil), onView: self.view, delay: 1) { _ in
               // 画面遷移など行う
              }
          }
       }
   }

   // delegateのプロトコルに設定されているため、書いておく
   func presentationAnchor(for _: ASAuthorizationController) -> ASPresentationAnchor {
       return view.window!
   }
   
   // Appleのログイン側でエラーがあった時に呼ばれる
   func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
     // Handle error.
     print("Sign in with Apple errored: \(error)")
   }
   
   
}

extensionでdelegateメソッドに追記をしていきます。
delegateメソッドは3つあります。

①Appleログインが成功した時
・func authorizationController(controller _: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) 
②Appleログインが失敗した時
・func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error)
③とりあえず設定が必要
・func presentationAnchor(for _: ASAuthorizationController) -> ASPresentationAnchor

順に解説していきます。

①Appleログインが成功した時

func authorizationController(controller _: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
       // credentialが存在するかチェック
       guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential else {
           return
       }
       // nonceがセットされているかチェック
       guard let nonce = currentNonce else {
         fatalError("Invalid state: A login callback was received, but no login request was sent.")
       }
       // credentialからtokenが取得できるかチェック
       guard let appleIDToken = appleIDCredential.identityToken else {
           print("Unable to fetch identity token")
           return
       }
       // tokenのエンコードを失敗
       guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
            print("Unable to serialize token string from data: \(appleIDToken.debugDescription)")
           return
       }
       // 認証に必要なcredentialをセット
       let credential = OAuthProvider.credential(
           withProviderID: "apple.com",
           idToken: idTokenString,
           rawNonce: nonce
       )
       // Firebaseへのログインを実行
       Auth.auth().signIn(with: credential) { (authResult, error) in
          if let error = error {
              print(error)
         // 必要に応じて
              HUD.flash(.labeledError(title: "予期せぬエラー", subtitle: "再度お試しください。"), delay: 1)
              return
          }
          if let authResult = authResult {
              print(authResult)
         // 必要に応じて
              HUD.flash(.labeledSuccess(title: "ログイン完了", subtitle: nil), onView: self.view, delay: 1) { _ in
                // 画面遷移など行う
              }
          }
       }
   }

ログインが成功した場合は情報の取得に失敗しているの可能性があるので、guard文で必要な情報が入っていない場合はreturnするようにしています。

// 認証に必要なcredentialをセット
       let credential = OAuthProvider.credential(
           withProviderID: "apple.com",
           idToken: idTokenString,
           rawNonce: nonce
       )
// Firebaseへのログインを実行
       Auth.auth().signIn(with: credential) { (authResult, error) in
          if let error = error {
              print(error)
         // 必要に応じて
              HUD.flash(.labeledError(title: "予期せぬエラー", subtitle: "再度お試しください。"), delay: 1)
              return
          }
          if let authResult = authResult {
              print(authResult)
         // 必要に応じて
              HUD.flash(.labeledSuccess(title: "ログイン完了", subtitle: nil), onView: self.view, delay: 1) { _ in
                // 画面遷移など行う
              }
          }
       } 

認証に必要な情報をcredentialにセットして、Firebaseへのログインを実行します。

こちらで実装は完了です!
ログインできているかの確認ですが、シミュレーターだと自分のappleIDでログインしていないと思うので、実機で確認するのがいいと思います。

Authenticationに追加されていれば認証は完了です。

こちらで全てのログイン機能の実装が完了しました。
実際に自分のオリジナルアプリを作る場合は必要な昨日だけ抜粋してあげるといいと思います!
Firebaseにはこの他にもFacebookログインやTwitterログインなども簡単に実装できるようになっています!
是非やってみてください!

この記事が気に入ったらサポートをしてみませんか?