- ①基本設定編
- ②メールアドレス・パスワード認証によるユーザー登録編
- ③メールアドレス・パスワード認証によるログイン・ログアウト編
- ④メールアドレスとパスワードの更新編
- ⑤メールアドレスの確認メール送信・パスワード再設定メール送信編
- ⑥ユーザーの再認証編
- ⑦Googleアカウントログイン編
- ⑧Apple アカウントログイン編(本記事)
- ⑨ユーザープロフィール更新編
- ⑩匿名ログイン編
前回の Googleアカウントログインに引き続いて、今回は、Appleアカウントログインの手順を解説します。
前提として、Appleアカウントによるログインを実装するには、Apple Developer Program に参加(有料)していなければいけません。
Apple Developer Program の登録手順は以下の記事で紹介していますので参考にしてください。
Firebase Authentication の iOS 導入手順【⑧Appleアカウントログイン編】【SwiftUI】
(1) デベロッパーサイトで「Apple でサインイン」を有効にする
Apple デベロッパーサイトの Account ページを開き、[Certificates, Identifiers & Profiles] > [Identifier] と進み、導入したいアプリのAppIDをクリックします。
「Edit your App ID Configuration」のページが開くので、「Capabilities」の中から「Sign In with Apple」にチェックをし、[Save] > [Confirm] とクリックします。
(2) Firebase 側で Apple ログインを有効化する
次に、Firebase コンソールに移動し、Apple をログインプロバイダとして有効にします。
[Authentication] > [Sign-in method] から、「Apple」を選択し、有効にして保存します。ステータスが有効になったか確認しましょう。
(3) Signing & Capabilities に Sign in with Apple を追加
次に Xcode プロジェクト側の設定です。
[Target] > [Signing & Capabilities] から 「+ Capability」をクリックし、開いたウィンドウで「Sign In with Apple」をダブルクリックで追加します。開発、ステージング、本番など、Target が複数ある場合はそれぞれ追加するようにしてください。
(4) プログラム実装
基本的なプログラムは Firebase の公式ページのサンプルコードを踏襲していますが、SwiftUI で実装することを考慮した内容となっています。
1) AppleSignInButton
「Sign in with Apple」のボタンを、SwiftUI の画面で使える View として作成します。
struct AppleSignInButton: View {
var completed: (() -> Void)?
init(completed: (() -> Void)?) {
self.completed = completed
}
var body: some View {
AppleSignInButtonViewController(completed: self.completed)
}
}
サインイン完了後に内部でコールしてもらうクロージャを渡しています。
実際の body 部分は、後述する AppleSignInButtonViewController となっており、completed を更に受け渡しています。
2) AppleSignInButtonViewController
AppleSignInButton の実体(?)部分です。
UIViewControllerRepresentable 継承し、makeUIViewController で UIViewController を生成します。
こちらでも completed を受け渡しています。
struct AppleSignInButtonViewController: UIViewControllerRepresentable {
var completed: (() -> Void)?
init(completed: (() -> Void)?) {
self.completed = completed
}
func makeUIViewController(context: Context) -> UIViewController {
let viewController = AppleSignInViewController()
viewController.completed = self.completed
return viewController
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
}
}
3) AppleSignInViewController
今回の中核部分になります。大きく分けて以下の4つがポイントです。
- ASAuthorizationAppleIDButton の作成
- 「Sign in with Apple」のフロー呼び出し(startSignInWithAppleFlow)
- ASAuthorizationControllerPresentationContextProviding の実装
- ASAuthorizationControllerDelegate の実装
AppleSignInViewController 全体のソースコードは以下の通りです。
import UIKit
import AuthenticationServices
import CryptoKit
import FirebaseAuth
class AppleSignInViewController: UIViewController {
var completed: (() -> Void)?
fileprivate var currentNonce: String?
override func viewDidLoad() {
super.viewDidLoad()
let appleSignInButton = ASAuthorizationAppleIDButton(authorizationButtonType: .default, authorizationButtonStyle: .black)
appleSignInButton.addTarget(self, action: #selector(appleSignInButtonTapped(sender:)), for: .touchUpInside)
self.view.addSubview(appleSignInButton)
}
@objc func appleSignInButtonTapped(sender: Any) {
startSignInWithAppleFlow()
}
@available(iOS 13, *)
private 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()
}
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
}
@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
}
}
extension AppleSignInViewController: ASAuthorizationControllerPresentationContextProviding {
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
guard let window = UIApplication.shared.delegate?.window else {
fatalError()
}
return window!
}
}
extension AppleSignInViewController: 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
}
let credential = OAuthProvider.credential(
withProviderID: "apple.com",
idToken: idTokenString,
rawNonce: nonce
)
Auth.auth().signIn(with: credential) { (authResult, error) in
if let error = error {
print(error.localizedDescription)
return
}
self.completed?()
}
}
}
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
print(error.localizedDescription)
}
}
部分部分を切り取って見ていきましょう。
先ず、viewDidLoad で Apple 規定のデザインボタンである ASAuthorizationAppleIDButton を作成しています。タップ時のアクション(appleSignInButtonTapped)もここで関連付けしています。
let appleSignInButton = ASAuthorizationAppleIDButton(authorizationButtonType: .default, authorizationButtonStyle: .black)
appleSignInButton.addTarget(self, action: #selector(appleSignInButtonTapped(sender:)), for: .touchUpInside)
self.view.addSubview(appleSignInButton)
「Sign in with Apple」の認証リクエストを行っているのが startSignInWithAppleFlow メソッドの内容です。
@available(iOS 13, *)
private 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()
}
ログインのたびにランダムな文字列(ナンス)をリクエストに与える必要があります。ナンスの作成には、Firebase 公式ページに紹介されているメソッドをそのまま流用しています。
ランダム文字列の生成を randomNonceString メソッドで、暗号化を sha256 メソッドで行っています。
リクエストデータを渡して ASAuthorizationController 生成します。delegate と presentationContextProvider に self(UIViewController)をセットし、performRequests でサインインフローが開始されます。
ASAuthorizationControllerPresentationContextProviding の実装では、presentationAnchor を実装しアプリケーションの UIWindow を返すようにします。
extension AppleSignInViewController: ASAuthorizationControllerPresentationContextProviding {
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
guard let window = UIApplication.shared.delegate?.window else {
fatalError()
}
return window!
}
}
ASAuthorizationControllerDelegate の実装では、
「Apple でサインイン」が正常に行われた際にコールされる authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) の実装と、
何らかのエラー発生時にコールされる authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) の実装を行います。
extension AppleSignInViewController: 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
}
let credential = OAuthProvider.credential(
withProviderID: "apple.com",
idToken: idTokenString,
rawNonce: nonce
)
Auth.auth().signIn(with: credential) { (authResult, error) in
if let error = error {
print(error.localizedDescription)
return
}
self.completed?()
}
}
}
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
print(error.localizedDescription)
}
}
didCompleteWithAuthorization では、ASAuthorizationAppleIDCredential で Apple の認証情報を取り出します。
サインインをリクエストした際に保存しておいたナンスと共に、Firebase のサインイン用の認証情報(credential)を OAuthProvider.credential で作成し、Auth.auth().signIn で Firebase にサインインします。
サインアウトする際は Auth.auth().signOut をコールすればOKです(サインアウトはプロバイダ種別を問いません)。
4) SwiftUI での実装サンプル
最後に、SwiftUI の View に Sign in with Apple のボタンを設置したサンプルを紹介します。
struct ContentView: View {
@State private var showSignedIn = false
var body: some View {
VStack {
AppleSignInButton(completed: {
self.showSignedIn = true
})
.alert(isPresented: $showSignedIn) {
Alert(title: Text("Sign in with Apple."), message: Text("サインインしました"), dismissButton: .default(Text("OK")))
}
}
}
}
不格好ですが、左上にサインインボタンがあります。
ボタンをタップし、正常にリクエストされると以下のような画面が表示されるはずです。
Continue で進み、パスワードを入力して認証されるとアプリ側に処理が戻ります。無事 Firebase のサインインまで完了するとダイアログが表示されるはずです。
以上が「Sign in with Apple」の導入手順となります。
他、メールアドレスとパスワードによるサインアップ・サインイン、Google アカウントによるサインインについての記事もありますのでご参考ください。