58
32

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[iOS] Firebase AuthenticationでSign in with Apple

Last updated at Posted at 2019-11-15

こんにちは、株式会社実験のエンジニアのmogamingです。

今朝、firebase-ios-sdkにSign in with Apple対応がマージされてリリースされました🎉🎉🎉中の方々本当にありがとうございます。感謝しかない!

ちょうど新しいアプリでSNSログインが必要で、FirebaseのSDKに対応が入るのを待ちつつ別の機能を作ってました。せっかくリリースされたので早速対応してみたので、やったことを書きます。iOSアプリでの対応の話しかしません。

参考リンク

基本的にはドキュメントを読めばなんとかなります。読みましょう。

手順

Capabilityの設定

  • Certificates, Identifiers & Profilesで設定
    • メールを送りたい人はリレーの設定が必要ですが僕は不要なので割愛。ドメイン設定するだけ。
スクリーンショット 2019-11-15 16.17.24.png - Xcodeでも設定

Firebase側の設定

有効にするだけ

スクリーンショット 2019-11-15 16.20.42.png

コードを書く

Firebaseのドキュメントに書いてあるやつをそのまま持ってきます。ちょっと長いのでポイントを書いておくと

  • import AuthenticationServices が必須
  • SHA256import CryptKit が必要
  • authorizationController.presentationContextProvider = selfpresentationContextProviderASAuthorizationControllerPresentationContextProviding に準拠している必要がありますが、これは func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor をメソッドとして持っていればよく、この返り値の ASPresentationAnchorUIWindow のtypealiasとして定義されているようです。よって、このメソッドではSign in with Appleのモーダル表示を行いたい UIWindow を返せばよいです。僕はめんどくさかったので、AppDelegateに ASAuthorizationControllerPresentationContextProviding を準拠させて window を返すようにしてます。

以下関数はそのまま持ってきて、適当なUIViewControllerにおいたやつ。たぶん動くでしょう。

import UIKit
import AuthenticationServices
import CryptoKit

class ViewController: UIViewController {
    // Adapted from 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
    }
    
    // Unhashed nonce.
    fileprivate var currentNonce: String?

    @available(iOS 13, *)
    func startSignInWithAppleFlow() {
      let nonce = randomNonceString()
      currentNonce = nonce
      let appleIDProvider = ASAuthorizationAppleIDProvider()
      let request = appleIDProvider.createRequest()
      request.requestedScopes = [.fullName, .email]
      request.nonce = sha256(nonce)

      let authorizationController = ASAuthorizationController(authorizationRequests: [request])
      authorizationController.delegate = self
      authorizationController.presentationContextProvider = self
      authorizationController.performRequests()
    }

    @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
    }
}

@available(iOS 13.0, *)
extension ViewController: ASAuthorizationControllerDelegate {
  func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
    if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
      guard let nonce = currentNonce else {
        fatalError("Invalid state: A login callback was received, but no login request was sent.")
      }
      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: \(appleIDToken.debugDescription)")
        return
      }
      // Initialize a Firebase credential.
      let credential = OAuthProvider.credential(withProviderID: "apple.com",
                                                idToken: idTokenString,
                                                rawNonce: nonce)
      // Sign in with Firebase.
      Auth.auth().signIn(with: credential) { (authResult, error) in
        if error {
          // Error. If error.code == .MissingOrInvalidNonce, make sure
          // you're sending the SHA256-hashed nonce as a hex string with
          // your request to Apple.
          print(error.localizedDescription)
          return
        }
        // User is signed in to Firebase with Apple.
        // ...
      }
    }
  }

  func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
    // Handle error.
    print("Sign in with Apple errored: \(error)")
  }
}

@available(iOS 13.0, *)
extension ViewController: ASAuthorizationControllerPresentationContextProviding {
    func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
        return self.view.window!
    }
}

ボタンの配置

Human Interface Guideline に従っておけばよいでしょう。ASAuthorizationAppleIDButton を使えば見た目は全部やってくれますが、ボタンの文言は予め用意されているものしか使えず、文字の大きさはボタンの大きさによって勝手に変わるようになっています。cornerRadiusは設定できます。自前で作っちゃだめとは書いてないように思うので、自分で作っても良いと思います。

iOS12の対応

iOS12では CrypoKit がバンドルされていないため、ランタイムでクラッシュしてしまいます。 BuildSettingsOther Linker Flags-weak_framework CryptoKitを追加設定しましょう。

挙動メモ

UI

  • requestedScopes に指定したものが提供されないと 続ける が有効にならない
  • 名前の部分はこの画面上で編集できるし、編集後のものがアプリには渡される
  • ログインの体験としては個人的には最高だった…

発行されるIDTokenのPayload

IDTokenにはFullNameが入っていなかったように思う。

{
  "iss": "https://appleid.apple.com",
  "aud": "アプリのBundleID",
  "exp": 1573797591,
  "iat": 1573796991,
  "sub": "Apple側で管理しているUserIDかな?Sign in with Appleの仕様にそこまで詳しくないがグローバルなものではないと思う。アプリごととか開発者アカウントごとな気がしている。",
  "nonce": "2ca34ffa6c2c5379d7454f457ba3b6eb53c419ccff0cffd801ed340bf24e1751",
  "c_hash": "OBuSAYvYURTfMqOzMWgWaA",
  "email": "vd9xrua52z@privaterelay.appleid.com(僕の非公開メアド。ここには許可したドメインからしかメールが送れない)",
  "email_verified": "true",
  "is_private_email": "true",
  "auth_time": 1573796991
}

Firebase.User.displayName

firebase側のUserクラスにはdisplayNameという変数があり、例えばTwitterログインするとTwitterの名前が入っている。Sign in with AppleでfullName指定してログインしたら入ってるかなーと気軽にやってみたら入っていなかった。下記ツイート以下を見てもらえればよいが、そういうものっぽい。

fullNameのフォーマット

ASAuthorizationAppleIDCredential から fullName が取れる。こいつの型は PersonNameComponentsFormatter でこの中にgivenNameとかfamilyNameがあるんだけど、いい感じに組み立ててほしかったので調べたらフォーマッターが用意されていた。PersonNameComponentsFormatter というやつで、style を適宜変えるといい感じになって返ってくる。ドキュメントに例が載っていてめちゃくちゃわかりやすいのでぜひ見てみて。

おわりに

iOSアプリのみとかの場合はとってもいい認証方法だと思うが、一般の方々がどれくらいAppleIDに対して抵抗があるかよくわからないのでなんだかんだこれ1本にはし難いかもしれない。僕はこれ1本にしたい。

58
32
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
58
32

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?