Skip to content
Permalink
Fetching contributors…
Cannot retrieve contributors at this time
224 lines (154 sloc) 7.29 KB
title lastmod publishdate author draft description tags youtube github vimeo pro
Advanced Firebase Auth with Custom Claims
2019-09-23 08:11:12 -0700
2019-09-23 08:11:12 -0700
Jeff Delaney
false
Implement role-based user authorization and security rules by adding custom claims to the Firebase Auth ID token.
firebase
auth
pro
362111823
true

Firebase allows developers to modify authentication ID tokens to provide fine-grained system access to authorized users. The follow lesson adds custom claims to the Firebase user record to build a role-based access control feature that is secured with Firestore rules.

Set Custom Claims from the Command Line

It is not possible (currently) to add custom claims from the Firebase Console. This means we need to write some code with Firebase Admin.

{{% box icon="scroll" %}} In the real world, you will most likely update custom claims from a Cloud Function. For example, the user makes a purchase and you update their claim as a paid user. {{% /box %}}

Initial Setup

Th easiest way to update custom claims is to write a simple Node.js script. If starting from scratch, run the commands below in an empty directory.

{{< file "terminal" "command line" >}} {{< highlight terminal >}} npm init -y npm i firebase-admin touch index.js {{< /highlight >}}

Service Account

Download your service account from the Firebase settings tab and save it as service-account.json in the project directory. ⚠️ The service account contains sensitive API keys that should not be exposed publicly.

{{< figure src="img/functions-service-account.png" caption="Download your service account" >}}

Update Claims with Firebase Auth

The script below calls the auth.setCustomUserClaims method to add a claims object to the user's ID token. You can save any data to the claims object as long as it can be serialized to JSON.

{{% box icon="hazard" %}} The max size of the claims object is 1000 bytes. It is a good practice to only save the bare minimum amount of data required for authorization logic. {{% /box %}}

{{< file "js" "index.js" >}} {{< highlight js >}} const admin = require('firebase-admin'); admin.initializeApp();

const auth = admin.auth();

const uid = 'NUc3KyG2RAfNuOrE0pzMMZegFHS2';

const customClaims = { roles: ['creator', 'auditor', 'admin'] };

(async() => { await auth.setCustomUserClaims(uid, customClaims); const user = await auth.getUser(uid); console.log('success', user) process.exit() })(); {{< /highlight >}}

Run the script from the command line making sure to set an environment variable that points to the service account.

{{< file "terminal" "command line" >}} {{< highlight terminal >}} GOOGLE_APPLICATION_CREDENTIALS=service-account.json node index.js {{< /highlight >}}

Now that we have a way to update claims, let's looks at a few common use cases.

Simple Admin Levels

Use Case

The most basic type of access control is to distinguish between a boolean value, such as paid/unpaid users or admins/non-admins. You might also fine tune access levels by also setting an integer value.

{{< highlight javascript >}} const customClaims = { admin: true, level: 7, }; {{< /highlight >}}

Firestore Rules

The Firestore rules grant read access to all admins, but limit write access to admins above level 5.

{{< file "firebase" "firebase.rules" >}} {{< highlight js >}} match /{path=**}/secrets/{id} { allow read: if request.auth.token.admin; allow write: if request.auth.token.level > 5; } {{< /highlight >}}

Role-based Access Control

Use Case

A very common requirement is role-based authorization. A user can be assigned a list of roles, which determine if that user is can perform a certain action. This strategy is common is CMS platforms - Wordpress for example has roles like super_admin, admin, editor, author, and so on.

{{< highlight javascript >}} const customClaims = { roles: ['creator', 'auditor', 'admin'], }; {{< /highlight >}}

Firestore Rules

We can assume a user with any role has read access, so checking by length is sufficient. For write access, we allow the admin to perform all actions, but limit the creator and auditor roles to specific types of writes.

{{< file "firebase" "firebase.rules" >}} {{< highlight js >}} match /{path=**}/secrets/{id} { allow read: if request.auth.token.roles.size > 0; allow create: if request.auth.token.roles.hasAny(['creator', 'admin']); allow update: if request.auth.token.roles.hasAny(['auditor', 'admin']); allow delete: if request.auth.token.roles.hasAny(['admin']); } {{< /highlight >}}

Tip: You could make the code above more concise my extracting the logic into a rules function.

{{< highlight js >}} match /{path=**}/secrets/{id} { allow create: if userHasRole(['creator', 'admin']); allow update: if userHasRole(['auditor', 'admin']); allow delete: if userHasRole(['admin']); }

function userHasRole(roles) { return request.auth.token.roles.hasAny(roles); } {{< /highlight >}}

Access Control List

Use Case

Imagine an app where users can belong to multiple organizations AND have customized access assigned for each organization. For example, each company may want to limit access to a specific department.

An access control list (ACL) is the opposite of role-based auth. Rather than give the user sweeping access to a path, the ID of the resource determines if access can be granted to that user.

{{< highlight javascript >}} const customClaims = { orgs: { uber: ['accounting', 'finance'], tesla: ['manufacturing', 'ai'], mcdonalds: ['nuggets'] }, } {{< /highlight >}}

Firestore Rules

The rule below matches the document ID to the user's custom claims. For example a read to orgs/tesla/departments/manufacturing would be allowed in this example.

{{< file "firebase" "firebase.rules" >}} {{< highlight js >}} match /{path=**}/orgs/{orgId}/departments/{departmentId} { allow read: if request.auth.token.orgs[orgId].hasAny([departmentId]) } {{< /highlight >}}

Validate Claims from the Frontend

Retrieve the ID Token

Custom claims can also be used to build the frontend UI. Get the result of the ID token like so:

{{< file "js" "app.js" >}} {{< highlight js >}} const token = await firebase.auth().currentUser.getIdTokenResult();

if (token.claims.admin) { // show admin UI } {{< /highlight >}}

Force Refresh of the ID Token

The ID token will refresh every 1 hour, or you can force it to refresh manually to propagate changes to the custom claims.

{{< file "js" "app.js" >}} {{< highlight js >}} firebase.auth().currentUser.getIdTokenResult(true); {{< /highlight >}}

👀 Alternative Approach

Learn how to implement roles without custom claims with the Firestore User Roles Tutorial

You can’t perform that action at this time.