[Firebase][iOS] Firebase Authentication で会員機能を作ってみよう

94件のシェア(ちょっぴり話題の記事)

はじめに

モバイルアプリサービス部の中安です。

今さらながというわけでもありませんが、最近 Firebase を触って色々と試しています。

試していったサービスをできるだけアウトプットしていこうかなということで、 今回は「Firebase Authenticationを使って会員機能を作ってみる」というテーマで Firebaseを絡めた会員機能をもったiOSアプリを作る前提の実装ベースで書いていきます。

ウダウダと書きますが、何かのお役に立てば幸いです。

準備

さて、この記事は

  • Firebaseプロジェクトの作成は終わっている
  • Xcode側ではFirebaseSDKの組み込みなどが終わっている

という前提で書いていきます。

このあたりがまだという方は、ドキュメントを参照してくださいませ。

Xcodeプロジェクト側

Cocoapods

PodfileFirebase Authentication用のライブラリを含めて、ターミナルで pod install します

pod 'Firebase/Auth'

Firebaseプロジェクト側

ログイン方法

上図のように ログイン方法 タブから メール/パスワード を選択します。鉛筆のアイコンで編集画面を開きます。

有効にしたら保存します。

下準備としてはこれくらいでしょうか、簡単ですねぇ。

サインアップ

「サインアップ」は言わずもがな「新規アカウント登録」のことです。

それは複雑なフローの場合もあるでしょうし、簡易なフローの場合もあるとは思いますが、 多くのサービスでは下図のような手順で新規アカウント登録が行われることが多いと思います。

  1. ユーザがメールアドレス、パスワード、そして表示用名前を入力する
  2. サインアップボタン押下で、ユーザはまず仮会員(または未会員状態)になることができる
  3. 入力したメールアドレス宛に確認用のメールが届いて、ユーザは書かれているアクティベート用のリンクを踏む
  4. ユーザは本会員(確認済みユーザ)になる

一から作ろうと思うとそこそこ工数がかかりそうなこのサインアップのフローを Firebase ではあまり複雑に考えずに実装ができます。

前提

まず、以下のようなビューコントローラを作っておきます。

import UIKit
import Firebase

class SignUpViewController: UIViewController {
    
    @IBOutlet private weak var nameTextField: UITextField!
    @IBOutlet private weak var emailTextField: UITextField!
    @IBOutlet private weak var passwordTextField: UITextField!
    
    @IBAction private func didTapSignUpButton() {
        // あとで実装
    }
}

ストーリーボードには、上図の一番左にある画面のように各部品がレイアウト配置してあるものとします。

エラーを表示する共通メソッド

この辺は開発する人の好みかもしれませんが、 エラーがあったときにその内容を表示するようなメソッドを用意しておくといいかもしれません。

今回は簡易的なエラーダイアログで出すようにします。

private func showErrorIfNeeded(_ errorOrNil: Error?) {
    // エラーがなければ何もしません
    guard let error = errorOrNil else { return }
    
    let message = "エラーが起きました" // ここは後述しますが、とりあえず固定文字列
    let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
    present(alert, animated: true, completion: nil)
}

実装

サインアップボタンが押下されたタイミングで Firebaseに処理を投げることにします。

「登録しますか?」などのダイアログなどは今回つけない前提です。

@IBAction private func didTapSignUpButton() {
    let email = emailTextField.text ?? ""
    let password = passwordTextField.text ?? ""
    let name = nameTextField.text ?? ""
    
    Auth.auth().createUser(withEmail: email, password: password) { [weak self] result, error in
        guard let self = self else { return }
        if let user = result?.user {
            let req = user.createProfileChangeRequest()
            req.displayName = name
            req.commitChanges() { [weak self] error in
                guard let self = self else { return }
                if error == nil {
                    user.sendEmailVerification() { [weak self] error in
                        guard let self = self else { return }
                        if error == nil {
                            // 仮登録完了画面へ遷移する処理
                        }
                        self.showErrorIfNeeded(error)
                    }
                }
                self.showErrorIfNeeded(error)
            }
        }
        self.showErrorIfNeeded(error)
    }
}

なかなか長ったらしいソースコードになったので分割して話します。

新しいユーザアカウント作成

新しいユーザアカウントを作るためには、FirebaseSDKが提供するAuthクラスのシングルトンインスタンス(auth()で呼び出す)には createUser() メソッドがあるので、それを呼び出すだけです。

まずは最低限の情報であるメールアドレスとパスワードのみを渡します。 成功すると結果オブジェクトに作成されたUserクラスのユーザオブジェクトが返されてきます。

ユーザに表示用の名前を与える

ユーザに名前を持たせるには、 返ってきたユーザオブジェクトが持つcreateProfileChangeRequest()で変更用リクエストオブジェクトを生成し、 そこに名前を設定して commitChanges() を呼び出すことでFirebaseに再通信します。

確認(アクティベート)メールをユーザに送る

最後は、アクティベート用のメールアドレスをユーザに送る sendEmailVerification() を呼び出してやります。 こうすることで Firebaseがユーザに対してメールを送信してくれます。

ここまで成功すれば「登録できました」とユーザに表示する、または完了画面に遷移させるなどをすればいいわけです。

動作確認

ここまでを実際にアプリを実行して動かしてみると、メールが実際に届きました。

リンクを踏むとブラウザに「メールアドレスは確認済みです。新しいアカウントでログインできるようになりました」という味気ない画面が登場します。 この表示のカスタマイズはFirebase Authenticationコンソールの テンプレートタブ > メールアドレスの確認 > 編集(鉛筆アイコン) > アクション URL をカスタマイズ から行えるようです。また、メールの内容や言語も同じ編集画面で行えます。

詳しくは、ヘルプを確認ください

サンプルなのでエラー処理についてはもう少し考える必要がありそうですが、 これだけの実装で上に書いた図のフローを完成させることができました。

ネストを減らそう

先ほど書いたソースコードは一連の流れをまとめて書いたのでネストが深くて読みづらくなってしまいました。 現実的にはメソッドを分けてあげるほうが見通しは良さそうです。 (さらにいうと、ビューコントローラからFirebase自体の処理は切り離したいところですが、今回は割愛です)

@IBAction private func didTapSignUpButton() {
    let email = emailTextField.text ?? ""
    let password = passwordTextField.text ?? ""
    let name = nameTextField.text ?? ""
    
    signUp(email: email, password: password, name: name)
}

private func signUp(email: String, password: String, name: String) {
    Auth.auth().createUser(withEmail: email, password: password) { [weak self] result, error in
        guard let self = self else { return }
        if let user = result?.user {
            self.updateDisplayName(name, of: user)
        }
        self.showError(error)
    }
}

private func updateDisplayName(_ name: String, of user: User) {
    let request = user.createProfileChangeRequest()
    request.displayName = name
    request.commitChanges() { [weak self] error in
        guard let self = self else { return }
        if error != nil {
            self.sendEmailVerification(to: user)
        }
        self.showError(error)
    }
}

private func sendEmailVerification(to user: User) {
    user.sendEmailVerification() { [weak self] error in
        guard let self = self else { return }
        if error != nil {
            self.showSignUpCompletion()
        }
        self.showError(error)
    }
}

private func showSignUpCompletion() {
    // 完了したことを表示する
}

アクティベートされたユーザ

Firebaseではverifiedという言葉で表されていますが、 「メールで確認したユーザ」と「そうでないユーザ」に分かれます。

メール確認をしたユーザしかアプリとして受けつけないという仕様にするかそうしないかは、 セキュアのレベル要件によって変わると思います。

「メール確認をしたユーザかどうか」は UserオブジェクトのisEmailVerifiedというフラグプロパティで取得できます。

createUser()をした時点でFirebaseのユーザとしては成立してしまっているので、 アプリ側でこのフラグを確認して、どのように分岐させるかは考えなければいけません。

サインイン

「サインイン」は「(既存ユーザとして)ログイン」のことです。

多くのサービスでは下図のように認証情報を入力することで会員としての機能が使えるようになると思います。

シンプルですね

前提

まず、以下のようなビューコントローラを作っておきます。

import UIKit
import Firebase

class SignInViewController: UIViewController {
    
    @IBOutlet private weak var emailTextField: UITextField!
    @IBOutlet private weak var passwordTextField: UITextField!
    
    @IBAction private func didTapSignInButton() {
        // あとで実装
    }
}

ストーリーボードには、認証情報を入れるテキストフィールドとサインインボタンのあるレイアウト配置をしてあるものとします。

実装

サインインボタンが押下されたときの実装は下記のようになります。 フローが少ないぶん実装もサインアップよりはシンプルになります。

@IBAction private func didTapSignInButton() {
    let email = emailTextField.text ?? ""
    let password = passwordTextField.text ?? ""
    
    Auth.auth().signIn(withEmail: email, password: password) { [weak self] result, error in
        guard let self = self else { return }
        if let user = result?.user {
            // サインイン後の画面へ
        }
        self.showErrorIfNeeded(error)
    }
}

エラー処理について

さて、サインアップでもサインインでも起きうるのはエラーです。 それはネットワーク起因の問題かもしれないし、Firebase起因かもしれない、または認証情報を打ち間違えたなどのユーザ起因によるものかもしれません。

ユーザの入力した内容については、先にアプリで入力値バリデーションをしておきたいところですが、 たとえば既に登録されているアカウントへのサインアップであったり、メールアドレスの妥当性、パスワード間違いなどという Firebase側で判定されて返してくるエラーもきちんと見ておく必要もあると思います。

Firebaseが返すエラーはドキュメントに記載されていて、FirebaseSDKもそのエラーに従ってAuthErrorCodeというエラー定義がされています。

どのエラーが返されたかは下記のように取得しておきます。

// 変数errorはSDKが返してきたエラー
let errcd = AuthErrorCode(rawValue: (error as NSError).code)

これを例えば先ほど作った showErrorIfNeeded()に組み込んでみると、このような感じです。

private func showErrorIfNeeded(_ errorOrNil: Error?) {
    // エラーがなければ何もしません
    guard let error = errorOrNil else { return }
    
    let message = errorMessage(of: error) // エラーメッセージを取得
    let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
    present(alert, animated: true, completion: nil)
}

private func errorMessage(of error: Error) -> String {
    var message = "エラーが発生しました"
    guard let errcd = AuthErrorCode(rawValue: (error as NSError).code) else {
        return message
    }
    
    switch errcd {
    case .networkError: message = "ネットワークに接続できません"
    case .userNotFound: message = "ユーザが見つかりません"
    case .invalidEmail: message = "不正なメールアドレスです"
    case .emailAlreadyInUse: message = "このメールアドレスは既に使われています"
    case .wrongPassword: message = "入力した認証情報でサインインできません"
    case .userDisabled: message = "このアカウントは無効です"
    case .weakPassword: message = "パスワードが脆弱すぎます"
    // これは一例です。必要に応じて増減させてください
    default: break
    }
    return message
}

パスワードリセット

一般的なサインインの画面ではおなじみですが、 ユーザが「久しぶりにログインすることになったけど、認証情報を忘れてしまった」というケースも救ってあげる必要があります。

いわゆる「パスワードリセット」の機能ですが、これもFirebaseには備わっています。

フローとしてはこのように

  1. メールアドレスを入力してもらってボタンを押下すると、そのメールアドレスにメールが届く
  2. ユーザはメールに書かれたパスワードリセット用のリンクを踏んでもらう
  3. ユーザはリンク先のページで新しいパスワードを入力する
  4. そうすることでアカウントのパスワードは変更される

こちらも工数がかかりそうなフローですが Firebase はそのあたりを吸収してくれます。

前提

まず、以下のようなビューコントローラを作っておきます。

import UIKit
import Firebase

class PasswordResetViewController: UIViewController {
    
    @IBOutlet private weak var emailTextField: UITextField!
    
    @IBAction private func didTapSendButton() {
        // あとで実装
    }
}

実装

フロー自体は長いですが、アプリでやることは指定のメールアドレスにメールを送るようFirebaseに伝えるだけです。

@IBAction private func didTapSendButton() {
    let email = emailTextField.text ?? ""
    
    Auth.auth().sendPasswordReset(withEmail: email) { [weak self] error in
        guard let self = self else { return }
        if error != nil {
            // 送信完了画面へ
        }
        self.showErrorIfNeeded(error)
    }
}

sendPasswordReset()メソッドを呼び出せば、よしなにやってくれます。

メールの内容やリンク先のカスタマイズは、サインアップのときと同じようにFirebaseのコンソールから行うことができます。

サインアウト

会員機能にはサインアウト、ログアウトする機能はほしいところです。

サインアウトはその名の通りのsignOut()メソッドが用意されています。 このメソッドはこれまでと違いコールバックのクロージャは引き取らないなので、 下記のように実装します

@IBAction private func didTapSignOutButton() {
    do {
        try Auth.auth().signOut()
    } catch let error {
        showErrorIfNeeded(error)
    }
}

退会

ユーザはアプリが提供する会員サービス自体を辞めたいと思うこともあると思います。 Firebaseもそこは織り込み済みで、登録したアカウント自体を削除させることもできます。 いわゆる「退会機能」ですね。

@IBAction private func didTapWithdrawButton() {
    Auth.auth().currentUser?.delete() { [weak self] error in
        guard let self = self else { return }
        if error != nil {
            // 非ログイン時の画面へ
        }
        self.showErrorIfNeeded(error)
    }
}

Withdrawは日本語にすると「退会」の意味です。退会ボタンを押したときに現在ログイン中のユーザオブジェクトに対してdelete()メソッドで削除させています。 もちろん、アカウント削除なのでこの処理をする前にはダイアログなどでユーザに確認をとる必要があると思います。

ユーザ

FirebaseAuthenticationコンソールのユーザでは実際に管理しているユーザを見ることができます。

ユーザは

  • ユーザID uid
  • メールアドレス(識別子) email

で検索・閲覧することができますが、

  • 表示上の名前 displayName
  • 写真URL photoURL

などでは検索・閲覧はできないようです。

ユーザにもっと情報を与えたい(例えば誕生日などのプロフィール)場合は、Database (Cloud Firestore)とユーザを紐付けさせる必要があるようですが、それはまた別の機会でやりたいと思います。

最後に

FirebaseAuthenticationの「メールアドレス認証」の一部だけでも一般的な会員機能はこのようにして作成することができます。

他にもパスワード不要のメールリンク認証や、SNS認証、他サービスによる認証など 幅広い機能が備わっているので、触ってみたときにアウトプットしてきたいと思います。

詳しくは Firebase Authentication のリファレンスを参照ください