スマホアプリ開発を加速する、Firebaseを使ってみよう

第5回Firebaseのデータをセキュアに保つ

前回までで、Firebase上にさまざまな方法でデータを保存し、用途に合わせたいろいろなコールバックを使ってデータを読み出す方法を学んできました。簡単なアプリならばこれまでの知識で十分作ることができますが、今回はサービスを安全に提供する上で欠かすことのできない「セキュリティ」について解説して行きたいと思います。

今回も、前回の連載に引き続き、例としてリアルタイムチャットサービスを取り上げます。

Firebaseを適切に設定することで、

  • メールアドレスとパスワードを使ってログイン処理を行う
  • チャットメッセージは適切にログインしたユーザしか閲覧もメッセージの投稿もできない

といったよくある機能を簡単に実現することができます。

ユーザ認証とアクセス制御

Authentication(認証)

「セキュリティ」を考える場合に、そのユーザが「誰であるのか」を確認することは不可欠です。 ⁠誰であるのか」がわかって初めて、

  • 「Aさんは /messages/01 を閲覧できる」
  • 「Bさんは /messages/02 を閲覧・書き込みできる」

といったアクセス制御が可能です。

この「誰であるのかを確認する」作業をAuthentication(認証)と言います。

Firebaseは次の表にある認証方法を提供しており、アプリに柔軟な認証機能を簡単に組み込むことができます。

認証の種類概要
大手SNS認証Facebook, GitHub, Google, Twitter アカウントを使った認証
パスワード認証Firebase自身にEメール、パスワードを登録し、それを使った認証
匿名ログイン一時的な匿名ログイン
カスタムログイントークンすでに自前で持っている認証基盤との連携

今回は、Firebase自身にEメール、パスワードを登録して、それを使ってユーザ認証を試してみます。

今後の連載中では、実践テクニックとしてTwitterアカウントを使った認証もご紹介する予定ですので、楽しみにしてください。

Authorization(認可)

ユーザ認証によって、そのユーザが「誰であるか」がわかり、したがって「何をすることができ、どこにアクセスすることができる」というようなことがわかります。こういった情報をAuthorization(認可)と呼びます。

Firebaseではこの認可情報でユーザを識別し、⁠アカウントダッシュボード」「Firebase Rules」と呼ばれる設定ルールに従って細やかなアクセス制御を実現することができます。

たとえば、以下のルールは/fooは誰でも閲覧できるが、書き込みはできない」という意味になります。詳しくは本連載で解説して行きたいと思います。

{
  "rules": {
    "foo": {
      ".read": true,
      ".write": false
    }
  }
}

パスワード認証

今回の連載では、Firebase自身にEメール、パスワードを登録する「パスワード認証」を利用します。

パスワード認証を利用するには、ダッシュボードから「Login & Auth」を選択し、⁠Enable Email & Password Authenticaion」にチェックを入れます。

図1 Enable Email & Password Authentication
図1 Enable Emai

これで準備は整いました。

さっそくFirebase上にユーザを作成するコードを見て行きましょう。

ユーザの作成

まずは以下のコードを実行してみてください。

Firebase ref = new Firebase("https://<YOUR-FIREBASE-APP>.firebaseio.com");

ref.createUser("fu.shiroyama@gmail.com", "abcd1234", new Firebase.ValueResultHandler<Map<String, Object>>() {
    @Override
    public void onSuccess(Map<String, Object> result) {
        Log.d(TAG, "Successfully created user account with uid: " + result.get("uid"));
    }
    @Override
    public void onError(FirebaseError firebaseError) {
        Log.d(TAG, "error: " + firebaseError.getMessage());
    }
});

まず、1行目でFirebaseのルート参照を取得します。

次に、ユーザを作成するためにcreateUser()メソッドを呼び出します。createUser()は第1引数にEメールアドレス、第2引数にパスワードを取ります。

認証の結果は、第3引数のFirebase.ValueResultHandlerインタフェースで受け取ることができます。成功した場合はonSuccess()の第1引数に認証結果がMap形式で渡されます。ここではユーザを一意に識別するuidを取得しています。

以下のようにログ出力されれば成功です。

D/Firebase: Successfully created user account with uid: 17613df6-4d51-4148-9adf-e948c6699b53

失敗した場合はonError()の第1引数にFirebaseError形式でエラーが渡されます。FirebaseErrorからはgetMessage()getDetail()でエラー理由を取得することができます。

たとえば、すでに存在するユーザをもう一度作成しようとした場合は、以下のようにエラー出力されます。

D/Firebase: error: The specified email address is already in use.

登録済みユーザはダッシュボードから確認することができます。

図2 Registered Users
図2 Registered Users

uid

このuidは、Firebaseが提供するさまざまな認証方法によらず、一意であることが保証されています。同じEメールアドレスとパスワードを使ったとしても、⁠Twitter認証」「Facebook認証」といった具合に認証プロバイダが異なれば、きちんと違うユーザと見なされますので、ここで取得できるuidをアプリで一意なユーザのユニークキーとして利用するのは正しい使い方です。

ユーザの認証

ユーザの作成ができたので、さっそくパスワード認証を試してみましょう。

パスワード認証にはauthWithPassword()メソッドを利用します。第1引数にEメールアドレス、第2引数にパスワードを指定し、認証結果は第3引数のFirebase.AuthResultHandlerで受け取ります。

ref.authWithPassword("fu.shiroyama@gmail.com", "abcd1234", new Firebase.AuthResultHandler() {
    @Override
    public void onAuthenticated(AuthData authData) {
        Log.d(TAG, "User ID: " + authData.getUid() + ", Provider: " + authData.getProvider());
        Log.d(TAG, "expires: " + authData.getExpires());
    }
    @Override
    public void onAuthenticationError(FirebaseError firebaseError) {
        Log.d(TAG, "error: " + firebaseError.getMessage());
    }
});

認証に成功すると、onAuthenticated()の第1引数にAuthDataオブジェクトが渡されます。以下のようにログ出力されれば成功です。

D/Firebase: User ID: 17613df6-4d51-4148-9adf-e948c6699b53, Provider: password
D/Firebase: expires: 1463468344

エラーはユーザ登録の時と同様、FirebaseError形式で受け取ります。以下は、認証パスワードを間違えた際のエラー例です。

D/Firebase: error: The specified password is incorrect.

AuthData

AuthDataはさまざまな認可情報が詰まった非常に重要なデータです。AuthDataからは以下のような情報を取得することができます。

プロパティ概要
uidユーザを一意に識別するID
token認証トークン
expires認証の有効期限を表現するUNIXタイム
provider認証プロバイダ情報

他にも、認証プロバイダの種類によって、プロフィール画像等の重要な情報を取得することができます。

また、expiresの期限が過ぎるとこの認証は無効になってしまうため、プログラマが適切に管理してユーザに再認証を促す必要があります。認証の有効期限はデフォルトで24時間です。

認証の解除

認証は、有効期限が切れる前でも ref.unauth() メソッドを呼び出すことでいつでも失効させることができます。ユーザログアウト時等に利用することが考えられるでしょう。

アクセス制御

ここまでで、Firebase上にユーザを作成し、パスワード認証で認証する方法をみてきました。ここからは、認証したユーザの特定のパスに対するアクセス制御について確認してみたいと思います。

アクセス制御は、ダッシュボードの「Security & Rules」タブを選択し、⁠Firebase Rules」ルールを編集することで設定することができます。

図3 Security & Rules
図3 Securit

ここでは、前回に引き続き、チャットメッセージ一覧を格納する/messagesを使い、ここへのアクセスは認証済みのユーザにのみ許可するように設定変更してみたいと思います。

まず、rules直下の.read.writeを両方ともfalseにします。

{
  "rules": {
    ".read": false,
    ".write": false
  }
}

Firebase Rulesは、上の階層から下の階層に伝播するように設計されており、上位のパスで許可されたアクセス件を下位のパスの設定で覆すことができないようになっています。したがって、まずは最上位のルールをfalseにし、その後任意のパスのルールを設定していくのが大原則となります。

次に、messagesに認証済みのユーザのみアクセス可能とします。認証済みのユーザは、uidという組み込み変数に必ず値が入るので、uid != nullとすることで認証済みのユーザかどうかを判定します。

{
  "rules": {
    ".read": false,
    ".write": false
    "messages": {
      ".read": "auth != null",
      ".write": "auth != null"
    }
  }
}

設定が終わったら、⁠SAVE RULES」ボタンを押してルールを保存してください。

図4 Save Rules
図4 Save Rules

試しに、未認証の状態で/messagesの情報の取得を試み、以下のようなエラーが出力されることを確認してください。

D/Firebase: onCancel: Permission denied

次に、ユーザを認証して、これまでの連載同等メッセージ情報が取得できることを確認してください。きちんと情報が取得できれば成功です。

さまざまな組み込み変数

Firebase Rulesには、uid以外にもさまざまな組み込み変数が用意されており、非常に柔軟な認証を実現することができます。

組み込み変数概要
nowFirebaseサーバの現在時刻
rootFirebaseデータベースのrootパス
newDataこれから書き込まれるデータ
data現在このパスに存在するデータ
$variablesデータのIDに動的にマッチする変数。後述
auth認証情報

この内、$variablesは非常によく使うので使い方を解説します。

$ variables

$variablesは、特定のパスの下のデータのIDに動的にマッチする変数です。$に任意のアルファベットで変数名を付けるだけで、任意のデータにマッチします。

たとえば、次のようなルールがあった場合、$user_idは任意のユーザIDににマッチします。マッチさせたIDはアクセス制御に利用します。

{
  "rules": {
    "users": {
      "$user_id": {
        ".write": "$user_id === auth.uid"
      }
    }
  }
}

auth組み込み変数を使ってuidを比較し、特定のuidを持つユーザが自分のuidと同じパスにアクセスした場合にのみデータの書き込みを許可しています。

組み込み変数と$variablesを組み合わせると、思いのままのアクセス制御が可能です。ぜひ、いろいろ工夫してアクセスルールを考えてみてください。

今後の連載では実践テクニックとして細やかなアクセス制御のルールを公開予定です。楽しみにしていてください。

まとめ

今回の連載では、ユーザ認証とアクセス制御を組み合わせることで、アプリに柔軟でセキュアな認証基盤を簡単に付与することができるのを見て行きました。

さて、次回は、Firebaseのデータベース構造についてより深く踏み込み、どうすればより高速で効率的なデータ設計を実現できるのか見て行きたいと思います。どうぞお楽しみに。

おすすめ記事

記事・ニュース一覧