# Group Security with Firestore Storage and Next.js

> Learn how to write Firebase security rules for groups of users

*Published: 2022-07-22*
*Canonical: https://makerkit.dev/blog/tutorials/group-security-firebase-storage-nextjs*

---

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: {% a className="underline text-black-500" href="/blog/tutorials/firestore-storage-rules" %}
Using Firestore in Firebase Storage Rules
{% /a %}

---

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**:

```tsx
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:**

```tsx
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:

 ```tsx {% title="/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
);
```

2) Secondly, we write a hook that takes care of calling this endpoint using our
hook `useApiRequest`:

 ```tsx {% title="~/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);
}
```

3) 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)`

 ```tsx {% title="~/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](https://github.com/sponsors/makerkit),
where we took care of all this code for you already.

Looking to build an AI-powered SaaS? Check out the [AI SaaS Starter Kit](/ai-saas-starter) for a complete foundation.
