#始めに
本ドキュメントは次の2点を目標とします。
- iOSアプリにAppleサインインを実装する
- Appleサインイン情報からFirebaseアカウントを作成する
執筆時の開発環境は次の通りです。
- Xcode 12.4
- Swift 5
Xcodeプロジェクトとそれに対応するFirebaseプロジェクトが作成済みであり、iOS側のFirebase初期導入手順は実装済みである事が前提です。
また次のライブラリが必要です。
- FirebaseAuth
基本的にはFirebaseドキュメント通りの内容ですが、次の2点が異なります。
- 必要最小限の情報のみ記載
- ビルドエラーを解決したコード
#実装
##Firebaseの設定
Authentication画面を表示します。
Sign-in methodタブを選択し、Appleを有効化して保存します。
その他の項目を編集する必要はありません。
##iOSアプリの実装
次の流れを実装します。
- Appleサインインし、Apple認証情報を取得する
- 取得したApple認証情報をFirebaseに渡し、Firebaseアカウントを作成する
###Capabilities設定
XcodeのSetting & Capabilities画面を表示し、「+Capability」をクリックします。
表示された一覧の中からSign in with Appleを追加します。
###AccountManagerクラスの実装
新しくAccountManagerという名前のシングルトンクラスを実装します。
このクラスにサインインに必要な処理を実装していきます。
class AccountManager: NSObject {
static var shared = AccountManager()
private override init() {
super.init()
}
}
####Appleサインイン機能
#####ナンス生成関数
Appleサインインする際、得られたトークンの正当性を検証する為「ナンス」と呼ばれる文字列を使用します。
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型を拡張し、文字列の暗号化関数を実装します。
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サインイン関数
実装するクラスに次のプロパティを追加します。
class AccountManager: NSObject {
// 生成したナンスを保持し、検証に使用します
var currentNonce = ""
}
Appleサインインを行う関数を実装します。
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を返しています。
extension AccountManager: ASAuthorizationControllerPresentationContextProviding {
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
return UIApplication.shared.windows.first { $0.isKeyWindow }!
}
}
#####Firebaseサインイン処理
Appleサインインの結果はASAuthorizationControllerDelegateを通して通知されます。
Appleサインインが正しく終了した場合、その情報を使ってFirebaseのアカウントを作成します。
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サインインボタンを配置し、タップ時の処理を実装します。
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コンソールでアカウントが作られている事が確認できます。
#蛇足
AccountManagerに次のプロパティを追加すると便利です。
import Firebase
class AccountManager: NSObject {
// Firebaseのサインイン状況
var isSignedIn: Bool { Auth.auth().currentUser != nil }
// FirebaseのUserID
var uid: String? { Auth.auth().currentUser?.uid }
}