Firebase Storage and Authorization Rules engine 'helloworld'

2020-05-10

A couple days ago i finally started to tinker with firebase storage and its rules engine.

I did know about the rules engine for many years now and knew it was pretty flexible but never really noticed that it can use both request and resource attributes. What i mean by that is a firebase rule can evaluate and grant access at runtime using both something contained within the request (eg, id_token) and something about the target being accessed (eg a resource attribute).

As a concrete example, you can setup a rule that decodes a users JWT id_token, extract out some custom claim there, then use that field value in a rule. Well, thats nice but the real cool thing is it can also use some metadata on the resource being accessed. In our case, is some custom metadata we associated with the Storage Object.

This small snippet details a simple POC similar to the ‘getting started’ guide for firebase:

We will create a firebase project and enable Storage.

An admin will upload a file to Storage and set some custom metadata on that: eg,

    Metadata:               
        owner:              12345

An admin will associate a Custom Claim with a given user’s entry:

“groupId”: “12345”,

Create a firebase rule that says “allow a user to read a file only if their id_token has a custom claim called “groupId” and the value contained in that matches the Storage Objects metadata value for “owner”

THAT pretty cool to me!

You can set this up and play around with other firebase rules and constructs!

You can find the code links in the following gists:


Setup

1 Create Firebase Project

2 Enable Email/Password authentication

3 Enable Firebase Storage

Add Rule `match`
```
    // allow the user to write to a path that includes the unique userID
    match /{userId}/{fileName} {
        allow write: if request.auth.uid == userId;
    }

    // allow the user read only if the GCS's object has metadata called "owner" which has the 
    // value "12345"  **and** that value is present in the id_token presented to firebase
    // Also, allow write only if the users token includes a claim where the groupID field
    // matches the path to write to. (i.,e if the claim has groupId=12345, the user can only write
    // to gs://project.appspot.com/12345/filename)
    // With these rules, if a user writes a new file, it must have metadata "owner" set to
    // the groupID...otherwise, it can't get read back later by any user.

    match /{groupId}/{fileName} {
        allow read: if resource.metadata.owner == request.auth.token.groupId;
        //allow read: if request.auth.token.groupId == groupId;
        allow write: if request.auth.token.groupId == groupId;
    }
  }
```

images/rule.png

4 Download Firebase Service Account

Save it as fb-svc-account.json

images/service_account.png

5 Create Application Under “Project Settings > Add App” Name it anything (eg, storageapp)

Copy the Configuration Settings

```javascript
<script>
  // Your web app's Firebase configuration
  var firebaseConfig = {
    apiKey: "AIzaSyDS32ruqLyTQGSoFUcV01g2rS8",
    authDomain: "sa-broker.firebaseapp.com",
    databaseURL: "https://sa-broker.firebaseio.com",
    projectId: "sa-broker",
    storageBucket: "sa-broker.appspot.com",
    messagingSenderId: "653262156156",
    appId: "1:653262156156:web:6e8c116d19d5c6"
  };
  // Initialize Firebase
  firebase.initializeApp(firebaseConfig);
</script>
```

images/appconfig.png

6 Run “client”

Edit login.js Add firebaseConfig into the app

7 Create user

The following setp will create a user with the following username and password `123456`

```bash
$ node login.js createuser sal@somedomain.com 123456
  user created
    sal@somedomain.com
```

Note at this point a user was created dynamically

  ![images/user.png](images/user.png)

8 Run “server”

Edit admin.js, Add configuration and service Account

```javascript
var serviceAccount = require("/path/to/fb-svc-account.json");

admin.initializeApp({
    credential: admin.credential.cert(serviceAccount),

    apiKey: "AIzaSyDS32ruqLyTQGSoFUcV",
    authDomain: "sa-broker.firebaseapp.com",
    databaseURL: "https://sa-broker.firebaseio.com",
    projectId: "sa-broker",
    storageBucket: "sa-broker.appspot.com",
    messagingSenderId: "653262156156",
    appId: "1:653262156156:web:6e8c116d19d5"    

});
```

9 Add custom claim to user

The following will attach a custom claim called `groupId` to the user with value `12345`


```
$ node admin.js updateuser sal@somedomain.com 12345
```

Note, the `customClaim` section is empty because whats shown there is the original insert of the user.
If you run the script again, you'll see the cliaclaimsms are returned back.

```json
{
  "admin": true,
  "groupId": "12345",          <<<<<<<<<<<
  "iss": "https://securetoken.google.com/sa-broker",
  "aud": "sa-broker",
  "auth_time": 1589582639,
  "user_id": "N14MY2U5t6TRT7drPytTp779EUv1",
  "sub": "N14MY2U5t6TRT7drPytTp779EUv1",
  "iat": 1589582640,
  "exp": 1589586240,
  "email": "sal@somedomain.com",
  "email_verified": false,
  "firebase": {
    "identities": {
      "email": [
        "sal@somedomain.com"
      ]
    },
    "sign_in_provider": "password"
  }
}
```

10 Upload a file

As an admin, upload a file to Storage with some metadata.  In this case the metdata is `owner=12345`
```
echo "hello world" >/tmp/file.txt

node admin.js upload /tmp/file.txt 12345/file.txt 12345
```

Note, the file as just created:

![images/file.png](images/file.png

has custom metadata

```bash
$ gsutil stat gs://sa-broker.appspot.com/12345/file.txt
gs://sa-broker.appspot.com/12345/file.txt:
    Creation time:          Fri, 15 May 2020 20:23:36 GMT
    Update time:            Fri, 15 May 2020 20:31:02 GMT
    Storage class:          STANDARD
    Content-Length:         12
    Content-Type:           text/plain; charset=utf-8
    Metadata:               
        owner:              12345
    Hash (crc32c):          8P9ykg==
    Hash (md5):             b1kCrCNwJL3QwXbLkwY9xA==
    ETag:                   CMLPoLTZtukCEAI=
    Generation:             1589574216722370
    Metageneration:         2
```

11 Download file as user

Now the user can download the file...login as the user and  try to download the file in path `12345/file.txt`:

```
$ node login.js download sal@somedomain.com 123456 12345/file.txt
```

Thats it!. nothing big. I just wrote this up for my own use

Note that the Storage bucket is owned entirely by the firebase’s own service account:

images/sa-owner.png

References

This site supports webmentions. Send me a mention via this form.