Firebase Authentication - RBAC - Role Based Access Control

There is no official documentation, so I figure I could help on this. I have been using Firebase for years, and personally spent hours getting it to work with Dgraph. I will also try to answer some common questions that I had.

Step 1 - Create a Firebase Project

Very self explantatory. Firebase is a set of products. Firebase Realtime Database and Firestore are not used by default unless you set them up. Obviously, we are using Dgraph instead as the database.

Step 2 - Edit your schema, add Firebase Project ID

# Dgraph.Authorization {"Header":"X-Auth-Token","Namespace":"https://dgraph.io/jwt/claims","JWKURL":"https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com","Audience":["YOUR_PROJECT_ID"]}

Step 3 - Create Firebase Function for Custom Claims

You actually need to create a firebase function to use RBAC. In Dgraph 21.03, you will have access to the email address without having to create a custom claim (since it is already there in a firebase token but in the field level), but that does not solve the problem of roles. If that is not important to you, and every user has the same level of security, then you can skip this step… only after 21.03

Go to your project root…

a) - install firebase cli

npm i -g firebase-tools

b) - setup firebase

  • firebase init
  • just select Functions and continue… (If you get an issue, run firebase use --add and select your project.)
  • select typescript if you want and install dependencies, but skip eslint as I have problems with it working as expected sometimes

c) - create function

  • cd functions
  • navigate to functions/src/index.ts

Change the file to this after you update your admin email:

import * as functions from "firebase-functions";
import * as admin from "firebase-admin";

admin.initializeApp();

const ADMIN_EMAIL = 'YOUR ADMIN EMAIL';

exports.addUser = functions.auth
  .user()
  .onCreate((user: admin.auth.UserRecord) =>
    admin
      .auth()
      .setCustomUserClaims(user.uid, {
        "https://dgraph.io/jwt/claims": {
          "USER": user.email,
          "ROLE": user.email === ADMIN_EMAIL ? 'ADMIN' : 'USER'
        }
      }).catch((e: string) => console.error(e))
  );

This function with automatically create a custom claim when a user is created. This custom claim persists forever. It will give your personal email the ADMIN role on create.

d) - deploy the function

firebase deploy

Note: You should be able to see your live function in the firebase console under Functions. If you have issues, click the logs tab and select your function name.

Step 4 - Logging in the User

There are many different ways to login the user depending on your framework. Youtube has thousands of videos to get you started. For some basics:

This Repository or This One

Firebase can do regular login, google, etc. This should get you started as well.

Tip: You can use pipe operator to merge your login state with your User Type in the database…

Step 5 - Dealing with the token

While you may call dgraph graphql from Apollo, URQL, or a simple fetch, you must post the token as X-Auth-Token=token info in your header.

A few things:

  • All Firebase Token’s expire after 1 hour, but user sessions persist
  • You must refresh the token at that point
  • The recommended method is to store that token in the localStorage, check for expiry time, and refresh it
  • When you first login, the custom claim will not yet be present. You must login/logout or refresh the token

You need custom code to check for expired tokens. If you just want to get started, just grab the token and refresh it every time like so:

async getToken(): Promise<any> {
  return await new Promise((resolve: any, reject: any) => {
    firebase.auth().onAuthStateChanged((user: any) => {
      if (user) {
        user.getIdToken(true).then((token: string) => {
          console.log(token);
          resolve(token);
        }, reject);
      }
    });
  });
}
  • The firebase.auth() on onAuthStateChanged object will be different, depending on your framework. This checks for changes in the user object (logged in, new token, etc).
  • I also print it to console so you know it works with your custom claims…
  • The user.getIdToken(true) - set to true to refresh every time…

If you want to actually deal with local storage and do it the right way, here are a few ideas: here and here

I personally do not see any loading time differences in refreshing the token every time. Firebase also deals with a lot of this automatically for you, so it may not be worth messing with the cache.

You need to attach this token to every query or mutation sent to graphql. This means, you must have an async header, or call localstorage. Here is an URQL and Svelte example. URQL is recommended over Apollo as it is faster in all cases. React should be similar to this. Every step you add makes your configuration more complicated (async, subscriptions, ssr, etc), so this is the extreme case.

Step 6 - Changing the role

If you want to be able to edit a user’s role, you could create a callable function like this:

exports.changeRole = functions.https
  .onCall(async (data: any, context: functions.https.CallableContext) => {

    const userId = data.userId;
    const newRole = data.role;

    // get logged in user
    const currentUser = await admin.auth().getUser(context.auth?.uid as string);
    const currentClaims: any = currentUser.customClaims;

    // user to edit
    const editUser = await admin.auth().getUser(userId);
    const editClaims: any = editUser.customClaims;

    // must already be an admin to change role
    if (currentClaims['ROLE'] === 'ADMIN') {

      // you could also check for allowed Roles

      // add new claims, new user role
      admin.auth().setCustomUserClaims(userId, {
        "https://dgraph.io/jwt/claims": {
          "USER": editUser.email,
          "ROLE": newRole,
          ...editClaims
        }
      }).catch((e: string) => console.error(e));
    }
  });

Firebase makes it easy to call external functions…

const changeRole = firebase.functions().httpsCallable('changeRole');
changeRole({ userId: 'sleisllekt', role: 'MODERATOR' });

Step 7 - Security and Data Integrity - Current and the Future

type User @auth(
    add: { rule:  "{$ROLE: { eq: \"ADMIN\" } }"}
    delete: { rule:  "{$ROLE: { eq: \"ADMIN\" } }"}
) { 

As far as I know, there is no way to make sure you’re only adding a user based on the currently logged in user using only the @auth directive. …basically because you can’t query something that isn’t there yet. Please correct me if I’m wrong and post below.

+ 1 for pre-hooks and other backend secuirty options

So, I only see two options. 1.) Don’t worry who creates a user, or if that user is validated 2.) lambdas and firebase functions…

so for 2)

a) - create the user in your firebase function (the settings.ts file exports your URI and ADMIN_EMAIL)

import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
import fetch from 'node-fetch';
import { ADMIN_EMAIL, URI } from './settings';

admin.initializeApp();

const ADD_USER = `mutation addUser($user: AddUserInput!) {
  addUser(input: [$user]) {
    user {
      id
      email
      displayName
      createdAt
    }
  }
}`;

exports.addUser = functions.auth
  .user()
  .onCreate((user: admin.auth.UserRecord) =>
    admin
      .auth()
      .setCustomUserClaims(user.uid, {
        "https://dgraph.io/jwt/claims": {
          "USER": user.email,
          "ROLE": user.email === ADMIN_EMAIL ? 'ADMIN' : 'USER'
        }
      })
      // create user in dgraph
      .then(async () =>
        fetch(URI, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({
            query: ADD_USER,
            variables: {
              user: {
                email: user.email,
                displayName: user.displayName,
                createdAt: new Date().toISOString()
              }
            }
          })
        })
      ).catch((e: string) => console.error(e))
  );

This works great if you do not need to use the @auth directive with a token. That can be a problem. You cannot create a Firebase Token in Cloud Functions (you can create a Google Auth Token FWI) since it is dependent on the user (as far as I can find).

So, your only current real option option for 100% security and Data Integrity is to secure the User type, and create a lambda Mutation. In the lambda mutation, basically create a password field. Hash, encrypt, and prefix the password field in your Mutation. Since it is from backend to backend, you can check it easily. No headers or tokens required. For more on lambdas mutation security.

Step 8) - Add user from client

If facility is more important than security, and you don’t want to create the user on the backend, deal with mutation lambdas etc, you can simply create the user IFF the user is a new user:

 .signInWithPopup(provider)
      .then((credential: firebase.auth.UserCredential | any) => {
        // check for first signin
        if (credential.additionalUserInfo.isNewUser) {
          
        // execute dgraph mutation here to create the user

        }
        return null;
      });

Obviously this depends on your framework etc.

DONE - While you may not need all that craziness, I tried to post everything I know about Firebase Auth in Dgraph in one place.

- What I would like to see in the future -

There are many posts here on field validations, and auth headers. I personally would like to do away with all that. In a potential world, I would have one firebase function. It would not need custom claims, as the email is already in the token. It would simply call a mutation to graphql dgraph to add the user to the database on the create user hook.

  • All role validation would be done internally, as your Type would have a role node
  • Things like pre-hooks and the many other ideas you see here like firestore rules could allow you to check a role based on the database itself… Example, a user can only add a new user if his role in the dgraph database is ADMIN or if his token is the same as the email he is trying to add.
  • To update a user’s role, you update the role node.

if user.email === request.email etc…

I hope this helps some people, as a Secure Dgraph with Firebase can be complicated. If I missed something, or if you know of a better way to do things, or if this information becomes out of date, please let us know here.

Hope this helps someone,

J

7 Likes

hello @jdgamble555, nice to meet you

We’d like to include your article about Firebase auth on our Dgraph docs. If it’s ok, I can create a PR directly on our GitHub repository ( GitHub - dgraph-io/dgraph-docs: A native GraphQL Database with a graph backend ) with your content.

If you prefer to create the PR yourself, please feel free to submit it.

thanks for your collaboration, cheers

1 Like

Hi @damian,

I was going to update the article as soon as 21.03.X is released on Cloud Dgraph so that I could test it (with proof of concept on github). There are several ways to simplify the code (multiple JWT URLS, standard claims auth). I also learned a few things since I wrote the article :slight_smile:

So I can submit the PR right after the new release on Cloud DGraph if you guys are okay with it. I am assuming with Discourse I have 2 months to edit the post…

J

1 Like

Hello @jdgamble555,

That sounds great, let’s do that. I’ll wait for your PR so we can include it in our core docs.
(btw, our Cloud docs repo is available here: GitHub - dgraph-io/cloud-docs: Documentation for Slash GraphQL )

thanks again, cheers

1 Like

Hi @damian, can you guys unlock the post so that I can edit it now that 21.03 is out on the cloud, or should I just create a new post?

J


UPDATE: 6/23/21

I have updated this article since 21.03 to better use Custom Claims, and how to use Standard Claims:

Firebase Demo Apps:

There is really not much difference in the apps, but what is important is the fact that you don’t have to use firebase functions.

J

What do you mean by unlock? I’m not seeing any lock on this post. Not sure if it is a Discourse thing, but maybe I should turn it into a wiki?

Cheers.

Yes, Discourse has a 2 month limit where a post is locked from editing after that. Whoever is the Admin on this Forum should be able to turn that off.

J

I can’t see how tho. I have admin rights, but I see no option to unlock. In fact it says it is not locked.

I am not sure. I just know there is no edit button anymore (except for the title for some reason) like on other posts.

Maybe look at the settings for all posts expiration, or my trust level?

J

I’ll ping @dmai to find out what is happening. But you can always respond this topic if you need to add more info.

Yeah, I actually had a couple of things wrong when I wrote this topic that I updated in the new article (currently posted on dev.to)

J