🐟

Firebase developerClaimsを使ったセキュアなアクセス制御

2023/03/31に公開

Firebase developerClaimsとは

signInWithCustomTokenでサインインするトークンをcreateCustomTokenで生成しますが、引数にdeveloperClaimsをキーバリューで登録できます。これら登録した値は、トークンを生成する毎にclaimsを含むトークンを生成するため、セッション単位でクレームの中身を変更するといったことができます。

DIMBULA では、ユーザの所属を示すIDを設定しています。FirestoreやStorageのリソースにアクセスする際は、ログインしたユーザが適切なアクセス権を持ったユーザであるか、つまり同じ組織のユーザかどうかを確認するために利用しています。

import * as firebase from "firebase-admin"

const org = "hoge"
const token = await firebase.auth().createCustomToken(uid, {org})

tokenをデコードすると以下のような構造を示します。

{
  "alg": "RS256",
  "typ": "JWT"
}
{
  "aud": "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit",
  "iat": 1680226320,
  "exp": 1680229920,
  "iss": "xxx@appspot.gserviceaccount.com",
  "sub": "xxx@appspot.gserviceaccount.com",
  "uid": "yyy",
  "claims": {
    "org": "hoge"
  }
}

https://firebase.google.com/docs/auth/admin/create-custom-tokens?hl=ja

クレームの使い方

request.auth.tokenにクレームが設定されます。今回の例では、以下のようにアクセスできます。

request.auth.token.org

https://firebase.google.com/docs/rules/rules-and-auth?hl=ja

ルールへの適用

DIMBULA は、E2Eテスト結果の閲覧を、同じ組織に属するユーザのみが閲覧できるアクセス制御を設けています。「ログインしている且つ取得するデータの所有組織がユーザの所属と同じ」といったルールをFirestoreで作る場合は、こういった方法で記述できます。
また、サブコレクションがある場合も、親ドキュメントのIDのオブジェクトを取得し、必要なフィールドと比較することで、アクセス権限を確認することができます。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    function isAuthenticated() {
      return request.auth != null;
    }
    function isValidOrg(database) {
      return request.auth.token.org == resource.data.org;
    }
    function isValidOrgByTestId(database, testId) {
      return request.auth.token.org == get(/databases/$(database)/documents/tests/$(testId)).data.org;
    }
    match /tests/{testId} {
      allow read: if isAuthenticated() && isValidOrg(database);
      allow write: if false;
      match /testPlans/{testPlanId} {
        allow read: if isAuthenticated() && isValidOrgByTestId(database, testId);
        allow write: if false;
      }
    }
  }
}

Storageの場合も同様なルールを設けることができます。

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    function isAuthenticated() {
      return request.auth != null;
    }
    function isValidOrg(org) {
      return request.auth.token.org == org;
    }
    match /test/{org}/{allPaths=**} {
      allow read: if isAuthenticated() && isValidOrg(org);
      allow write: if false;
    }
  }
}

ユーザ単位でクレームを作る

トークン生成時にクレームを作る場合の他、ユーザ単位でもクレームを作成できますので、ニーズに合わせて使い分けることができます。
https://firebase.google.com/docs/auth/admin/custom-claims?hl=ja

ルール適用の注意

Firebase StorageとFirestoreには、フロントエンドからのアクセスを制御するfirestore.rulesstorage.rulesを定義できます。ただしルールは、フロントエンドのアクセスが制限されるのであって、Functionsといったサーバー側からアクセスする場合は、このルールが適用されないので、注意が必要です。サーバー側で制御を行う場合は、IAMによるリソースへのアクセス管理 を設定する必要があります。

https://firebase.google.com/docs/firestore/security/get-started?hl=ja

注: サーバー クライアント ライブラリは、すべての Cloud Firestore セキュリティ ルールをバイパスし、代わりに Google アプリケーションのデフォルト認証情報を使用して認証を行います。REST API または RPC API 用のサーバー クライアント ライブラリを使用する場合は、Cloud Firestore 用の Identity and Access Management(IAM)を設定してください。

最後に

サーバー側からのアクセスは、このルールをバイパスしてしまうので、ケアの仕方が違いますが、フロントエンドでリソースへのアクセス制御は、予期しないトラブルを防ぐためにも施しておくことが望ましいでしょう。もしアクセス権がなければ、403の応答になりますので、その点も注意が必要かもしれません。

また、PostgreSQLでは、行セキュリティポリシー という機能があり、似たようにアクセス制御が可能だと理解してます。Firebaseを含むBaaSは、データストアーをクライアント側からアクセスできる便利な仕組みがある一方で、直接作用するリスクもあるため、このあたりの整備は一定必要になるかと思います。

Discussion