Group Security with Firestore Storage and Next.js

Learn how to write Firebase security rules for groups of users

·6 min read
Cover Image for Group Security with Firestore Storage and Next.js

As a result of the new changes in Firebase Storage allowing you to read Firestore data, we suggest to use the new method outlined in the following article: Using Firestore in Firebase Storage Rules


Firebase Storage's security rules are relatively simple to write when the entity to protect is individual: for example, making sure that the current user can perform certain actions, or that an individual organization can access certain assets in a bucket.

What's slightly trickier with Firebase Storage is writing rules that affect several users or groups. What makes it particularly hard is the lack of context we can use in the security rules: for example, we cannot read any data from Firestore, and are confined to the session's authentication data of the signed-in user.

Among the authentication data, we can access within the Firebase Storage security rules, we also find custom claims, which are some metadata that you can attach to each user. This is great, but the bad news is that the space is extremely limited, hence we cannot simply store all the groups an individual belongs to.

For example, if your user belongs to an organization, you may want the user to be able to read all the assets belonging to the organization. Unfortunately, this is simpler said than done.

While the Firebase documentation does indeed mention a solution involving custom tokens for this specific problem, but it does not go as far as showing a proper implementation.

Thankfully, we at Makerkit have thought of a simple and painless solution, which we will describe in this blog post.

Scenario: A shared folder within an Organization

The following is an extremely common scenario in any SaaS that supports users of an organization, project, or team: sharing a folder among team members of the same team (or whatever we want to call it).

Writing rules that can guard against nonauthorized access to a specific organization would be fairly simple, but how can we write the logic necessary to protect access to the users that do not belong to the team?

  1. One way would be to use Firebase Storage from the Firebase Admin on the server, and block access using server-side logic. Unfortunately, this is cumbersome and hard to maintain, which defeats the purpose of using Firebase Storage straight from the client. So, in most cases, this is not the way to go.

  2. Another way can be using Custom Claims, and storing the group information (such as an ID) in the user's session data. Because we can access these in the Firebase Storage security rules, we can write them such to block or grant access to the user.

Let's assume our Organization has the ID 123, and the current user belongs to such organization. Now, let's try to write a security rule that knowing the user's custom claims would be able to grant access to the user:

So, let's write a rule that, given an organization organizationId, can grant access to only the users who have a custom claim named organizationId that matches the path being written.

The path we want to write to is: /{organizationId}/uploads/{fileName=**}:

match /{organizationId}/uploads/{fileName=**} { allow create, read: if organizationId == request.auth.token.organizationId; allow delete, update: if resource.metadata.organizationId == request.auth.token.organizationId; }

This rule would prevent users who do not have the correct custom claim organizationId to rear, write, update or delete any item within that path.

The question now is, how can we assign the organizationId custom claim to the user?

We can do this when:

  • the server renders a page
  • the user changes organization

Let's see how we do it in Makerkit!

Assigning Custom Claims

On any guarded page (eg. server-side protected) of the application, we have the possibility to assign the current organization ID (passed via session cookie) to the user's metadata.

The logic is simple:

  1. fetch custom claims
  2. get the current organization ID from the cookies
  3. if these are different, then update the custom claims with the current organization ID

Assigning custom claims at page load

Assigning custom claims is also reasonably simple using the Firebase Authentication SDK.

1) First, we define the function to update the custom claims:

function setOrganizationIdCustomClaims( userId: string, organizationId: string, existingClaims: UnknownObject ) { const auth = getAuth(); return auth.setCustomUserClaims(userId, { ...existingClaims, organizationId, }); }

2) Here is a snippet of code with the logic above:

const userDidChangeOrganization = authOrganizationId !== currentOrganizationId; const shouldUpdateTokenClaims = !authOrganizationId || userDidChangeOrganization; if (shouldUpdateTokenClaims) { await setOrganizationIdCustomClaims( user.id, organization.id, customClaims ); }

Where to add this logic?

Remember: the snippet above should be called before rendering the page, ideally within the getServerSideProps function.

Assigning custom claims when the user switches organization

If your users can switch to another organization, we need to make sure to update the metadata with the current organization ID.

Notice: instead of adding a new organization, we simply update it due to the limited amount of space we can store within the custom claims.

You can achieve this in two different ways:

  1. Simply refresh the page when the user changes organization (which sometimes can be good for avoiding state management issues)
  2. A more seamless way that involves sending an HTTP request to update the current user's custom claims

In Makerkit, we opted for the second option, which is a bit more complex but offers a better UX.

An API endpoint to update the user's custom claims

  1. First of all, we need an API endpoint capable of updating the user's custom claims using the newly selected organization:
/api/organizations/token.tsx
import { NextApiRequest, NextApiResponse } from 'next'; import { getAuth } from 'firebase-admin/auth'; import { withAuthedUser } from '~/core/middleware/with-authed-user'; import { withPipe } from '~/core/middleware/with-pipe'; import { withMethodsGuard } from '~/core/middleware/with-methods-guard'; import { getCurrentOrganization } from '~/lib/server/organizations/get-current-organization'; import { forbiddenException, notFoundException } from '~/core/http-exceptions'; export async function organizationTokenHandler( req: NextApiRequest, res: NextApiResponse ) { const userId = req.firebaseUser.uid; const organizationId = req.cookies.organizationId; if (!userId || !organizationId) { return forbiddenException(res); } const organization = await getCurrentOrganization(userId); if (!organization) { return forbiddenException(res); } const auth = getAuth(); const user = await auth.getUser(userId); if (!user) { return notFoundException(res); } await auth.setCustomUserClaims(userId, { ...(user.customClaims ?? {}), organizationId, }); return res.send({ success: true }); } export default withPipe( withMethodsGuard(['POST']), withAuthedUser, organizationTokenHandler );
  1. Secondly, we write a hook that takes care of calling this endpoint using our hook useApiRequest:
~/lib/organizations/hooks/use-update-organization-id-token
import { useApiRequest } from '~/core/hooks/use-api'; export function useUpdateOrganizationIdToken() { const path = `/api/organizations/token`; return useApiRequest(path); }
  1. Finally, we call the hook when the user changes the organization. This involves two steps:
  1. Executing the HTTP request written above
  2. Hard refreshing the user's custom claims using the Firebase SDK's API auth.currentUser?.getIdTokenResult(true)
~/components/OrganizationsSelector.tsx
import { useUpdateOrganizationIdToken } from '~/lib/organizations/hooks/use-update-organization-id-token'; // within the Component const [updateOrganizationIdToken] = useUpdateOrganizationIdToken(); // this is called by your component when the organization ID changes const onChange = useCallback(async () => { await updateOrganizationIdToken(); await auth.currentUser?.getIdTokenResult(true); }, [auth.currentUser, updateOrganizationIdToken]); return <OrganizationsListBox onChange={onChange} />;

Conclusion

And that's all!

With the code above, we can easily add Group Security to our organizations, teams, projects, and any entity where multiple users are a part of in Firebase.

I hope you enjoyed this post. Please consider subscribing to our newsletter or purchasing a copy of Makerkit, where we took care of all this code for you already.



Read more about Tutorials

Cover Image for Building an AI Writer SaaS with Next.js and Supabase

Building an AI Writer SaaS with Next.js and Supabase

·57 min read
Learn how to build an AI Writer SaaS with Next.js and Supabase - from writing SEO optimized blog posts to managing subscriptions and billing.
Cover Image for Announcing the Data Loader SDK for Supabase

Announcing the Data Loader SDK for Supabase

·8 min read
We're excited to announce the Data Loader SDK for Supabase. It's a declarative, type-safe set of utilities to load data into your Supabase database that you can use in your Next.js or Remix apps.
Cover Image for Adding AI capabilities to your Next.js SaaS with Supabase and HuggingFace

Adding AI capabilities to your Next.js SaaS with Supabase and HuggingFace

·20 min read
In this tutorial, we will learn how to use add AI capabilities to your SaaS using Supabase Vector, HuggingFace models and Next.js Server Components.
Cover Image for Building an AI-powered Blog with Next.js and WordPress

Building an AI-powered Blog with Next.js and WordPress

·17 min read
Learn how to build a blog with Next.js 13 and WordPress and how to leverage AI to generate content.
Cover Image for Using Supabase Vault to store secrets

Using Supabase Vault to store secrets

·6 min read
Supabase Vault is a Postgres extension that allows you to store secrets in your database. This is a great way to store API keys, tokens, and other sensitive information. In this tutorial, we'll use Supabase Vault to store our API keys
Cover Image for Introduction to Next.js Server Actions

Introduction to Next.js Server Actions

·9 min read
Next.js Server Actions are a new feature introduced in Next.js 13 that allows you to run server code without having to create an API endpoint. In this article, we'll learn how to use them.